DAY 008 / SPIRAL SAFE
Spiral Safe: rebuilding a passkey-gated signing platform across eight repositories
How I reconciled eight repositories into a WebAuthn-authorized signing platform with a Vault boundary, scoped API keys, usage accounting, four demos, Raft deployment intent, and explicit Nitro gates.
01 / THE PRODUCT
Authorize a remote wallet with a passkey—without putting its private key in the browser.
Spiral Safe is a development-stage, WebAuthn-authorized signing and account platform. A dApp can discover a Solana wallet, ask a browser authenticator to approve a bound signing ceremony, and receive signed bytes while the chain key remains behind a Vault plugin.
The finished development pass spans eight repositories: the extension and Wallet Standard adapter, a direct SDK, the HTTP and Vault services, account and usage storage, developer and administrator consoles, deployment scaffolds, demos, and a canonical evidence map. It is a coherent prototype now. It is not an audited custody or billing product.
“Keeps keys out of the browser” is narrower than “non-custodial.” The Vault plugin still loads private-key bytes in process memory to sign, and Vault operators remain part of the trusted computing base. This note uses the narrower claim throughout.
02 / THE ARCHAEOLOGY RUN
The first deliverable was a trustworthy map of what already existed.
I began by cloning every public repository in the Spiral Safe organization and retaining its independent Git history. The first pass built the extension, SDK, adapter, legacy Hugo site, and token-list tooling; started the Vault plugin, Express adapter, and browser demo; exercised a bounded /check probe; and documented the existing Nitro-oriented branch.
That work mattered because the organization profile, website, issue roadmap, extension, and backend described overlapping versions of the product. The runtime was not one monorepo with one authoritative boundary. It was a set of experiments that had to be reconciled before new features could be trusted.
2026-08-30 · core integration
├── extension 05e9b31 backend-mediated Wallet Standard provider
├── sdk 197ab53 authenticated chain-aware HTTP client
├── wallet-adapter 2922ba6 callable Wallet Standard feature routing
├── services 5c80fc5 hardened service, WebAuthn, Raft, Veil baseline
└── specs bf7bb8b architecture, runbook, security, verification
2026-08-31 · product and accounting layer
├── extension ef79738 secure annotated extension demo
├── sdk def536c operation-bound completion contract
├── services 34ff343 accounts, billing consoles, recordings, Nitro admission
└── specs 38e3431 reconciled product and quorum documentationThe exact final main-branch heads were checked again for this article. The core repositories do not currently publish hosted CI workflows, so a public commit proves the source is present—not that every recorded command ran on GitHub infrastructure.
03 / THE FAULT LINE
The browser and backend disagreed about who owned the key.
The extension prototype generated a Solana key inside the browser, stored its secret in local storage, transported it through extension messages, and exposed methods that were not fully implemented. Separately, the service prototype already placed a key behind a Vault secrets-engine plugin and used WebAuthn, but its HTTP boundary had no application authorization and relied on a development root token.
browser extension prototype
├── generated its own Solana keypair
├── placed secretKey in localStorage
├── moved that key through extension messages
└── advertised wallet methods that were still stubs
Vault service prototype
├── created a server-side key behind a Vault plugin
├── used WebAuthn ceremonies to authorize signing
├── exposed the adapter with broad CORS and no client authorization
└── used one development root token between HTTP and Vault
finding
└── the browser and backend disagreed about who owned the keyThis was not a one-function bug. Making the extension “work with the backend” meant replacing its custody model, completing Wallet Standard behavior, defining the browser bridge, authenticating the HTTP service, binding concurrent ceremonies, and preserving a chain-neutral seam inside the Vault plugin.
04 / EIGHT REPOSITORIES
Four repositories contain current client or runtime code.
RUNTIME REQUEST PATH
ALTERNATIVE CLIENTS
SYSTEM CONTEXT
| Repository | Layer | Role | Pin |
|---|---|---|---|
| extension | Browser | Vault-backed Solana Wallet Standard provider | ef79738 |
| wallet-adapter | Package | Feature registration and routing; no credentials | 2922ba6 |
| sdk | Client | Typed bearer-authenticated service contract | def536c |
| services | Core | Vault plugin, HTTP API, billing, deployment, demos | 34ff343 |
| specs | Evidence | Canonical architecture, security, runbook, verification | 38e3431 |
| token-list | Non-runtime | Inherited Jupiter data/tooling fork | 7bf90dc |
| website | Non-runtime | Legacy Hugo marketing site | e6ce502 |
| .github | Non-runtime | Organization profile and license | 88cf033 |
A request does not traverse all four. The browser path bundles the wallet adapter into the extension and calls the service; a direct SDK client calls the same service contract without the extension.
The token list is an inherited Jupiter data fork and is not imported by the signer. The Hugo website is separate from both current demos. The organization profile contains aspirational production language. The specs repository is the canonical map, but two of its inventory sentences still describe the state immediately before the final pushes; this note therefore pins the final remote heads instead of repeating those stale counts.
05 / THE CUSTODY BOUNDARY
The page handles intent; the trusted worker handles credentials; Vault handles keys.
The injected provider runs in the page’s JavaScript world and speaks a versioned bridge to an isolated content script. The extension service worker then checks the browser-reported top-level tab, frame, and exact origin before it calls the service. Public configuration may return to the page, but the bearer token and ceremony bindings do not.
page context
├── may send: account name, chain, public payload bytes
├── may receive: WebAuthn options, address, signed result
└── must never receive: API token, Vault token, wallet private key
extension trusted worker
├── exact allowlisted origin + top-level frame
├── 2 MB request cap + correlated request ID
├── API token in chrome.storage.session
└── ceremony bound to tab, frame, origin, user, chain, operation
service + Vault plugin
├── derive tenant and scopes from authentication
├── bind payload to a random two-minute ceremony
├── require WebAuthn user verification
└── load the chain key inside the plugin only when signingThe API token and non-secret pending ceremony state live in trusted-context-only browser session storage. That allows a Manifest V3 worker to suspend during a passkey prompt without writing those values to durable page-visible storage. Session storage reduces persistence; it is not a hardware secret store.
Backend and RPC URLs are also reduced to their origins before page-facing inspection, because provider credentials are sometimes embedded in URL paths. The full endpoints remain available only to the worker that performs network requests.
06 / THE WEBAUTHN BRIDGE
Every signature is a two-part ceremony with server-owned continuity.
registration
POST /init
→ navigator.credentials.create(options)
→ POST /create { ceremonyId, credential }
signing
POST /signin { chain, operation, payload }
→ navigator.credentials.get(options)
→ POST /complete { chain, operation, ceremonyId, credential }
server invariants
├── registration and signing states are distinct
├── ceremony IDs are random, expiring, and single-use
├── the requested operation must equal the stored operation
├── a nonzero signature-counter regression disables the credential
└── malformed completion consumes the ceremony before validationThe plugin generates a random, two-minute ceremony ID and stores the operation and payload behind it. Completion consumes that ID before it validates attacker-controlled credential data, preventing replay at the cost of making a malformed or cancelled completion non-retryable. Concurrent tabs receive independent ceremony state instead of racing one mutable user slot.
User verification is required. A cryptographic software authenticator covers the complete server-side lifecycle, including replay, user-verification, ceremony-binding, and signature-counter regression cases. Physical Touch ID, Windows Hello, synchronized passkeys, roaming security keys, and a full browser matrix remain separate release gates.
07 / SERVICE HARDENING
Four credentials now have four different jobs.
API key
└── account + tenant + scopes + username allowlist
WebAuthn assertion
└── one registration or signing ceremony
console session
└── developer or administrator web role + CSRF state
Vault token
└── service workload → Vault; never a browser/client inputProduction account mode authenticates a high-entropy API key, then derives the account, tenant, scopes, and non-empty username allowlist on the server. The caller cannot select another tenant in the request body. Unauthorized scope or username is rejected before Vault. Console sessions never act as wallet API bearer credentials, and Vault tokens are workload credentials rather than client input.
The HTTP adapter now has exact CORS origins, identifier and base64 validation, a decoded payload limit, request IDs, stable error mappings, timeouts, security headers, and bounded process-local rate limiting. Kubernetes mode exchanges a service-account JWT for a short-lived, engine-scoped Vault token and refreshes it before lease expiry.
08 / CHAIN-SPECIFIC SIGNING
A reusable ceremony does not make every chain operation safe by default.
The Go plugin now selects a ChainSigner for key generation, address derivation, and operation-specific signing. WebAuthn and storage code stay shared, while each chain owns its parser, signature encoding, and supported-operation boundary.
type ChainSigner interface {
Generate() (privateKey []byte, address string, err error)
Address(privateKey []byte) (string, error)
Sign(privateKey, payload []byte, operation string) (SignResult, error)
}
solana
├── Ed25519 address
├── legacy transaction signing
└── raw message signing
ethereum
├── secp256k1 + EIP-55 address
└── EIP-191 personal-message signing only| Chain | Key / address | Implemented | Boundary |
|---|---|---|---|
| Solana | Ed25519; base58 address | Legacy transactions and raw messages | Versioned transactions are rejected; trusted intent display is absent. |
| Ethereum | secp256k1; EIP-55 address | EIP-191 personal messages | No transaction signing, EIP-712, EIP-155, nonce, fees, simulation, or broadcast. |
A panic boundary turns malformed Solana parser input into a normal signing error rather than losing the Vault plugin process. Ethereum proves the abstraction with one EIP-191 message path. It does not establish “anychain” custody, Ethereum transaction support, or a generic safety policy for future chains.
09 / THE ACCOUNT AND BILLING LAYER
Reserve usage before custody work; export it after a trusted result.
PostgreSQL now owns accounts, plans, password-derived console users, keyed session-token hashes, scoped API-key hashes, quotas, usage reservations, committed usage, an outbox, and Stripe webhook claims. A newly created API key’s plaintext is revealed once; only its non-secret prefix, scope, user allowlist, and peppered digest persist.
request
├── authenticate one-time-reveal API key from stored HMAC digest
├── resolve account, tenant, scopes, users, plan, period
└── reserve active_wallet or transaction_signed usage
Vault fails
└── cancel a newly created reservation
Vault succeeds and stored operation matches
├── commit usage + durable outbox row in PostgreSQL
├── return signature or signed transaction
└── export stable event asynchronously to Metronome
provider boundary
└── Metronome ingest creates no charge until an externally verified
customer/contract/rate-card → Stripe mapping existsThe first successful action for a wallet in a billing period commits one active-wallet unit. Only successful transaction completion commits a transaction unit; message signatures do not. Idempotency keys prevent duplicate accounting, and the service compares the requested completion operation with the value returned from Vault before exposing output or charging usage.
Stripe Checkout and Customer Portal helpers, signature-verified webhooks, and asynchronous Metronome export are implemented. They have not produced a live invoice or collected a charge. Production also fails closed until an administrator attests the current Metronome-customer, contract/rate-card, and Stripe-customer mapping for each account.
10 / THREE DEPLOYMENT STORIES
Runnable Compose, rendered Kubernetes, and Nitro admission are different evidence levels.
DISPOSABLE LOCAL
PRODUCTION-INTENT
HARDWARE EXPERIMENT
The production Kustomize overlay describes two service replicas and three Vault StatefulSet members using Integrated Storage/Raft, retained data and audit volumes, TLS, anti-affinity, a disruption budget, Kubernetes authentication, restricted pod security, and NetworkPolicies. The production profile deletes the local browser client.
PostgreSQL, KMS workload identity, ingress, public TLS, provider egress, StorageClass, recovery-key custody, snapshots, monitoring, and backup/restore remain operator inputs. The manifests were rendered during the development session; they were not applied to a real production cluster.
11 / VEIL AND NITRO ENCLAVES
One image can describe bootstrap and join—but it still cannot form a real quorum.
The historical Nitriding build path was replaced with a pinned Veil revision. One Linux/amd64 image can render either a bootstrap or join configuration from a strict, short-lived manifest. The enrollment script verifies nonce-bound PCR, TLS-certificate, source, and image expectations before it may call an external enrollment hook, then checks that the candidate appears as the expected Raft voter with healthy Autopilot state.
one pinned source image
├── mode: bootstrap | join
├── pinned source revision + Veil revision
├── unique Raft node ID and advertised addresses
├── shared KMS auto-unseal identity
├── fresh admission window ≤ 30 minutes
└── verification must pass before enrollment hook
three still-missing production boundaries
├── private cross-host L4 routing
├── rollback-protected durable enclave storage
└── attestation-bound manifest, TLS, and KMS delivery
therefore
└── same-image simulation ≠ EIF ≠ live Nitro quorumThe local simulation proves that one source image can render distinct node identities and controlled retry_join configuration. The enrollment test uses mock verifier, hook, peer-list, and Autopilot responses. No EIF was created, no enclave ran on EC2, and no live PCR, NSM, KMS, storage, cross-host route, or quorum behavior was demonstrated.
“Anyone can run a node” is incompatible with ordinary Vault Raft replication: membership grants a node custody-state access. A genuinely open operator network needs an explicit admission and governance protocol—and potentially threshold or MPC custody rather than a shared Vault database.
12 / FOUR ANNOTATED WALKTHROUGHS
The videos show product wiring—not hardware, cloud, billing, or capacity proof.
One final Playwright run produced four separate 1440×900 recordings. Each one contains five highlighted steps, a title and explanation overlay, a machine-readable timeline, and a permanent FIXTURE MODE · SYNTHETIC LOCAL DATA badge. They exercise real product code around named substitutes.
Visual transcript for this silent fixture recording
There is no narration or audio track. The five visible chapters are:
- 01Configure the trusted worker with loopback-only fixture settings.
- 02Discover the extension through Wallet Standard.
- 03Register through navigator.credentials.create and a virtual authenticator.
- 04Authorize a deterministic message signature.
- 05Authorize a deterministic legacy Solana transaction signature.
Visual transcript for this silent fixture recording
There is no narration or audio track. The five visible chapters are:
- 01Review the backend custody boundary.
- 02Select the Ethereum message-signing demonstration.
- 03Register the deterministic fixture wallet.
- 04Prepare a clearly synthetic message.
- 05Complete navigator.credentials.get and show the fixture signature.
Visual transcript for this silent fixture recording
There is no narration or audio track. The five visible chapters are:
- 01Sign in with a clearly fake fixture account.
- 02Review the seeded developer overview.
- 03Open scoped API-key management.
- 04Fill—but deliberately do not submit—the synthetic key form.
- 05Inspect active-wallet and transaction usage against fixture limits.
Visual transcript for this silent fixture recording
There is no narration or audio track. The five visible chapters are:
- 01Sign in through the actual administrator route with fixture credentials.
- 02Review the synthetic system posture.
- 03Open the tenant directory.
- 04Inspect a clearly labeled seeded account.
- 05Review pending and delivered usage-outbox state.
13 / LOAD AND ENDPOINT COVERAGE
The harness covers every route while refusing to masquerade as a capacity benchmark.
The guarded runner distinguishes safe probes from mutation scenarios, requires an explicit token, refuses remote targets and remote plaintext tokens without separate opt-ins, observes redirects instead of following them, and creates a unique ceremony ID for every request. Its parser and safety contract pass nine fresh tests at the pinned service commit.
The final session result was 260 requests across 26 method/path scenarios with zero unexpected statuses or client errors. Wallet mutation cases use successful /init plus expected negative or missing-user completion paths. The run does not measure successful WebAuthn-signing throughput, sustained capacity, multi-replica contention, soak behavior, or production latency.
14 / WHAT BROKE
The difficult bugs lived at boundaries, not inside the happy-path signature.
The key lived on both sides of the architecture
The original extension generated a local key while the backend already owned a Vault key. Integration began by deleting that disagreement, not by polishing either UI.
Redirect following hid the load result
Automatic redirects erased the status the endpoint matrix intended to inspect. The harness now preserves redirects and classifies exact expected outcomes.
Concurrent probes reused one-use ceremony state
The load runner needed a fresh ceremony identifier per request because replay protection is part of the contract, not noise to disable for testing.
Correct CORS rejection looked like a failure
A console login probe without the exact Origin correctly returned 403. The load expectation changed; the security boundary did not.
Completion lost the requested operation
The SDK and service now carry transaction versus message through completion and compare it with Vault’s stored ceremony before returning output or committing usage.
A deterministic demo hid persuasive security moments
Virtual WebAuthn removes operating-system UI, and the key form intentionally stops before secret reveal. Those are safer recordings, but they require explicit explanation.
15 / VERIFIED RESULT
The exact public pins reproduce the self-contained suites and keep external evidence labeled.
$ npm test # extension: 15 / 15
$ npm test # sdk: 6 / 6
$ npm test # wallet-adapter: 2 / 2
$ npm test # services: 45 pass + 1 PostgreSQL skip locally
$ npm run load:test # harness: 9 / 9
$ npm run recording:test # recorder: 8 / 8
$ go test ./... # Vault/chain core: 10 / 10 in pinned Go image
$ go test ./... # Nitro config renderer: 8 / 8 in pinned Go image
$ npm run build # extension production bundle passed with warnings
$ npm run build:demo # extension demo bundle passed with warnings
remote main pins
├── extension ef797388f9f38a9b5f2879ee62c74bf2714a886e
├── sdk def536cdbef94b3456b89c1e824d131ef17d2bda
├── services 34ff343bcb5a5f81ecceff7f8ed3102ead53645b
├── specs 38e34313a3c8046ac567177a49fa79e95f5f8425
└── four other repositories pinned in the evidence ledger below| Evidence class | Observed result | Safe conclusion |
|---|---|---|
| Final public Git pins | Eight remote main refs match the commits linked by this note. | Code, tests, manifests, and documented boundaries exist at those exact revisions. |
| Fresh no-network rerun | 103 tests passed: extension 15, SDK 6, adapter 2, service 45, load 9, recorder 8, Vault/chain Go 10, and Nitro renderer 8; one database test skipped. | Named client, service, harness, recorder, and Go behavior reproduced on one machine; no hosted CI claim. |
| Pinned verification record | Full 46/46 service run with ephemeral PostgreSQL, Go plugin checks, rendered manifests, and provider-mocked billing tests are recorded. | Committed session evidence; this site audit did not independently rerun PostgreSQL, Kubernetes, Stripe, or Metronome. |
| Fixture walkthrough run | Four 1440×900 recordings, twenty annotated steps, and no recorder warnings. | Product wiring and presentation with a virtual authenticator, fake wallet, fake credentials, and loopback services. |
| Live all-route fixture load | 260 requests across 26 method/path scenarios with zero unexpected statuses or client errors. | Endpoint/control-plane smoke with negative signing paths—not successful-ceremony throughput or capacity. |
Across the current pinned suites, 103 tests passed and one PostgreSQL integration test skipped. The fresh service run intentionally had no TEST_DATABASE_URL, so 45 service tests passed; the pinned verification record preserves the earlier full 46-test run against an ephemeral PostgreSQL database. Its SDK count of five predates the final operation-binding test; the final SDK pin now passes six.
The extension production and demo bundles also build. Webpack reports non-blocking size warnings—462 KiB for the injected production bundle and 279 KiB for the demo bundle. That is a real performance debt, not a reason to hide a passing build behind a green checkmark.
16 / CURRENT TRUTH
The system is integrated and demonstrable. Production custody remains gated.
| Gate | Current evidence | Required next proof |
|---|---|---|
| Human authorization | Software and virtual authenticators exercise the ceremony. | Run physical Touch ID, Windows Hello, passkey, and security-key matrices across supported browsers. |
| Signing intent | WebAuthn binds an opaque payload and operation. | Decode, simulate, apply policy, and show a trusted human-readable recipient, amount, program, fee, and nonce. |
| Identity and recovery | Password consoles, scoped keys, and account-local roles exist. | Add OIDC/SSO, MFA, forced reset, credential recovery, delegated approvals, and centralized abuse controls. |
| Billing | Stripe/Metronome adapters, webhooks, mapping gates, quotas, and outbox exist. | Prove sandbox Checkout through invoice and collection, reconcile mappings, decide tax, and test provider failure. |
| Kubernetes and Vault | Skaffold/Kustomize describe a three-node Raft baseline. | Apply to a real cluster; test TLS, KMS, failover, backups, restore, upgrades, monitoring, and database HA. |
| Nitro quorum | Pinned Veil image and strict bootstrap/join admission scaffold exist. | Implement routing, durable anti-rollback storage, attestation-bound delivery, then run EIF/EC2/NSM and quorum tests. |
| Release assurance | Local tests and dated dependency scans are recorded. | Add core CI, SBOMs, provenance, signed releases, license review, penetration testing, and an external custody audit. |
WebAuthn currently approves an opaque challenge. There is no trusted transaction display, policy engine, wallet recovery, multi-credential management, or complete result-recovery protocol if a valid signature response is lost. A compromised allowlisted dApp can still choose confusing bytes for the user to approve.
There has been no independent custody, billing, or security audit; no penetration test; no production PostgreSQL recovery drill; no live Stripe charge; no real Kubernetes apply; and no Nitro enclave quorum. Until those gates close, use disposable devnet keys and test messages only.
17 / FILE GUIDE
The final system keeps custody, product, deployment, and evidence surfaces inspectable.
Spiral-Safe/
├── extension/ # Manifest V3 provider and trusted worker
├── wallet-adapter/ # Wallet Standard registration and feature routing
├── sdk/ # direct TypeScript HTTP client
├── services/
│ ├── backend.go # Vault secrets-engine plugin
│ ├── signer.go # Solana and Ethereum signer boundary
│ ├── src/ # HTTP adapter, consoles, billing runtime
│ ├── migrations/ # PostgreSQL account and usage schema
│ ├── load-test/ # guarded endpoint matrix
│ ├── recording/ # four annotated Playwright flows
│ ├── deploy/ # Kustomize local/production overlays
│ └── nitro/ # pinned Veil same-image admission scaffold
├── specs/docs/ # architecture, API, runbook, security, evidence
├── token-list/ # inherited data fork; outside runtime
├── website/ # legacy Hugo site; outside runtime
└── .github/ # organization profile; outside runtimeARCHITECTURE
Trust boundaries and flows
Browser, account, billing, Vault, Kubernetes, and Veil boundaries in one map.
EXTENSION
Wallet Standard integration
Trusted-worker design, unpacked demo, supported methods, and production notes.
ACCOUNTING
API keys, usage, and billing
PostgreSQL, quotas, idempotency, Checkout, webhooks, and outbox semantics.
NITRO
Veil admission boundary
Same-image manifest, verification order, membership checks, and missing gates.
RECORDINGS
Annotated walkthrough contract
Four real product surfaces, deterministic substitutes, redaction, and timelines.
SECURITY
Release blockers
Current controls, threat assumptions, residual risks, and production gates.
18 / WHAT IS NEXT
The next milestone should prove one real boundary at a time.
19 / EVIDENCE LEDGER
The article is pinned to final public source and labels session artifacts separately.
- Spiral-Safe/.github@88cf033
- Spiral-Safe/extension@ef79738
- Spiral-Safe/sdk@def536c
- Spiral-Safe/services@34ff343
- Spiral-Safe/specs@38e3431
- Spiral-Safe/token-list@7bf90dc
- Spiral-Safe/wallet-adapter@2922ba6
- Spiral-Safe/website@e6ce502
- W3C Web Authentication: Level 3
- HashiCorp Vault Integrated Storage
- Pinned Veil revision 2b8c06c
- AWS Nitro Enclaves attestation guide
The shared development conversation supplied chronology and the final local recording manifest. Repository claims were checked against the public pins above. The four embedded WebMs are ignored development-session artifacts published here with explicit fixture labeling; their recorder source, flow definitions, and boundaries are committed in services. No private conversation URL, credential, trace, or private model deliberation is published.