Files
smirk-backend-core/migrations/20260622000001_initial_schema.sql
T
jwinterm 416fa53c17 feat: Nostr-native data models + initial schema
models/db.rs: User keyed by pubkey_hash/nostr_pubkey/username (pubkey_hash and
seed_fingerprint peppered at rest); Wallet, Session (platform=extension|web|nostr),
UserKey, GrinSlatepack, AuditLog. Dropped all telegram/discord/social-identity
columns and every tip/social/swap struct (swaps land as their own feature unit).

migrations/20260622000001_initial_schema: consolidated clean schema; enum types;
UNIQUE on identity columns; FK ON DELETE (CASCADE/SET NULL) for self-service
erasure; indexes on every lookup column. PostgreSQL 13+.
2026-06-22 07:10:55 -04:00

121 lines
6.2 KiB
SQL

-- Initial schema for smirk-backend-core.
--
-- Nostr-native identity: a user is keyed by pubkey_hash (peppered), an optional
-- nostr_pubkey, and an optional reserved username. No platform-login columns.
-- pubkey_hash and seed_fingerprint are stored HMAC-peppered (the pepper lives in
-- config); UNIQUE constraints are on the peppered values. FK ON DELETE clauses
-- support self-service erasure. Requires PostgreSQL 13+ (gen_random_uuid).
-- ── Enum types ──────────────────────────────────────────────────────────────
CREATE TYPE asset_type AS ENUM ('btc', 'ltc', 'xmr', 'wow', 'grin');
CREATE TYPE slatepack_status AS ENUM (
'pending_recipient', 'pending_sender', 'finalized', 'expired', 'cancelled'
);
CREATE TYPE audit_action AS ENUM (
'user_created', 'user_login', 'wallet_created', 'wallet_registered',
'tx_broadcast', 'session_created', 'session_revoked'
);
-- ── users ───────────────────────────────────────────────────────────────────
CREATE TABLE users (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
username TEXT UNIQUE,
pubkey_hash TEXT UNIQUE, -- peppered at rest
nostr_pubkey TEXT UNIQUE,
wallet_birthday TIMESTAMPTZ,
seed_fingerprint TEXT UNIQUE, -- peppered at rest
xmr_start_height BIGINT,
wow_start_height BIGINT,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
last_seen_at TIMESTAMPTZ
);
-- UNIQUE constraints already index username/pubkey_hash/nostr_pubkey/seed_fingerprint.
-- ── wallets ─────────────────────────────────────────────────────────────────
CREATE TABLE wallets (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
asset asset_type NOT NULL,
address TEXT NOT NULL,
view_key TEXT,
derivation_index INTEGER,
registered_with_node BOOLEAN NOT NULL DEFAULT FALSE,
registration_error TEXT,
label TEXT,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
CREATE INDEX idx_wallets_user_id ON wallets (user_id);
CREATE INDEX idx_wallets_address ON wallets (address);
-- ── sessions ────────────────────────────────────────────────────────────────
CREATE TABLE sessions (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
refresh_token_hash TEXT NOT NULL, -- peppered at rest
platform TEXT NOT NULL, -- extension | web | nostr
device_info TEXT,
ip_address INET,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
expires_at TIMESTAMPTZ NOT NULL,
revoked_at TIMESTAMPTZ,
last_used_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
CREATE INDEX idx_sessions_user_id ON sessions (user_id);
CREATE INDEX idx_sessions_refresh_token_hash ON sessions (refresh_token_hash);
-- ── user_keys ───────────────────────────────────────────────────────────────
CREATE TABLE user_keys (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
asset asset_type NOT NULL,
public_key TEXT NOT NULL,
public_spend_key TEXT,
key_type TEXT NOT NULL DEFAULT 'primary',
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
UNIQUE (user_id, asset, key_type)
);
CREATE INDEX idx_user_keys_user_id ON user_keys (user_id);
-- ── grin_slatepacks ─────────────────────────────────────────────────────────
CREATE TABLE grin_slatepacks (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
slate_id TEXT NOT NULL,
sender_user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
recipient_user_id UUID REFERENCES users(id) ON DELETE SET NULL,
recipient_address TEXT,
slatepack_content TEXT NOT NULL,
amount_nanogrin BIGINT NOT NULL,
status slatepack_status NOT NULL DEFAULT 'pending_recipient',
response_slatepack TEXT,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
expires_at TIMESTAMPTZ NOT NULL,
finalized_at TIMESTAMPTZ,
tx_hash TEXT
);
CREATE INDEX idx_grin_slatepacks_slate_id ON grin_slatepacks (slate_id);
CREATE INDEX idx_grin_slatepacks_sender ON grin_slatepacks (sender_user_id);
CREATE INDEX idx_grin_slatepacks_recipient ON grin_slatepacks (recipient_user_id);
CREATE INDEX idx_grin_slatepacks_status ON grin_slatepacks (status);
-- ── audit_logs ──────────────────────────────────────────────────────────────
CREATE TABLE audit_logs (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
user_id UUID REFERENCES users(id) ON DELETE SET NULL,
action audit_action NOT NULL,
resource_type TEXT,
resource_id UUID,
details JSONB,
ip_address INET,
user_agent TEXT,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
CREATE INDEX idx_audit_logs_user_id ON audit_logs (user_id);
CREATE INDEX idx_audit_logs_action ON audit_logs (action);
CREATE INDEX idx_audit_logs_created_at ON audit_logs (created_at);