···11+CREATE TYPE notification_channel AS ENUM ('email', 'discord', 'telegram', 'signal');
22+CREATE TYPE notification_status AS ENUM ('pending', 'processing', 'sent', 'failed');
33+CREATE TYPE notification_type AS ENUM (
44+ 'welcome',
55+ 'email_verification',
66+ 'password_reset',
77+ 'email_update',
88+ 'account_deletion',
99+ 'admin_email'
1010+);
1111+1212+CREATE TABLE IF NOT EXISTS users (
1313+ id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
1414+ handle TEXT NOT NULL UNIQUE,
1515+ email TEXT NOT NULL UNIQUE,
1616+ did TEXT NOT NULL UNIQUE,
1717+ password_hash TEXT NOT NULL,
1818+ created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
1919+ updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
2020+2121+ -- status & moderation
2222+ deactivated_at TIMESTAMPTZ,
2323+ invites_disabled BOOLEAN DEFAULT FALSE,
2424+ takedown_ref TEXT,
2525+2626+ -- notifs
2727+ preferred_notification_channel notification_channel NOT NULL DEFAULT 'email',
2828+2929+ -- auth & verification
3030+ password_reset_code TEXT,
3131+ password_reset_code_expires_at TIMESTAMPTZ,
3232+3333+ email_pending_verification TEXT,
3434+ email_confirmation_code TEXT,
3535+ email_confirmation_code_expires_at TIMESTAMPTZ
3636+);
3737+3838+CREATE INDEX IF NOT EXISTS idx_users_password_reset_code ON users(password_reset_code) WHERE password_reset_code IS NOT NULL;
3939+CREATE INDEX IF NOT EXISTS idx_users_email_confirmation_code ON users(email_confirmation_code) WHERE email_confirmation_code IS NOT NULL;
4040+4141+CREATE TABLE IF NOT EXISTS invite_codes (
4242+ code TEXT PRIMARY KEY,
4343+ available_uses INT NOT NULL DEFAULT 1,
4444+ created_by_user UUID NOT NULL REFERENCES users(id),
4545+ created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
4646+ disabled BOOLEAN DEFAULT FALSE
4747+);
4848+4949+CREATE TABLE IF NOT EXISTS invite_code_uses (
5050+ id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
5151+ code TEXT NOT NULL REFERENCES invite_codes(code),
5252+ used_by_user UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
5353+ used_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
5454+ UNIQUE(code, used_by_user)
5555+);
5656+5757+-- TODO: encrypt at rest!
5858+CREATE TABLE IF NOT EXISTS user_keys (
5959+ user_id UUID PRIMARY KEY REFERENCES users(id) ON DELETE CASCADE,
6060+ key_bytes BYTEA NOT NULL,
6161+ created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
6262+);
6363+6464+CREATE TABLE IF NOT EXISTS repos (
6565+ user_id UUID PRIMARY KEY REFERENCES users(id) ON DELETE CASCADE,
6666+ repo_root_cid TEXT NOT NULL,
6767+ created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
6868+ updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
6969+);
7070+7171+-- content addressable storage
7272+CREATE TABLE IF NOT EXISTS blocks (
7373+ cid BYTEA PRIMARY KEY,
7474+ data BYTEA NOT NULL,
7575+ created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
7676+);
7777+7878+-- denormalized index for fast queries
7979+CREATE TABLE IF NOT EXISTS records (
8080+ id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
8181+ repo_id UUID NOT NULL REFERENCES repos(user_id) ON DELETE CASCADE,
8282+ collection TEXT NOT NULL,
8383+ rkey TEXT NOT NULL,
8484+ record_cid TEXT NOT NULL,
8585+ takedown_ref TEXT,
8686+ created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
8787+ UNIQUE(repo_id, collection, rkey)
8888+);
8989+9090+CREATE TABLE IF NOT EXISTS blobs (
9191+ cid TEXT PRIMARY KEY,
9292+ mime_type TEXT NOT NULL,
9393+ size_bytes BIGINT NOT NULL,
9494+ created_by_user UUID NOT NULL REFERENCES users(id),
9595+ storage_key TEXT NOT NULL,
9696+ takedown_ref TEXT,
9797+ created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
9898+);
9999+100100+CREATE TABLE IF NOT EXISTS sessions (
101101+ access_jwt TEXT PRIMARY KEY,
102102+ refresh_jwt TEXT NOT NULL UNIQUE,
103103+ did TEXT NOT NULL REFERENCES users(did) ON DELETE CASCADE,
104104+ created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
105105+);
106106+107107+CREATE TABLE IF NOT EXISTS app_passwords (
108108+ id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
109109+ user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
110110+ name TEXT NOT NULL,
111111+ password_hash TEXT NOT NULL,
112112+ created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
113113+ privileged BOOLEAN NOT NULL DEFAULT FALSE,
114114+ UNIQUE(user_id, name)
115115+);
116116+117117+-- naughty list
118118+CREATE TABLE reports (
119119+ id BIGINT PRIMARY KEY,
120120+ reason_type TEXT NOT NULL,
121121+ reason TEXT,
122122+ subject_json JSONB NOT NULL,
123123+ reported_by_did TEXT NOT NULL,
124124+ created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
125125+);
126126+127127+CREATE TABLE IF NOT EXISTS account_deletion_requests (
128128+ token TEXT PRIMARY KEY,
129129+ did TEXT NOT NULL REFERENCES users(did) ON DELETE CASCADE,
130130+ expires_at TIMESTAMPTZ NOT NULL,
131131+ created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
132132+);
133133+134134+CREATE TABLE IF NOT EXISTS notification_queue (
135135+ id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
136136+ user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
137137+ channel notification_channel NOT NULL DEFAULT 'email',
138138+ notification_type notification_type NOT NULL,
139139+ status notification_status NOT NULL DEFAULT 'pending',
140140+ recipient TEXT NOT NULL,
141141+ subject TEXT,
142142+ body TEXT NOT NULL,
143143+ metadata JSONB,
144144+ attempts INT NOT NULL DEFAULT 0,
145145+ max_attempts INT NOT NULL DEFAULT 3,
146146+ last_error TEXT,
147147+ created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
148148+ updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
149149+ scheduled_for TIMESTAMPTZ NOT NULL DEFAULT NOW(),
150150+ processed_at TIMESTAMPTZ
151151+);
152152+153153+CREATE INDEX idx_notification_queue_status_scheduled
154154+ ON notification_queue(status, scheduled_for)
155155+ WHERE status = 'pending';
156156+157157+CREATE INDEX idx_notification_queue_user_id ON notification_queue(user_id);
-80
migrations/202512211400_initial_tables.sql
···11--- A very basic schema to get started.
22--- TODO: PRODUCTIONIZE BABY
33-44-CREATE TABLE IF NOT EXISTS users (
55- id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
66- handle TEXT NOT NULL UNIQUE,
77- email TEXT NOT NULL UNIQUE,
88- did TEXT NOT NULL UNIQUE,
99- password_hash TEXT NOT NULL,
1010- created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
1111- updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
1212-);
1313-1414-CREATE TABLE IF NOT EXISTS invite_codes (
1515- code TEXT PRIMARY KEY,
1616- available_uses INT NOT NULL DEFAULT 1,
1717- created_by_user UUID NOT NULL REFERENCES users(id),
1818- created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
1919-);
2020-2121-CREATE TABLE IF NOT EXISTS invite_code_uses (
2222- id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
2323- code TEXT NOT NULL REFERENCES invite_codes(code),
2424- used_by_user UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
2525- used_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
2626- UNIQUE(code, used_by_user)
2727-);
2828-2929--- OIII THIS TABLE CONTAINS PLAINTEXT PRIVATE KEYS, TODO: encrypt at rest!
3030-CREATE TABLE IF NOT EXISTS user_keys (
3131- user_id UUID PRIMARY KEY REFERENCES users(id) ON DELETE CASCADE,
3232- -- Storing as raw bytes
3333- -- secp256k1 is 32 bytes
3434- key_bytes BYTEA NOT NULL,
3535- created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
3636-);
3737-3838-CREATE TABLE IF NOT EXISTS repos (
3939- user_id UUID PRIMARY KEY REFERENCES users(id) ON DELETE CASCADE,
4040- repo_root_cid TEXT NOT NULL,
4141- created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
4242- updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
4343-);
4444-4545-CREATE TABLE IF NOT EXISTS blocks (
4646- cid BYTEA PRIMARY KEY,
4747- data BYTEA NOT NULL,
4848- created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
4949-);
5050-5151--- A denormalized table to quickly query for records
5252--- TODO: Do I actually need this?
5353-CREATE TABLE IF NOT EXISTS records (
5454- id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
5555- repo_id UUID NOT NULL REFERENCES repos(user_id) ON DELETE CASCADE,
5656- collection TEXT NOT NULL,
5757- rkey TEXT NOT NULL,
5858- record_cid TEXT NOT NULL,
5959- created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
6060- UNIQUE(repo_id, collection, rkey)
6161-);
6262-6363-CREATE TABLE IF NOT EXISTS blobs (
6464- cid TEXT PRIMARY KEY,
6565- mime_type TEXT NOT NULL,
6666- size_bytes BIGINT NOT NULL,
6767- created_by_user UUID NOT NULL REFERENCES users(id),
6868- created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
6969-7070- -- The key/path in the S3 bucket
7171- storage_key TEXT NOT NULL
7272-);
7373-7474-CREATE TABLE IF NOT EXISTS sessions (
7575- access_jwt TEXT PRIMARY KEY,
7676- refresh_jwt TEXT NOT NULL UNIQUE,
7777- did TEXT NOT NULL REFERENCES users(did) ON DELETE CASCADE,
7878- created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
7979-);
8080-
-9
migrations/202512211500_app_passwords.sql
···11-CREATE TABLE IF NOT EXISTS app_passwords (
22- id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
33- user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
44- name TEXT NOT NULL,
55- password_hash TEXT NOT NULL,
66- created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
77- privileged BOOLEAN NOT NULL DEFAULT FALSE,
88- UNIQUE(user_id, name)
99-);
-11
migrations/202512211600_moderation_and_status.sql
···11-ALTER TABLE users ADD COLUMN deactivated_at TIMESTAMPTZ;
22-33--- * reports u *
44-CREATE TABLE reports (
55- id BIGINT PRIMARY KEY,
66- reason_type TEXT NOT NULL,
77- reason TEXT,
88- subject_json JSONB NOT NULL,
99- reported_by_did TEXT NOT NULL,
1010- created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
1111-);
···11-CREATE TABLE IF NOT EXISTS account_deletion_requests (
22- token TEXT PRIMARY KEY,
33- did TEXT NOT NULL REFERENCES users(did) ON DELETE CASCADE,
44- expires_at TIMESTAMPTZ NOT NULL,
55- created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
66-);
-36
migrations/202512212000_notification_queue.sql
···11-CREATE TYPE notification_channel AS ENUM ('email', 'discord', 'telegram', 'signal');
22-CREATE TYPE notification_status AS ENUM ('pending', 'processing', 'sent', 'failed');
33-CREATE TYPE notification_type AS ENUM (
44- 'welcome',
55- 'email_verification',
66- 'password_reset',
77- 'email_update',
88- 'account_deletion'
99-);
1010-1111-CREATE TABLE IF NOT EXISTS notification_queue (
1212- id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
1313- user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
1414- channel notification_channel NOT NULL DEFAULT 'email',
1515- notification_type notification_type NOT NULL,
1616- status notification_status NOT NULL DEFAULT 'pending',
1717- recipient TEXT NOT NULL,
1818- subject TEXT,
1919- body TEXT NOT NULL,
2020- metadata JSONB,
2121- attempts INT NOT NULL DEFAULT 0,
2222- max_attempts INT NOT NULL DEFAULT 3,
2323- last_error TEXT,
2424- created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
2525- updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
2626- scheduled_for TIMESTAMPTZ NOT NULL DEFAULT NOW(),
2727- processed_at TIMESTAMPTZ
2828-);
2929-3030-CREATE INDEX idx_notification_queue_status_scheduled
3131- ON notification_queue(status, scheduled_for)
3232- WHERE status = 'pending';
3333-3434-CREATE INDEX idx_notification_queue_user_id ON notification_queue(user_id);
3535-3636-ALTER TABLE users ADD COLUMN IF NOT EXISTS preferred_notification_channel notification_channel NOT NULL DEFAULT 'email';
-4
migrations/202512212100_password_reset.sql
···11-ALTER TABLE users ADD COLUMN IF NOT EXISTS password_reset_code TEXT;
22-ALTER TABLE users ADD COLUMN IF NOT EXISTS password_reset_code_expires_at TIMESTAMPTZ;
33-44-CREATE INDEX IF NOT EXISTS idx_users_password_reset_code ON users(password_reset_code) WHERE password_reset_code IS NOT NULL;
-1
migrations/202512212200_admin_email_type.sql
···11-ALTER TYPE notification_type ADD VALUE IF NOT EXISTS 'admin_email';