-- BlindMaster schema. Idempotent: CREATE … IF NOT EXISTS so it works on a -- fresh DB and replays cleanly against an existing one. -- ── Core auth ─────────────────────────────────────────────────────────────── CREATE TABLE IF NOT EXISTS users ( id SERIAL PRIMARY KEY, name TEXT, email TEXT NOT NULL UNIQUE, password_hash_string TEXT NOT NULL, verification_token TEXT, is_verified BOOLEAN NOT NULL DEFAULT FALSE, timezone TEXT DEFAULT 'America/Chicago', apns_token TEXT, fcm_token TEXT, -- legacy (FCM era), kept harmless created_at TIMESTAMPTZ DEFAULT NOW() ); -- One active session token per user. Code does delete-then-insert on login, -- so PK on user_id enforces it. CREATE TABLE IF NOT EXISTS user_tokens ( user_id INTEGER PRIMARY KEY REFERENCES users(id) ON DELETE CASCADE, token TEXT NOT NULL UNIQUE, connected BOOLEAN NOT NULL DEFAULT FALSE, socket TEXT ); CREATE TABLE IF NOT EXISTS password_reset_tokens ( email TEXT PRIMARY KEY, token TEXT NOT NULL, expires_at TIMESTAMPTZ NOT NULL, created_at TIMESTAMPTZ DEFAULT NOW() ); CREATE TABLE IF NOT EXISTS user_pending_emails ( user_id INTEGER PRIMARY KEY REFERENCES users(id) ON DELETE CASCADE, pending_email TEXT NOT NULL, token TEXT NOT NULL, expires_at TIMESTAMPTZ NOT NULL ); -- ── Hardware: hubs (devices) and their controlled blinds (peripherals) ────── CREATE TABLE IF NOT EXISTS devices ( id SERIAL PRIMARY KEY, user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE, device_name TEXT NOT NULL, max_ports INTEGER NOT NULL DEFAULT 4, battery_soc SMALLINT, timezone TEXT, UNIQUE (user_id, device_name) ); -- token UNIQUE so socket-auth can `select device_id … where token=$1` quickly. CREATE TABLE IF NOT EXISTS device_tokens ( device_id INTEGER NOT NULL REFERENCES devices(id) ON DELETE CASCADE, token TEXT NOT NULL UNIQUE, connected BOOLEAN NOT NULL DEFAULT FALSE, socket TEXT ); CREATE INDEX IF NOT EXISTS device_tokens_device_id_idx ON device_tokens(device_id); CREATE TABLE IF NOT EXISTS peripherals ( id SERIAL PRIMARY KEY, device_id INTEGER NOT NULL REFERENCES devices(id) ON DELETE CASCADE, user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE, peripheral_number INTEGER NOT NULL, peripheral_name TEXT NOT NULL, last_pos INTEGER, last_set TIMESTAMPTZ, calibrated BOOLEAN DEFAULT FALSE, await_calib BOOLEAN DEFAULT FALSE, UNIQUE (device_id, peripheral_number) ); CREATE INDEX IF NOT EXISTS peripherals_user_id_idx ON peripherals(user_id); -- ── Multi-blind groups (e.g. "all kitchen blinds") ────────────────────────── CREATE TABLE IF NOT EXISTS groups ( id SERIAL PRIMARY KEY, user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE, name TEXT NOT NULL, timezone TEXT, created_at TIMESTAMPTZ DEFAULT NOW(), UNIQUE (user_id, name) ); CREATE TABLE IF NOT EXISTS group_peripherals ( group_id INTEGER NOT NULL REFERENCES groups(id) ON DELETE CASCADE, peripheral_id INTEGER NOT NULL REFERENCES peripherals(id) ON DELETE CASCADE, PRIMARY KEY (group_id, peripheral_id) ); -- ── Idempotent column additions for older DBs ─────────────────────────────── -- These mirror the runtime-bootstrap ALTERs the index.js used to do on every -- startup. Safe on fresh DBs because the columns are already in CREATE TABLE. ALTER TABLE users ADD COLUMN IF NOT EXISTS apns_token TEXT; ALTER TABLE users ADD COLUMN IF NOT EXISTS timezone TEXT DEFAULT 'America/Chicago'; ALTER TABLE devices ADD COLUMN IF NOT EXISTS battery_soc SMALLINT; ALTER TABLE devices ADD COLUMN IF NOT EXISTS timezone TEXT; ALTER TABLE groups ADD COLUMN IF NOT EXISTS timezone TEXT;