feat(db): Postgres target schema (Phase 1 of the migration)

server/db/schema.pg.sql — the full Postgres DDL, every column defined up front (no
ALTER-ordering fragility). SQLite→PG type mapping documented in-file (epoch-ms
INTEGER→BIGINT, 0/1 flags→SMALLINT kept numeric so app code is unchanged, sizes→
BIGINT, audit rowid→GENERATED IDENTITY). Mirrors the three existing FKs and adds a
new idx_messages_attachment (the /files auth scan we cached earlier becomes a keyed
lookup).

Validated against a throwaway Postgres 16: loads with no errors, 24 tables + 44
indexes created. Unwired — nothing uses it yet; the SQLite path is untouched.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-07-24 19:46:04 +05:30
parent b2c4b10e41
commit 1709a331d1
+304
View File
@@ -0,0 +1,304 @@
-- 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)
);