Skip to main content

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.

Deadman Vault uses Supabase as its PostgreSQL backend to store vault state, beneficiary records, heartbeat logs, and notification history. The schema is managed through four versioned migration files in supabase/migrations/ — each one builds on the last and must be applied in order. Row Level Security is enabled on every table with zero permissive policies, meaning nothing can be read or written through the browser-facing anon key; all database access flows through server-side API routes that use the privileged service role key.

Prerequisites

  • A Supabase account and project (the free tier is sufficient for development and moderate production workloads)
  • Supabase CLI installed locally
  • Your project’s URL, anon key, and service role key — available in Project Settings → API

Migration Steps

1

Create a Supabase project

Go to supabase.com and create a new project. Choose a region close to your Vercel deployment for lowest latency. Once the project is provisioned, navigate to Project Settings → API and copy three values:
  • Project URLNEXT_PUBLIC_SUPABASE_URL
  • Anon / public keyNEXT_PUBLIC_SUPABASE_ANON_KEY
  • Service role keySUPABASE_SERVICE_ROLE_KEY
Add all three to your .env.local.
2

Install the Supabase CLI

npm install -g supabase
Verify the installation:
supabase --version
3

Link your local project to Supabase

From the repository root, log in and link the CLI to your remote project:
supabase login
supabase link --project-ref <your-project-ref>
Your project ref is the subdomain of your Supabase URL — for https://xyzabcde.supabase.co it is xyzabcde.
4

Run the migrations

npm run db:migrate
This runs supabase db push, which applies all pending migration files in supabase/migrations/ in filename order against your remote database.You should see output confirming all four migrations were applied successfully.
5

Verify the schema

Open the Supabase Dashboard → Table Editor and confirm the following tables exist:
  • vaults — core vault state, keeper escrow columns, status lifecycle
  • beneficiaries — per-vault recipients with allocation and vesting config
  • heartbeats — immutable log of every owner check-in
  • notifications — deduplication log for emails sent by the keeper
You can also verify via the SQL editor:
SELECT table_name
FROM information_schema.tables
WHERE table_schema = 'public'
ORDER BY table_name;

Migration Reference

001_initial_schema.sql — Core Tables

Creates the four foundational tables and their indexes:
  • vaults — one row per vault. Tracks the owner’s Stacks address, heartbeat interval (days), last and next deadline block heights, total deposited balance, lifecycle status, and optional metadata (name, message to beneficiaries, owner email).
  • beneficiaries — one row per beneficiary per vault. Stores the recipient’s address, name, email, percentage allocation, optional vesting period (days), and claim state.
  • heartbeats — append-only log of every successful check-in. Each row records the block height, transaction ID, and deposited amount for audit purposes.
  • notifications — deduplication log used by the keeper to avoid re-sending the same email for the same vault event.
Key columns on the vaults table:
CREATE TABLE vaults (
  id                       UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  owner_address            TEXT NOT NULL,
  owner_email              TEXT,
  heartbeat_interval_days  INTEGER NOT NULL DEFAULT 30,
  last_heartbeat_block     BIGINT,
  heartbeat_deadline_block BIGINT,
  total_deposited          NUMERIC DEFAULT 0,
  status                   TEXT NOT NULL DEFAULT 'active'
    CHECK (status IN ('active', 'warning', 'grace_period', 'triggered', 'claimed')),
  message_to_beneficiaries TEXT,
  vault_name               TEXT,
  created_at               TIMESTAMPTZ DEFAULT NOW(),
  updated_at               TIMESTAMPTZ DEFAULT NOW()
);
Indexes are created on vaults(owner_address), vaults(status), beneficiaries(vault_id), beneficiaries(address), and heartbeats(vault_id).

002_keeper_escrow.sql — Custodial Keeper Columns

Adds four columns to vaults to support the testnet custodial keeper design:
ColumnTypePurpose
tx_idTEXTTransaction ID of the vault creation deposit on-chain
keeper_addressTEXTThe dedicated Stacks address generated for this vault’s keeper
keeper_key_ciphertextTEXTAES-256-GCM encrypted keeper private key (iv:authTag:ciphertext)
gas_fundedBOOLEANWhether the keeper address has been funded with STX for gas
This migration also expands the status constraint to include the funding state — the initial state before the keeper address receives its STX gas drip and the owner makes their first deposit.

003_enable_rls.sql — Row Level Security Lockdown

Enables Row Level Security on all four tables with no permissive policies:
ALTER TABLE vaults         ENABLE ROW LEVEL SECURITY;
ALTER TABLE beneficiaries  ENABLE ROW LEVEL SECURITY;
ALTER TABLE heartbeats     ENABLE ROW LEVEL SECURITY;
ALTER TABLE notifications  ENABLE ROW LEVEL SECURITY;
This is intentional. The Supabase anon key (which is embedded in the client bundle) cannot read or write any row in any table. Every database operation in the application goes through a server-side API route that instantiates a Supabase client with the SUPABASE_SERVICE_ROLE_KEY, which bypasses RLS by design. This prevents any direct-database attack vector from a compromised browser session or a leaked anon key.

004_vesting_schedule.sql — Vesting Enforcement

Adds the columns needed to enforce per-beneficiary vesting cliffs precisely:
TableColumnTypePurpose
vaultstriggered_atTIMESTAMPTZThe wall-clock timestamp when the vault flatlined. Provides a stable reference point for vesting calculations independent of when the keeper cron next runs.
beneficiariesvesting_release_atTIMESTAMPTZThe earliest timestamp at which this beneficiary’s share may be released, calculated as triggered_at + vesting_days.
A partial index accelerates the keeper’s retry sweep — querying for unclaimed beneficiaries whose vesting cliff has matured:
CREATE INDEX idx_beneficiaries_vesting_release
  ON beneficiaries(vesting_release_at)
  WHERE claimed = FALSE;

RLS Design Rationale

Deadman Vault deliberately takes a “zero trust from the browser” stance on its database. Here is why:
  • The NEXT_PUBLIC_SUPABASE_ANON_KEY is public — it ships in every user’s browser and in the built JavaScript bundle.
  • Enabling RLS with no policies means that even if someone extracts the anon key, they cannot query vault data, beneficiary addresses, or keeper ciphertexts.
  • All reads and writes are mediated by Next.js API routes running on Vercel’s server-side runtime, where the SUPABASE_SERVICE_ROLE_KEY is available as a secret environment variable.
  • This means there is no Supabase Realtime subscription or client-side query anywhere in the codebase — every data fetch goes through an API route.
Never run npm run db:reset against a production Supabase project. The db:reset script drops and recreates the entire database schema — it will permanently delete all vault records, beneficiary configurations, and heartbeat history. Reserve db:reset exclusively for local development environments.

Build docs developers (and LLMs) love