···2233Last verified: 2026-03-13
4455+## Latest Updates
66+- **V008**: Rebuilt accounts with nullable password_hash (mobile accounts have no password); added pending_did column to pending_accounts for DID pre-store retry resilience
77+58## Purpose
69Owns SQLite connection lifecycle and schema migration for the relay's server-level database.
710Keeps database concerns out of handler code and provides a reusable pool+migration API
···3740- `migrations/V005__pending_accounts.sql` - pending_accounts table: pre-provisioned account slots (id, email, handle, tier, claim_code)
3841- `migrations/V006__devices_v2.sql` - Rebuilds devices: replaces did FK (accounts) with account_id FK (pending_accounts); adds platform, public_key, device_token_hash; also rebuilds sessions, oauth_tokens, refresh_tokens (cascade due to FK references)
3942- `migrations/V007__pending_sessions.sql` - pending_sessions table: id, account_id (FK→pending_accounts), device_id (FK→devices), token_hash (UNIQUE), created_at, expires_at; used by POST /v1/accounts/mobile to issue a pre-DID session for the DID-creation step
4343+- `migrations/V008__did_promotion.sql` - Rebuilds accounts with nullable password_hash (mobile accounts have no password); adds pending_did column to pending_accounts for DID pre-store retry resilience
···11+-- V008: DID promotion support
22+-- Applied in a single transaction by the migration runner.
33+--
44+-- 1. Rebuilds the accounts table with nullable password_hash.
55+-- Mobile-provisioned accounts (via POST /v1/dids) have no password;
66+-- only accounts created via POST /v1/accounts have a password_hash.
77+-- SQLite does not support ALTER COLUMN, so a full table rebuild is required.
88+--
99+-- 2. Adds pending_did to pending_accounts for retry-safe DID pre-storage.
1010+-- Populated by POST /v1/dids before calling plc.directory (pre-store pattern).
1111+-- If the promotion transaction fails after plc.directory accepts the op,
1212+-- a client retry detects this non-NULL value and skips the directory call.
1313+1414+-- ── Rebuild accounts with nullable password_hash ─────────────────────────────
1515+1616+CREATE TABLE accounts_new (
1717+ did TEXT NOT NULL,
1818+ email TEXT NOT NULL,
1919+ password_hash TEXT, -- NULL for mobile-provisioned accounts
2020+ created_at TEXT NOT NULL,
2121+ updated_at TEXT NOT NULL,
2222+ email_confirmed_at TEXT,
2323+ deactivated_at TEXT,
2424+ PRIMARY KEY (did)
2525+);
2626+2727+INSERT INTO accounts_new
2828+ SELECT did, email, password_hash, created_at, updated_at, email_confirmed_at, deactivated_at
2929+ FROM accounts;
3030+3131+DROP TABLE accounts;
3232+3333+ALTER TABLE accounts_new RENAME TO accounts;
3434+3535+CREATE UNIQUE INDEX idx_accounts_email ON accounts (email);
3636+3737+-- ── Add pending_did to pending_accounts ──────────────────────────────────────
3838+3939+ALTER TABLE pending_accounts ADD COLUMN pending_did TEXT;
+4
crates/relay/src/db/mod.rs
···5656 version: 7,
5757 sql: include_str!("migrations/V007__pending_sessions.sql"),
5858 },
5959+ Migration {
6060+ version: 8,
6161+ sql: include_str!("migrations/V008__did_promotion.sql"),
6262+ },
5963];
60646165/// Open a WAL-mode SQLite connection pool with a maximum of 1 connection.