Deadman Vault’s security architecture is built around a single governing principle: no sensitive key material or raw database rows ever reach the browser. Every vault creates a dedicated server-side Stacks keypair (the “keeper”), whose private key is encrypted at rest with AES-256-GCM and decrypted only inside server-side API routes. Supabase Row-Level Security locks all four database tables from public access, routing every read and write through the service-role key on the server. This layered approach prevents browser-side exposure of private keys and vault metadata alike — though it also means the server is custodial, a tradeoff that is explicitly acceptable for testnet and explicitly unacceptable for mainnet.Documentation Index
Fetch the complete documentation index at: https://mintlify.com/natureloved/DeadMan-Vault/llms.txt
Use this file to discover all available pages before exploring further.
Custodial Keeper Design
Keypair Generation
When a vault is created, the server callsgenerateKeeperAccount() from src/lib/keeper/account.ts, which uses @stacks/transactions’ randomPrivateKey() to produce a fresh, dedicated Stacks keypair:
Encryption at Rest
The private key is immediately encrypted before being written to the database. Encryption uses AES-256-GCM (src/lib/keeper/crypto.ts) with the KEEPER_ENCRYPTION_KEY environment variable as the symmetric key:
keeper_key_ciphertext as a self-contained iv:authTag:ciphertext string (all hex). All three components needed for decryption are bundled together in a single field — the GCM auth tag also authenticates the ciphertext, so any tampering is detected at decrypt time.
KEEPER_ENCRYPTION_KEY must be exactly 64 hex characters (32 bytes). Generate a new key with:
Decryption is Server-Side Only
The keeper private key is only decrypted inside server-side API routes (and the keeper cron job). Thedecrypt() function is never called from client components. The call site in src/lib/keeper/settlement.ts is representative:
keeper_key_ciphertext is Stripped Before the Browser
serializeVault() in src/lib/supabase/serialize.ts maps the raw Supabase row to the camelCase shape returned to the frontend. It explicitly omits keeper_key_ciphertext:
KEEPER_ENCRYPTION_KEY were somehow exposed separately.
What the Owner Signs
The vault owner never signs FlowVault contract calls directly. The only on-chain action the owner ever signs is a plain SIP-010 USDCxtransfer to the keeper address — at vault creation and at each heartbeat. The keeper then performs all FlowVault operations (set-routing-rules, deposit, withdraw) using its server-held key.
This is why the custodial pattern exists in the first place:
flowvault-v2 keys all vault state off tx-sender with no vault-id argument. Only the address that called deposit can ever call withdraw. The keeper bridges this gap on testnet.What the Keeper CAN Do
The keeper’s capabilities are intentionally broad and represent the core custodial risk:- Withdraw the locked USDCx balance from FlowVault
- Transfer USDCx to beneficiary addresses
- Call
set-routing-rulesanddepositon the FlowVault contract - Any other Stacks transaction signed with its key
Database Security
All four Supabase tables are protected by Row-Level Security. Migration003_enable_rls.sql enables RLS with zero public policies:
SUPABASE_SERVICE_ROLE_KEY, which bypasses RLS by design. The browser-exposed NEXT_PUBLIC_SUPABASE_ANON_KEY is configured in the Supabase project but has zero table access in Deadman Vault.
Security Properties Summary
| What is protected | Mechanism |
|---|---|
| Keeper private key (plaintext) | AES-256-GCM encryption; never leaves server memory |
| Keeper private key (ciphertext) | Stripped from all client-facing responses by serializeVault() |
| Vault records and beneficiary data | RLS with no public policies; accessed only via service-role key |
| Heartbeat and notification logs | Same RLS lockdown as vaults |
| FlowVault contract calls | Signed exclusively by the keeper server-side |
| Owner wallet | Only signs a USDCx transfer; never signs contract calls |
Production-Grade Alternative
The README is explicit: the custodial keeper is a testnet-only shortcut. The production-grade alternative, as described in thesrc/lib/keeper/ source comments, is:
A non-custodial Clarity wrapper contract with a permissionless claim function, so no server ever holds a key that can move real funds.
In this design:
- The owner deposits directly into a custom Clarity contract (not via a keeper intermediary)
- The contract enforces the heartbeat deadline and beneficiary split on-chain
- Any caller can invoke
claimonce the deadline passes — no server key is needed - The keeper cron would still monitor deadlines and send notifications, but would never hold signing authority over funds
Attack Surface Summary
KEEPER_ENCRYPTION_KEY Compromise
If the encryption key is leaked, all stored keeper key ciphertexts become decryptable. Every vault’s keeper private key would be compromised. Rotate immediately by re-encrypting all rows — but note that key rotation in production requires a coordinated migration.
SUPABASE_SERVICE_ROLE_KEY Exposure
The service-role key bypasses RLS and has full table access. Exposure would allow reading all vault records, beneficiary data, and notification logs. It would not expose keeper private keys (those are AES-encrypted), but would expose all metadata.
Server-Side Code Injection
Because decryption happens server-side, any code execution vulnerability in the Next.js API layer could reach the decrypted keeper keys in memory. Keep dependencies audited and Next.js patched.
Keeper Address Underfunded
If the keeper account runs out of STX gas, it cannot broadcast transactions. The vault enters a degraded state.
KEEPER_FUNDING_PRIVATE_KEY provides an automated STX drip; manual testnet-faucet funding is the fallback.