-- PostgreSQL schema for Biz Connect — the target of the SQLite→Postgres migration. -- -- Every column is defined up front here (unlike the SQLite db.js, which layers columns via ALTER TABLE -- and hit a real ordering bug). Type mapping from the SQLite schema: -- SQLite INTEGER epoch-ms timestamp -> BIGINT (ms since epoch; JS Number-safe) -- SQLite INTEGER 0/1 boolean flag -> SMALLINT (kept numeric so app code still reads 0/1) -- SQLite INTEGER byte size / duration-> BIGINT (files can exceed INT range) -- SQLite INTEGER small count (peak) -> INTEGER -- SQLite AUTOINCREMENT rowid -> BIGINT GENERATED ALWAYS AS IDENTITY -- TEXT -> TEXT -- Foreign keys mirror the three the SQLite schema enforced (users→teams, sessions_auth→users, -- machines→teams). The data-migration script inserts in dependency order so these hold. CREATE TABLE IF NOT EXISTS teams ( id TEXT PRIMARY KEY, name TEXT NOT NULL, created_at BIGINT NOT NULL ); CREATE TABLE IF NOT EXISTS users ( id TEXT PRIMARY KEY, team_id TEXT NOT NULL REFERENCES teams(id), email TEXT NOT NULL UNIQUE, pw_hash TEXT NOT NULL, pw_salt TEXT NOT NULL, role TEXT NOT NULL DEFAULT 'technician', mfa_secret TEXT, mfa_enabled SMALLINT NOT NULL DEFAULT 0, created_at BIGINT NOT NULL, name TEXT, active SMALLINT NOT NULL DEFAULT 1, avatar_url TEXT, status TEXT NOT NULL DEFAULT 'active', bizgaze_user_id TEXT, last_seen BIGINT ); CREATE INDEX IF NOT EXISTS idx_users_bizgaze_user_id ON users(bizgaze_user_id); CREATE TABLE IF NOT EXISTS sessions_auth ( token TEXT PRIMARY KEY, user_id TEXT NOT NULL REFERENCES users(id), mfa_passed SMALLINT NOT NULL DEFAULT 0, created_at BIGINT NOT NULL, expires_at BIGINT NOT NULL ); CREATE TABLE IF NOT EXISTS machines ( id TEXT PRIMARY KEY, team_id TEXT NOT NULL REFERENCES teams(id), name TEXT NOT NULL, enroll_token TEXT NOT NULL UNIQUE, unattended SMALLINT NOT NULL DEFAULT 0, last_seen BIGINT, created_at BIGINT NOT NULL ); CREATE TABLE IF NOT EXISTS audit_log ( id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY, team_id TEXT NOT NULL, user_id TEXT, user_email TEXT, machine_id TEXT, machine_name TEXT, action TEXT NOT NULL, detail TEXT, at BIGINT NOT NULL ); CREATE TABLE IF NOT EXISTS sessions_log ( id TEXT PRIMARY KEY, team_id TEXT NOT NULL, agent_email TEXT, agent_name TEXT, ticket TEXT, started_at BIGINT NOT NULL, ended_at BIGINT, recording TEXT, transcript TEXT ); CREATE TABLE IF NOT EXISTS refresh_tokens ( token_hash TEXT PRIMARY KEY, user_id TEXT NOT NULL, created_at BIGINT NOT NULL, expires_at BIGINT NOT NULL, revoked SMALLINT NOT NULL DEFAULT 0 ); CREATE TABLE IF NOT EXISTS api_keys ( id TEXT PRIMARY KEY, team_id TEXT NOT NULL, name TEXT, key_hash TEXT NOT NULL UNIQUE, scopes TEXT NOT NULL DEFAULT '', created_by TEXT, created_at BIGINT NOT NULL, last_used_at BIGINT, revoked SMALLINT NOT NULL DEFAULT 0 ); CREATE TABLE IF NOT EXISTS webhooks ( id TEXT PRIMARY KEY, team_id TEXT NOT NULL, url TEXT NOT NULL, secret TEXT NOT NULL, events TEXT NOT NULL DEFAULT '', active SMALLINT NOT NULL DEFAULT 1, created_by TEXT, created_at BIGINT NOT NULL, last_status INTEGER, last_error TEXT, last_at BIGINT ); CREATE TABLE IF NOT EXISTS messages ( id TEXT PRIMARY KEY, team_id TEXT NOT NULL, sender_id TEXT NOT NULL, recipient_id TEXT NOT NULL, body TEXT NOT NULL, created_at BIGINT NOT NULL, read_at BIGINT, reply_to TEXT, attachment_id TEXT, conversation_id TEXT, mentions TEXT, delivered_at BIGINT, poll_id TEXT, msg_type TEXT, deleted SMALLINT NOT NULL DEFAULT 0, edited_at BIGINT, fwd_from TEXT ); CREATE INDEX IF NOT EXISTS idx_messages_pair ON messages(team_id, sender_id, recipient_id, created_at); CREATE INDEX IF NOT EXISTS idx_messages_unread ON messages(team_id, recipient_id, sender_id, read_at); CREATE INDEX IF NOT EXISTS idx_messages_conv ON messages(conversation_id, created_at); -- New: attachment lookups drove the /files auth scan (see static.js authAttachment). Index it so the -- per-Range playback auth is a keyed lookup, not a table scan (the auth cache stays as a second line). CREATE INDEX IF NOT EXISTS idx_messages_attachment ON messages(attachment_id); CREATE TABLE IF NOT EXISTS message_reactions ( message_id TEXT NOT NULL, user_id TEXT NOT NULL, emoji TEXT NOT NULL, created_at BIGINT NOT NULL, PRIMARY KEY (message_id, user_id, emoji) ); CREATE TABLE IF NOT EXISTS attachments ( id TEXT PRIMARY KEY, team_id TEXT NOT NULL, uploader_id TEXT NOT NULL, name TEXT NOT NULL, mime TEXT, size BIGINT, created_at BIGINT NOT NULL ); CREATE TABLE IF NOT EXISTS conversations ( id TEXT PRIMARY KEY, team_id TEXT NOT NULL, type TEXT NOT NULL DEFAULT 'group', name TEXT, created_by TEXT, created_at BIGINT NOT NULL, avatar_id TEXT, admin_only SMALLINT NOT NULL DEFAULT 0 ); CREATE TABLE IF NOT EXISTS conversation_members ( conversation_id TEXT NOT NULL, user_id TEXT NOT NULL, last_read_at BIGINT NOT NULL DEFAULT 0, joined_at BIGINT NOT NULL, admin SMALLINT NOT NULL DEFAULT 0, PRIMARY KEY (conversation_id, user_id) ); CREATE TABLE IF NOT EXISTS call_history ( id TEXT PRIMARY KEY, team_id TEXT NOT NULL, room TEXT NOT NULL, group_id TEXT, kind TEXT, title TEXT, peak INTEGER NOT NULL DEFAULT 0, participants TEXT, uids TEXT, started_at BIGINT NOT NULL, ended_at BIGINT NOT NULL ); CREATE INDEX IF NOT EXISTS idx_call_history_team ON call_history(team_id, ended_at); CREATE TABLE IF NOT EXISTS user_aliases ( old_id TEXT PRIMARY KEY, user_id TEXT NOT NULL, team_id TEXT, created_at BIGINT NOT NULL ); CREATE TABLE IF NOT EXISTS polls ( id TEXT PRIMARY KEY, team_id TEXT NOT NULL, conversation_id TEXT NOT NULL, message_id TEXT, question TEXT NOT NULL, options TEXT NOT NULL, multi SMALLINT NOT NULL DEFAULT 0, closed SMALLINT NOT NULL DEFAULT 0, created_by TEXT NOT NULL, created_at BIGINT NOT NULL ); CREATE TABLE IF NOT EXISTS poll_votes ( poll_id TEXT NOT NULL, user_id TEXT NOT NULL, option_idx INTEGER NOT NULL, created_at BIGINT NOT NULL, PRIMARY KEY (poll_id, user_id, option_idx) ); CREATE TABLE IF NOT EXISTS scheduled_meetings ( id TEXT PRIMARY KEY, team_id TEXT NOT NULL, group_id TEXT, room_code TEXT NOT NULL, title TEXT NOT NULL, description TEXT, scheduled_at BIGINT NOT NULL, created_by TEXT NOT NULL, created_at BIGINT NOT NULL, ended_at BIGINT, participants TEXT, reminded SMALLINT NOT NULL DEFAULT 0, cancelled SMALLINT NOT NULL DEFAULT 0, duration_mins INTEGER, recurrence TEXT, guest_emails TEXT, lobby SMALLINT ); CREATE INDEX IF NOT EXISTS idx_sched_team ON scheduled_meetings(team_id, scheduled_at); CREATE INDEX IF NOT EXISTS idx_sched_code ON scheduled_meetings(room_code); CREATE TABLE IF NOT EXISTS recordings ( id TEXT PRIMARY KEY, team_id TEXT NOT NULL, room TEXT, group_id TEXT, meeting_id TEXT, title TEXT, kind TEXT NOT NULL, file TEXT, mime TEXT, size BIGINT, duration_ms BIGINT, created_by TEXT, created_by_name TEXT, created_at BIGINT NOT NULL ); CREATE INDEX IF NOT EXISTS idx_rec_team ON recordings(team_id, created_at); CREATE INDEX IF NOT EXISTS idx_rec_room ON recordings(room); CREATE TABLE IF NOT EXISTS push_subscriptions ( id TEXT PRIMARY KEY, user_id TEXT NOT NULL, endpoint TEXT NOT NULL UNIQUE, p256dh TEXT NOT NULL, auth TEXT NOT NULL, created_at BIGINT NOT NULL ); CREATE INDEX IF NOT EXISTS idx_push_user ON push_subscriptions(user_id); CREATE TABLE IF NOT EXISTS device_tokens ( id TEXT PRIMARY KEY, user_id TEXT NOT NULL, tenant_id TEXT, platform TEXT NOT NULL, token TEXT NOT NULL UNIQUE, created_at BIGINT NOT NULL, last_seen BIGINT ); CREATE INDEX IF NOT EXISTS idx_devtok_user ON device_tokens(user_id); CREATE TABLE IF NOT EXISTS app_installs ( id TEXT PRIMARY KEY, install_id TEXT NOT NULL UNIQUE, user_id TEXT, user_email TEXT, tenant_id TEXT, platform TEXT, app_version TEXT, os TEXT, first_seen BIGINT NOT NULL, last_seen BIGINT NOT NULL ); CREATE INDEX IF NOT EXISTS idx_installs_tenant ON app_installs(tenant_id); CREATE INDEX IF NOT EXISTS idx_installs_user ON app_installs(user_id); CREATE TABLE IF NOT EXISTS favorites ( user_id TEXT NOT NULL, target TEXT NOT NULL, created_at BIGINT NOT NULL, PRIMARY KEY (user_id, target) );