CREATE EXTENSION IF NOT EXISTS pgcrypto; CREATE EXTENSION IF NOT EXISTS citext; CREATE TABLE IF NOT EXISTS users ( id uuid PRIMARY KEY DEFAULT gen_random_uuid(), email citext NOT NULL UNIQUE, password_hash text NOT NULL, display_name text, created_at timestamptz NOT NULL DEFAULT now(), updated_at timestamptz NOT NULL DEFAULT now(), disabled_at timestamptz, CONSTRAINT users_email_not_blank CHECK (length(trim(email::text)) > 0), CONSTRAINT users_password_hash_not_blank CHECK (length(trim(password_hash)) > 0) ); CREATE TABLE IF NOT EXISTS auth_sessions ( id uuid PRIMARY KEY DEFAULT gen_random_uuid(), user_id uuid NOT NULL REFERENCES users(id) ON DELETE CASCADE, token_hash text NOT NULL UNIQUE, issued_at timestamptz NOT NULL DEFAULT now(), expires_at timestamptz NOT NULL, revoked_at timestamptz, user_agent text, device_label text, CONSTRAINT auth_sessions_token_hash_not_blank CHECK (length(trim(token_hash)) > 0), CONSTRAINT auth_sessions_expires_after_issued CHECK (expires_at > issued_at) ); CREATE TABLE IF NOT EXISTS synced_resources ( server_id uuid PRIMARY KEY DEFAULT gen_random_uuid(), owner_user_id uuid NOT NULL REFERENCES users(id) ON DELETE CASCADE, resource_type text NOT NULL, client_id text NOT NULL, payload_json jsonb NOT NULL, schema_version integer NOT NULL, client_updated_at timestamptz NOT NULL, server_updated_at timestamptz NOT NULL DEFAULT now(), deleted_at timestamptz, origin_device_id text, CONSTRAINT synced_resources_type_check CHECK ( resource_type IN ( 'exercise', 'program', 'workoutTemplate', 'workoutHistory', 'mediaAsset' ) ), CONSTRAINT synced_resources_client_id_not_blank CHECK (length(trim(client_id)) > 0), CONSTRAINT synced_resources_schema_version_positive CHECK (schema_version > 0), CONSTRAINT synced_resources_owner_type_client_unique UNIQUE ( owner_user_id, resource_type, client_id ) ); CREATE INDEX IF NOT EXISTS idx_synced_resources_owner_type_updated ON synced_resources (owner_user_id, resource_type, server_updated_at); CREATE INDEX IF NOT EXISTS idx_synced_resources_owner_updated ON synced_resources (owner_user_id, server_updated_at); CREATE OR REPLACE FUNCTION set_synced_resources_server_updated_at() RETURNS trigger AS $$ BEGIN NEW.server_updated_at = now(); RETURN NEW; END; $$ LANGUAGE plpgsql; DROP TRIGGER IF EXISTS trg_synced_resources_set_server_updated_at ON synced_resources; CREATE TRIGGER trg_synced_resources_set_server_updated_at BEFORE UPDATE ON synced_resources FOR EACH ROW EXECUTE FUNCTION set_synced_resources_server_updated_at(); CREATE TABLE IF NOT EXISTS shares ( id uuid PRIMARY KEY DEFAULT gen_random_uuid(), sender_user_id uuid NOT NULL REFERENCES users(id) ON DELETE CASCADE, resource_type text NOT NULL, payload_json jsonb NOT NULL, created_at timestamptz NOT NULL DEFAULT now(), revoked_at timestamptz, CONSTRAINT shares_resource_type_check CHECK ( resource_type IN ('program', 'workoutTemplate') ) ); CREATE TABLE IF NOT EXISTS share_recipients ( id uuid PRIMARY KEY DEFAULT gen_random_uuid(), share_id uuid NOT NULL REFERENCES shares(id) ON DELETE CASCADE, recipient_user_id uuid NOT NULL REFERENCES users(id) ON DELETE CASCADE, status text NOT NULL DEFAULT 'pending', responded_at timestamptz, CONSTRAINT share_recipients_status_check CHECK ( status IN ('pending', 'accepted', 'declined', 'revoked') ), CONSTRAINT share_recipients_share_user_unique UNIQUE ( share_id, recipient_user_id ) ); CREATE INDEX IF NOT EXISTS idx_auth_sessions_user_id ON auth_sessions (user_id); CREATE INDEX IF NOT EXISTS idx_shares_sender_user_id ON shares (sender_user_id); CREATE INDEX IF NOT EXISTS idx_share_recipients_recipient_status ON share_recipients (recipient_user_id, status);