CREATE TABLE dumps ( id TEXT PRIMARY KEY, kind TEXT NOT NULL, title TEXT NOT NULL, comment TEXT, user_id TEXT NOT NULL, created_at TEXT NOT NULL, updated_at TEXT, url TEXT, -- Lossy lookup key for "has this been dumped already?" — see api/lib/canonical-url.ts url_canonical TEXT, slug TEXT, rich_content TEXT, file_name TEXT, file_mime TEXT, file_size INTEGER, vote_count INTEGER NOT NULL DEFAULT 0, is_private INTEGER NOT NULL DEFAULT 0, custom_thumbnail_mime TEXT, FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE ); CREATE TABLE users ( id TEXT PRIMARY KEY, username TEXT NOT NULL UNIQUE, password_hash TEXT NOT NULL, role TEXT NOT NULL DEFAULT 'user', created_at TEXT NOT NULL, updated_at TEXT, avatar_mime TEXT, description TEXT, invited_by TEXT REFERENCES users(id), email TEXT NOT NULL, chat_read_at TEXT, tokens_valid_after TEXT ); CREATE TABLE votes ( dump_id TEXT NOT NULL, user_id TEXT NOT NULL, created_at TEXT NOT NULL, PRIMARY KEY (dump_id, user_id), FOREIGN KEY (dump_id) REFERENCES dumps(id) ON DELETE CASCADE, FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE ); CREATE TABLE playlists ( id TEXT PRIMARY KEY, user_id TEXT NOT NULL, title TEXT NOT NULL, slug TEXT, description TEXT, is_public INTEGER NOT NULL DEFAULT 1, created_at TEXT NOT NULL, updated_at TEXT, image_mime TEXT, FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE ); CREATE TABLE playlist_dumps ( playlist_id TEXT NOT NULL, dump_id TEXT NOT NULL, position INTEGER NOT NULL, added_at TEXT NOT NULL, PRIMARY KEY (playlist_id, dump_id), FOREIGN KEY (playlist_id) REFERENCES playlists(id) ON DELETE CASCADE, FOREIGN KEY (dump_id) REFERENCES dumps(id) ON DELETE CASCADE ); CREATE TABLE categories ( id TEXT PRIMARY KEY, slug TEXT NOT NULL UNIQUE, name TEXT NOT NULL UNIQUE, position INTEGER NOT NULL DEFAULT 0, created_at TEXT NOT NULL, updated_at TEXT ); CREATE TABLE dump_categories ( dump_id TEXT NOT NULL, category_id TEXT NOT NULL, PRIMARY KEY (dump_id, category_id), FOREIGN KEY (dump_id) REFERENCES dumps(id) ON DELETE CASCADE, FOREIGN KEY (category_id) REFERENCES categories(id) ON DELETE CASCADE ); CREATE TABLE comments ( id TEXT PRIMARY KEY, dump_id TEXT NOT NULL, user_id TEXT NOT NULL, parent_id TEXT, body TEXT NOT NULL, created_at TEXT NOT NULL, updated_at TEXT, deleted INTEGER NOT NULL DEFAULT 0, like_count INTEGER NOT NULL DEFAULT 0, FOREIGN KEY (dump_id) REFERENCES dumps(id) ON DELETE CASCADE, FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE, FOREIGN KEY (parent_id) REFERENCES comments(id) ON DELETE CASCADE ); CREATE TABLE comment_likes ( comment_id TEXT NOT NULL, user_id TEXT NOT NULL, created_at TEXT NOT NULL, PRIMARY KEY (comment_id, user_id), FOREIGN KEY (comment_id) REFERENCES comments(id) ON DELETE CASCADE, FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE ); CREATE TABLE dump_backlinks ( source_type TEXT NOT NULL, source_id TEXT NOT NULL, from_dump_id TEXT NOT NULL, to_dump_id TEXT NOT NULL, created_at TEXT NOT NULL, PRIMARY KEY (source_type, source_id, to_dump_id), FOREIGN KEY (from_dump_id) REFERENCES dumps(id) ON DELETE CASCADE, FOREIGN KEY (to_dump_id) REFERENCES dumps(id) ON DELETE CASCADE ); CREATE INDEX idx_dumps_created ON dumps(created_at); CREATE INDEX idx_dumps_user_created ON dumps(user_id, created_at); CREATE UNIQUE INDEX idx_dumps_user_slug ON dumps(user_id, slug); CREATE INDEX idx_dumps_url ON dumps(url); CREATE INDEX idx_dumps_url_canonical ON dumps(url_canonical); CREATE INDEX idx_votes_user_created ON votes(user_id, created_at); CREATE INDEX idx_playlists_user ON playlists(user_id); CREATE UNIQUE INDEX idx_playlists_user_slug ON playlists(user_id, slug); CREATE INDEX idx_playlist_dumps_order ON playlist_dumps(playlist_id, position); CREATE INDEX idx_playlist_dumps_dump ON playlist_dumps(dump_id); CREATE INDEX idx_comments_dump ON comments(dump_id, created_at); CREATE INDEX idx_comments_parent ON comments(parent_id); CREATE INDEX idx_comments_user ON comments(user_id); CREATE INDEX idx_comment_likes_user ON comment_likes(user_id); CREATE INDEX idx_dump_backlinks_to ON dump_backlinks(to_dump_id); CREATE INDEX idx_dump_backlinks_from ON dump_backlinks(from_dump_id); CREATE INDEX idx_dump_categories_category ON dump_categories(category_id); CREATE TABLE follows ( id TEXT PRIMARY KEY, follower_id TEXT NOT NULL, followed_user_id TEXT, followed_playlist_id TEXT, created_at TEXT NOT NULL, FOREIGN KEY (follower_id) REFERENCES users(id) ON DELETE CASCADE, FOREIGN KEY (followed_user_id) REFERENCES users(id) ON DELETE CASCADE, FOREIGN KEY (followed_playlist_id) REFERENCES playlists(id) ON DELETE CASCADE, CHECK ( (followed_user_id IS NOT NULL AND followed_playlist_id IS NULL) OR (followed_user_id IS NULL AND followed_playlist_id IS NOT NULL) ) ); CREATE UNIQUE INDEX idx_follows_user ON follows(follower_id, followed_user_id) WHERE followed_user_id IS NOT NULL; CREATE UNIQUE INDEX idx_follows_playlist ON follows(follower_id, followed_playlist_id) WHERE followed_playlist_id IS NOT NULL; CREATE INDEX idx_follows_follower ON follows(follower_id); CREATE INDEX idx_follows_followed_user ON follows(followed_user_id); CREATE INDEX idx_follows_followed_playlist ON follows(followed_playlist_id); CREATE TABLE invites ( token TEXT PRIMARY KEY, inviter_id TEXT NOT NULL, used_at TEXT, created_at TEXT NOT NULL, FOREIGN KEY (inviter_id) REFERENCES users(id) ON DELETE CASCADE ); CREATE INDEX idx_invites_inviter ON invites(inviter_id); CREATE INDEX idx_users_invited_by ON users(invited_by); CREATE UNIQUE INDEX idx_users_username_lower ON users(lower(username)); -- An account from another instance has no password (''), and its email may -- also be a local account's. CREATE UNIQUE INDEX idx_users_email_lower ON users(lower(email)) WHERE password_hash != ''; CREATE TABLE attachments ( id TEXT PRIMARY KEY, resource_id TEXT, mime TEXT NOT NULL, created_at TEXT NOT NULL, resource_type TEXT ); CREATE INDEX idx_attachments_resource ON attachments(resource_id); CREATE TABLE password_reset_tokens ( token TEXT PRIMARY KEY, user_id TEXT NOT NULL, expires_at TEXT NOT NULL, used_at TEXT, FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE ); CREATE INDEX idx_password_reset_tokens_user ON password_reset_tokens(user_id); CREATE TABLE notifications ( id TEXT PRIMARY KEY, user_id TEXT NOT NULL, type TEXT NOT NULL, data TEXT NOT NULL, read INTEGER NOT NULL DEFAULT 0, created_at TEXT NOT NULL, source_key TEXT, FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE ); CREATE INDEX idx_notifications_user ON notifications(user_id, created_at); CREATE UNIQUE INDEX idx_notifications_dedup ON notifications(user_id, source_key) WHERE source_key IS NOT NULL; CREATE TABLE user_settings ( user_id TEXT PRIMARY KEY, style TEXT, color_scheme TEXT, digest_weekly INTEGER NOT NULL DEFAULT 1, digest_monthly INTEGER NOT NULL DEFAULT 1, updated_at TEXT NOT NULL, FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE ); CREATE TABLE digest_schedule ( user_id TEXT NOT NULL, cadence TEXT NOT NULL, window_start TEXT NOT NULL, PRIMARY KEY (user_id, cadence), FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE ); CREATE TABLE dump_slug_aliases ( user_id TEXT NOT NULL, slug TEXT NOT NULL, dump_id TEXT NOT NULL REFERENCES dumps(id) ON DELETE CASCADE, created_at TEXT NOT NULL, PRIMARY KEY (user_id, slug) ); CREATE INDEX idx_dump_slug_aliases_dump ON dump_slug_aliases(dump_id, created_at); CREATE TABLE playlist_slug_aliases ( user_id TEXT NOT NULL, slug TEXT NOT NULL, playlist_id TEXT NOT NULL REFERENCES playlists(id) ON DELETE CASCADE, created_at TEXT NOT NULL, PRIMARY KEY (user_id, slug) ); CREATE INDEX idx_playlist_slug_aliases_playlist ON playlist_slug_aliases(playlist_id, created_at); CREATE TABLE user_aliases ( username TEXT NOT NULL, user_id TEXT NOT NULL REFERENCES users(id) ON DELETE CASCADE, created_at TEXT NOT NULL, PRIMARY KEY (username) ); CREATE UNIQUE INDEX idx_user_aliases_lower ON user_aliases(lower(username)); CREATE INDEX idx_user_aliases_user ON user_aliases(user_id, created_at); CREATE TABLE chat_messages ( id TEXT NOT NULL PRIMARY KEY, user_id TEXT NOT NULL, body TEXT NOT NULL, created_at TEXT NOT NULL, updated_at TEXT, reply_to_id TEXT, mentions_everyone INTEGER NOT NULL DEFAULT 0, FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE ); CREATE INDEX idx_chat_messages_created_id ON chat_messages(created_at, id); CREATE INDEX idx_chat_messages_user ON chat_messages(user_id); CREATE TABLE federation_keys ( kid TEXT PRIMARY KEY, private_jwk TEXT NOT NULL, public_jwk TEXT NOT NULL, created_at TEXT NOT NULL ); CREATE TABLE user_identities ( user_id TEXT PRIMARY KEY REFERENCES users(id) ON DELETE CASCADE, issuer TEXT NOT NULL, subject TEXT NOT NULL, picture TEXT, synced_at TEXT NOT NULL ); CREATE UNIQUE INDEX idx_user_identities_subject ON user_identities(issuer, subject); CREATE TABLE federation_peers ( issuer TEXT PRIMARY KEY, status TEXT NOT NULL, name TEXT, created_at TEXT NOT NULL ); CREATE TABLE federation_logins ( state TEXT PRIMARY KEY, issuer TEXT NOT NULL, code_verifier TEXT NOT NULL, nonce TEXT NOT NULL, invite_token TEXT, created_at TEXT NOT NULL ); CREATE TABLE oauth_codes ( code_hash TEXT PRIMARY KEY, user_id TEXT NOT NULL REFERENCES users(id) ON DELETE CASCADE, client_id TEXT NOT NULL, redirect_uri TEXT NOT NULL, code_challenge TEXT NOT NULL, nonce TEXT NOT NULL, expires_at TEXT NOT NULL ); CREATE TABLE federation_grants ( user_id TEXT NOT NULL REFERENCES users(id) ON DELETE CASCADE, client_id TEXT NOT NULL, client_name TEXT, granted_at TEXT NOT NULL, last_used_at TEXT NOT NULL, PRIMARY KEY (user_id, client_id) ); CREATE TABLE federation_outbox ( id TEXT PRIMARY KEY, client_id TEXT NOT NULL, event TEXT NOT NULL, attempts INTEGER NOT NULL DEFAULT 0, next_attempt_at TEXT NOT NULL, created_at TEXT NOT NULL ); CREATE INDEX idx_federation_outbox_next ON federation_outbox(next_attempt_at);