CREATE EXTENSION IF NOT EXISTS pgcrypto; CREATE OR REPLACE FUNCTION set_updated_at() RETURNS TRIGGER AS $$ BEGIN NEW.updated_at = now(); RETURN NEW; END; $$ LANGUAGE plpgsql; CREATE TABLE IF NOT EXISTS users ( user_id uuid PRIMARY KEY DEFAULT gen_random_uuid(), username text NOT NULL UNIQUE, password_hash text NOT NULL, display_name text NOT NULL, created_at timestamptz NOT NULL DEFAULT now(), updated_at timestamptz NOT NULL DEFAULT now() ); DROP TRIGGER IF EXISTS users_set_updated_at ON users; CREATE TRIGGER users_set_updated_at BEFORE UPDATE ON users FOR EACH ROW EXECUTE FUNCTION set_updated_at(); CREATE TABLE IF NOT EXISTS user_totp ( user_id uuid PRIMARY KEY REFERENCES users(user_id) ON DELETE CASCADE, otp_secret text NOT NULL, created_at timestamptz NOT NULL DEFAULT now(), updated_at timestamptz NOT NULL DEFAULT now() ); DROP TRIGGER IF EXISTS user_totp_set_updated_at ON user_totp; CREATE TRIGGER user_totp_set_updated_at BEFORE UPDATE ON user_totp FOR EACH ROW EXECUTE FUNCTION set_updated_at(); CREATE TABLE IF NOT EXISTS sessions ( session_id uuid PRIMARY KEY DEFAULT gen_random_uuid(), user_id uuid NOT NULL REFERENCES users(user_id) ON DELETE CASCADE, expires_at timestamptz NOT NULL, created_at timestamptz NOT NULL DEFAULT now(), updated_at timestamptz NOT NULL DEFAULT now() ); CREATE INDEX IF NOT EXISTS sessions_user_id_idx ON sessions(user_id); CREATE INDEX IF NOT EXISTS sessions_expires_at_idx ON sessions(expires_at); DROP TRIGGER IF EXISTS sessions_set_updated_at ON sessions; CREATE TRIGGER sessions_set_updated_at BEFORE UPDATE ON sessions FOR EACH ROW EXECUTE FUNCTION set_updated_at(); CREATE TABLE IF NOT EXISTS sites ( site_id uuid PRIMARY KEY DEFAULT gen_random_uuid(), user_id uuid NOT NULL REFERENCES users(user_id) ON DELETE CASCADE, url text NOT NULL, alias text NOT NULL, certificate_issuer text, certificate_issued_at timestamptz, certificate_expires_at timestamptz, certificate_checked_at timestamptz, certificate_check_error text, created_at timestamptz NOT NULL DEFAULT now(), updated_at timestamptz NOT NULL DEFAULT now(), UNIQUE (user_id, url) ); ALTER TABLE sites ADD COLUMN IF NOT EXISTS certificate_issuer text, ADD COLUMN IF NOT EXISTS certificate_issued_at timestamptz, ADD COLUMN IF NOT EXISTS certificate_expires_at timestamptz, ADD COLUMN IF NOT EXISTS certificate_checked_at timestamptz, ADD COLUMN IF NOT EXISTS certificate_check_error text; CREATE INDEX IF NOT EXISTS sites_user_id_idx ON sites(user_id); DROP TRIGGER IF EXISTS sites_set_updated_at ON sites; CREATE TRIGGER sites_set_updated_at BEFORE UPDATE ON sites FOR EACH ROW EXECUTE FUNCTION set_updated_at(); CREATE TABLE IF NOT EXISTS notification_methods ( notification_method_id uuid PRIMARY KEY DEFAULT gen_random_uuid(), user_id uuid NOT NULL REFERENCES users(user_id) ON DELETE CASCADE, notification_type text NOT NULL CHECK (notification_type IN ('webhook', 'push')), alias text NOT NULL, url text, push_endpoint text, push_p256dh text, push_auth text, created_at timestamptz NOT NULL DEFAULT now(), updated_at timestamptz NOT NULL DEFAULT now() ); CREATE INDEX IF NOT EXISTS notification_methods_user_id_idx ON notification_methods(user_id); DROP TRIGGER IF EXISTS notification_methods_set_updated_at ON notification_methods; CREATE TRIGGER notification_methods_set_updated_at BEFORE UPDATE ON notification_methods FOR EACH ROW EXECUTE FUNCTION set_updated_at(); CREATE TABLE IF NOT EXISTS site_alert_conditions ( site_alert_condition_id uuid PRIMARY KEY DEFAULT gen_random_uuid(), site_id uuid NOT NULL REFERENCES sites(site_id) ON DELETE CASCADE, condition_type text NOT NULL CHECK (condition_type IN ('expires_within_hours')), threshold_hours integer NOT NULL CHECK (threshold_hours > 0 AND threshold_hours <= 17520), webhook_method_ids uuid[] NOT NULL DEFAULT '{}', push_enabled boolean NOT NULL DEFAULT false, created_at timestamptz NOT NULL DEFAULT now(), updated_at timestamptz NOT NULL DEFAULT now(), UNIQUE (site_id, condition_type, threshold_hours) ); CREATE INDEX IF NOT EXISTS site_alert_conditions_site_id_idx ON site_alert_conditions(site_id); DROP TRIGGER IF EXISTS site_alert_conditions_set_updated_at ON site_alert_conditions; CREATE TRIGGER site_alert_conditions_set_updated_at BEFORE UPDATE ON site_alert_conditions FOR EACH ROW EXECUTE FUNCTION set_updated_at(); CREATE TABLE IF NOT EXISTS alert_history ( alert_id uuid PRIMARY KEY DEFAULT gen_random_uuid(), user_id uuid NOT NULL REFERENCES users(user_id) ON DELETE CASCADE, site_id uuid REFERENCES sites(site_id) ON DELETE SET NULL, alert_type text NOT NULL, content text NOT NULL, occurred_at timestamptz NOT NULL DEFAULT now(), read_at timestamptz, delivery_channels text[] NOT NULL DEFAULT ARRAY['app'], delivery_result jsonb NOT NULL DEFAULT '{}'::jsonb, dedupe_key text, created_at timestamptz NOT NULL DEFAULT now(), updated_at timestamptz NOT NULL DEFAULT now(), UNIQUE (user_id, dedupe_key) ); CREATE INDEX IF NOT EXISTS alert_history_user_id_idx ON alert_history(user_id); CREATE INDEX IF NOT EXISTS alert_history_site_id_idx ON alert_history(site_id); CREATE INDEX IF NOT EXISTS alert_history_occurred_at_idx ON alert_history(occurred_at); DROP TRIGGER IF EXISTS alert_history_set_updated_at ON alert_history; CREATE TRIGGER alert_history_set_updated_at BEFORE UPDATE ON alert_history FOR EACH ROW EXECUTE FUNCTION set_updated_at();