Files
terdut-server/internal/db/migrations/001_schema.sql
T
Niklas Ye 9029d48584
CI / chart (pull_request) Successful in 2s
CI / security (pull_request) Failing after 19s
CI / test (pull_request) Successful in 5m34s
Let an operator authenticate with a seeded key, and reset the schema
- TERDUT_OPERATOR_KEY creates or re-keys the instance-scoped service account
  "terdut-operator" at every start, so terdut-operator needs no bootstrap
  handshake. An instance-scoped account now acts as owner of every team's
  configuration, but is not a member of any team.
- POST /api/teams takes an external_id (instance service accounts only) and
  is idempotent on it, so automation finds its own team again after a crash
  instead of adopting by display name. GET /api/teams?name= is removed.
- Integration and dead man's switch names are unique per team (409). The
  escalation PUT accepts usernames and resolves them itself.
- The 18 migrations are squashed into 001_schema.sql, with no Default team.
  TERDUT_DEADMAN_* and the env seeding of switches are removed: teams carry
  their own. Existing development databases must be recreated.

Security and robustness:
- GET /api/users no longer returns other people's email or ntfy topic to
  non-admins.
- The access log records the route pattern, so integration keys and ack
  tokens in the path are not written to the log. Server errors are logged.
- Rate limits take the client address TERDUT_TRUSTED_PROXIES hops from the
  right of X-Forwarded-For instead of trusting the first, forgeable entry.
- /api/bootstrap runs in a transaction under an advisory lock, so two
  concurrent calls cannot both create an administrator.
- API key last_used_at is written at most every five minutes.

Cleanup: remove GET /api/incidents/{id}/alerts, unused exports, SQLite
remnants in comments and config.

Claude-Session: https://claude.ai/code/session_016mBLURvJoMuUEr9cB2RpUN
2026-10-09 14:56:13 +02:00

588 lines
22 KiB
SQL

-- Terdut Server schema. One baseline: the project has not shipped, so the
-- migration history that led here (SQLite import, a Default team, per-team
-- deadman configs later replaced by switches) is not carried. Later changes are
-- new numbered files after this one.
--
-- Timestamps are Unix epoch seconds in BIGINT columns throughout.
CREATE TABLE alerts (
id bigint GENERATED BY DEFAULT AS IDENTITY NOT NULL,
fingerprint text NOT NULL,
name text NOT NULL,
status text NOT NULL,
labels jsonb DEFAULT '{}'::jsonb NOT NULL,
annotations jsonb DEFAULT '{}'::jsonb NOT NULL,
starts_at bigint NOT NULL,
ends_at bigint,
generator_url text DEFAULT ''::text NOT NULL,
received_at bigint DEFAULT (floor(EXTRACT(epoch FROM now())))::bigint NOT NULL,
archived_at bigint,
resolution_source text,
team_id bigint NOT NULL,
integration_id bigint,
CONSTRAINT alerts_status_check CHECK ((status = ANY (ARRAY['firing'::text, 'resolved'::text])))
);
CREATE TABLE api_keys (
id bigint GENERATED BY DEFAULT AS IDENTITY NOT NULL,
user_id bigint NOT NULL,
key_hash text NOT NULL,
name text NOT NULL,
created_at bigint DEFAULT (floor(EXTRACT(epoch FROM now())))::bigint NOT NULL,
last_used_at bigint,
expires_at bigint
);
CREATE TABLE deadman_switches (
id bigint GENERATED BY DEFAULT AS IDENTITY NOT NULL,
team_id bigint NOT NULL,
name text NOT NULL,
matcher text NOT NULL,
timeout_seconds bigint NOT NULL,
severity text DEFAULT 'critical'::text NOT NULL,
created_at bigint DEFAULT (floor(EXTRACT(epoch FROM now())))::bigint NOT NULL,
CONSTRAINT deadman_switches_timeout_seconds_check CHECK ((timeout_seconds > 0))
);
CREATE TABLE device_logins (
device_hash text NOT NULL,
user_code text NOT NULL,
status text DEFAULT 'pending'::text NOT NULL,
user_id bigint,
expires_at bigint NOT NULL,
last_polled_at bigint DEFAULT 0 NOT NULL,
CONSTRAINT device_logins_status_check CHECK ((status = ANY (ARRAY['pending'::text, 'approved'::text, 'denied'::text])))
);
CREATE TABLE escalation_levels (
id bigint GENERATED BY DEFAULT AS IDENTITY NOT NULL,
team_id bigint NOT NULL,
"position" bigint NOT NULL,
timeout_seconds bigint NOT NULL,
CONSTRAINT escalation_levels_timeout_seconds_check CHECK ((timeout_seconds > 0))
);
CREATE TABLE escalation_policies (
team_id bigint NOT NULL,
repeat_count bigint DEFAULT 0 NOT NULL,
fallback_topic text DEFAULT ''::text NOT NULL,
updated_at bigint DEFAULT (floor(EXTRACT(epoch FROM now())))::bigint NOT NULL,
CONSTRAINT escalation_policies_repeat_count_check CHECK (((repeat_count >= 0) AND (repeat_count <= 10)))
);
CREATE TABLE escalation_targets (
id bigint GENERATED BY DEFAULT AS IDENTITY NOT NULL,
level_id bigint NOT NULL,
kind text NOT NULL,
user_id bigint,
CONSTRAINT escalation_targets_check CHECK ((((kind = 'user'::text) AND (user_id IS NOT NULL)) OR ((kind = 'oncall'::text) AND (user_id IS NULL)))),
CONSTRAINT escalation_targets_kind_check CHECK ((kind = ANY (ARRAY['user'::text, 'oncall'::text])))
);
CREATE TABLE incident_ack_tokens (
token_hash text NOT NULL,
incident_id bigint NOT NULL,
user_id bigint NOT NULL,
created_at bigint NOT NULL,
expires_at bigint NOT NULL
);
CREATE TABLE incident_alerts (
incident_id bigint NOT NULL,
alert_id bigint NOT NULL,
added_at bigint DEFAULT (floor(EXTRACT(epoch FROM now())))::bigint NOT NULL
);
CREATE TABLE incident_events (
id bigint GENERATED BY DEFAULT AS IDENTITY NOT NULL,
incident_id bigint NOT NULL,
type text NOT NULL,
user_id bigint,
alert_id bigint,
detail text,
created_at bigint DEFAULT (floor(EXTRACT(epoch FROM now())))::bigint NOT NULL,
service_account_id bigint,
actor_user_id bigint,
actor_service_account_id bigint,
CONSTRAINT incident_events_actor_xor_chk CHECK (((user_id IS NULL) OR (service_account_id IS NULL))),
CONSTRAINT incident_events_assign_actor_xor_chk CHECK (((actor_user_id IS NULL) OR (actor_service_account_id IS NULL)))
);
CREATE TABLE incidents (
id bigint GENERATED BY DEFAULT AS IDENTITY NOT NULL,
group_key text NOT NULL,
title text NOT NULL,
group_labels jsonb DEFAULT '{}'::jsonb NOT NULL,
status text NOT NULL,
severity text,
triggered_at bigint NOT NULL,
acknowledged_by bigint,
acknowledged_at bigint,
assigned_to bigint,
snoozed_until bigint,
resolved_at bigint,
resolution_source text,
archived_at bigint,
team_id bigint NOT NULL,
escalation_level bigint DEFAULT 0 NOT NULL,
escalation_level_at bigint,
escalation_round bigint DEFAULT 0 NOT NULL,
signature text DEFAULT ''::text NOT NULL,
acknowledged_by_service_account_id bigint,
CONSTRAINT incidents_ack_actor_xor_chk CHECK (((acknowledged_by IS NULL) OR (acknowledged_by_service_account_id IS NULL))),
CONSTRAINT incidents_status_check CHECK ((status = ANY (ARRAY['triggered'::text, 'acknowledged'::text, 'resolved'::text])))
);
CREATE TABLE integrations (
id bigint GENERATED BY DEFAULT AS IDENTITY NOT NULL,
team_id bigint NOT NULL,
kind text NOT NULL,
name text NOT NULL,
key_hash text NOT NULL,
created_at bigint DEFAULT (floor(EXTRACT(epoch FROM now())))::bigint NOT NULL,
last_used_at bigint,
CONSTRAINT integrations_kind_check CHECK ((kind = 'alertmanager'::text))
);
CREATE TABLE invites (
id bigint GENERATED BY DEFAULT AS IDENTITY NOT NULL,
token_hash text NOT NULL,
team_id bigint NOT NULL,
role text NOT NULL,
created_by bigint,
created_at bigint DEFAULT (floor(EXTRACT(epoch FROM now())))::bigint NOT NULL,
expires_at bigint NOT NULL,
max_uses bigint DEFAULT 1 NOT NULL,
uses bigint DEFAULT 0 NOT NULL,
revoked_at bigint,
CONSTRAINT invites_max_uses_check CHECK (((max_uses > 0) AND (max_uses <= 100))),
CONSTRAINT invites_role_check CHECK ((role = ANY (ARRAY['owner'::text, 'member'::text])))
);
CREATE TABLE notifications (
id bigint GENERATED BY DEFAULT AS IDENTITY NOT NULL,
incident_id bigint NOT NULL,
user_id bigint,
topic text NOT NULL,
kind text NOT NULL,
created_at bigint NOT NULL,
send_after bigint NOT NULL,
attempts bigint DEFAULT 0 NOT NULL,
sent_at bigint,
last_error text,
CONSTRAINT notifications_kind_check CHECK ((kind = ANY (ARRAY['triggered'::text, 'reminder'::text, 'resolved'::text, 'escalated'::text])))
);
CREATE TABLE oidc_logins (
state_hash text NOT NULL,
nonce text NOT NULL,
pkce_verifier text NOT NULL,
expires_at bigint NOT NULL,
next text DEFAULT '/'::text NOT NULL
);
CREATE TABLE rate_limit_counters (
key text NOT NULL,
window_start bigint NOT NULL,
count integer NOT NULL
);
CREATE TABLE schedule_entries (
id bigint GENERATED BY DEFAULT AS IDENTITY NOT NULL,
user_id bigint NOT NULL,
date text NOT NULL,
created_at bigint DEFAULT (floor(EXTRACT(epoch FROM now())))::bigint NOT NULL,
team_id bigint NOT NULL
);
CREATE TABLE service_account_keys (
id bigint GENERATED BY DEFAULT AS IDENTITY NOT NULL,
service_account_id bigint NOT NULL,
key_hash text NOT NULL,
name text NOT NULL,
created_at bigint DEFAULT (floor(EXTRACT(epoch FROM now())))::bigint NOT NULL,
last_used_at bigint
);
CREATE TABLE service_accounts (
id bigint GENERATED BY DEFAULT AS IDENTITY NOT NULL,
name text NOT NULL,
scope text NOT NULL,
team_id bigint,
created_by bigint,
created_at bigint DEFAULT (floor(EXTRACT(epoch FROM now())))::bigint NOT NULL,
CONSTRAINT service_accounts_scope_check CHECK ((scope = ANY (ARRAY['instance'::text, 'team'::text]))),
CONSTRAINT service_accounts_scope_team_id_chk CHECK ((((scope = 'team'::text) AND (team_id IS NOT NULL)) OR ((scope = 'instance'::text) AND (team_id IS NULL))))
);
CREATE TABLE sessions (
id bigint GENERATED BY DEFAULT AS IDENTITY NOT NULL,
token_hash text NOT NULL,
user_id bigint NOT NULL,
created_at bigint NOT NULL,
last_seen_at bigint NOT NULL,
expires_at bigint NOT NULL,
user_agent text,
max_expires_at bigint
);
CREATE TABLE settings (
key text NOT NULL,
value text NOT NULL,
updated_at bigint DEFAULT (floor(EXTRACT(epoch FROM now())))::bigint NOT NULL
);
CREATE TABLE team_members (
team_id bigint NOT NULL,
user_id bigint NOT NULL,
role text NOT NULL,
joined_at bigint DEFAULT (floor(EXTRACT(epoch FROM now())))::bigint NOT NULL,
source text DEFAULT 'manual'::text NOT NULL,
CONSTRAINT team_members_role_check CHECK ((role = ANY (ARRAY['owner'::text, 'member'::text]))),
CONSTRAINT team_members_source_check CHECK ((source = ANY (ARRAY['manual'::text, 'oidc'::text])))
);
CREATE TABLE teams (
id bigint GENERATED BY DEFAULT AS IDENTITY NOT NULL,
name text NOT NULL,
created_at bigint DEFAULT (floor(EXTRACT(epoch FROM now())))::bigint NOT NULL,
oidc_member_group text,
oidc_owner_group text,
-- A stable identity for a team managed by automation (terdut-operator:
-- "<namespace>/<name>" of its TerdutTeam), so it can find or recreate its own
-- team without trusting a display name. NULL for a team a person made.
external_id text
);
CREATE TABLE user_identities (
id bigint GENERATED BY DEFAULT AS IDENTITY NOT NULL,
user_id bigint NOT NULL,
issuer text NOT NULL,
subject text NOT NULL,
created_at bigint DEFAULT (floor(EXTRACT(epoch FROM now())))::bigint NOT NULL,
last_login_at bigint DEFAULT (floor(EXTRACT(epoch FROM now())))::bigint NOT NULL
);
CREATE TABLE users (
id bigint GENERATED BY DEFAULT AS IDENTITY NOT NULL,
username text NOT NULL,
email text NOT NULL,
created_at bigint DEFAULT (floor(EXTRACT(epoch FROM now())))::bigint NOT NULL,
ntfy_topic text,
password_hash text,
is_admin boolean DEFAULT false NOT NULL,
disabled_at bigint,
invited_via bigint,
onboarding_dismissed_at bigint,
admin_source text DEFAULT 'manual'::text NOT NULL,
CONSTRAINT users_admin_source_check CHECK ((admin_source = ANY (ARRAY['manual'::text, 'oidc'::text])))
);
ALTER TABLE alerts
ADD CONSTRAINT alerts_pkey PRIMARY KEY (id);
ALTER TABLE api_keys
ADD CONSTRAINT api_keys_key_hash_key UNIQUE (key_hash);
ALTER TABLE api_keys
ADD CONSTRAINT api_keys_pkey PRIMARY KEY (id);
ALTER TABLE deadman_switches
ADD CONSTRAINT deadman_switches_pkey PRIMARY KEY (id);
ALTER TABLE device_logins
ADD CONSTRAINT device_logins_pkey PRIMARY KEY (device_hash);
ALTER TABLE device_logins
ADD CONSTRAINT device_logins_user_code_key UNIQUE (user_code);
ALTER TABLE escalation_levels
ADD CONSTRAINT escalation_levels_pkey PRIMARY KEY (id);
ALTER TABLE escalation_levels
ADD CONSTRAINT escalation_levels_team_id_position_key UNIQUE (team_id, "position");
ALTER TABLE escalation_policies
ADD CONSTRAINT escalation_policies_pkey PRIMARY KEY (team_id);
ALTER TABLE escalation_targets
ADD CONSTRAINT escalation_targets_pkey PRIMARY KEY (id);
ALTER TABLE incident_ack_tokens
ADD CONSTRAINT incident_ack_tokens_pkey PRIMARY KEY (token_hash);
ALTER TABLE incident_alerts
ADD CONSTRAINT incident_alerts_pkey PRIMARY KEY (incident_id, alert_id);
ALTER TABLE incident_events
ADD CONSTRAINT incident_events_pkey PRIMARY KEY (id);
ALTER TABLE incidents
ADD CONSTRAINT incidents_pkey PRIMARY KEY (id);
ALTER TABLE integrations
ADD CONSTRAINT integrations_key_hash_key UNIQUE (key_hash);
ALTER TABLE integrations
ADD CONSTRAINT integrations_pkey PRIMARY KEY (id);
ALTER TABLE invites
ADD CONSTRAINT invites_pkey PRIMARY KEY (id);
ALTER TABLE invites
ADD CONSTRAINT invites_token_hash_key UNIQUE (token_hash);
ALTER TABLE notifications
ADD CONSTRAINT notifications_pkey PRIMARY KEY (id);
ALTER TABLE oidc_logins
ADD CONSTRAINT oidc_logins_pkey PRIMARY KEY (state_hash);
ALTER TABLE rate_limit_counters
ADD CONSTRAINT rate_limit_counters_pkey PRIMARY KEY (key);
ALTER TABLE schedule_entries
ADD CONSTRAINT schedule_entries_pkey PRIMARY KEY (id);
ALTER TABLE service_account_keys
ADD CONSTRAINT service_account_keys_key_hash_key UNIQUE (key_hash);
ALTER TABLE service_account_keys
ADD CONSTRAINT service_account_keys_pkey PRIMARY KEY (id);
ALTER TABLE service_accounts
ADD CONSTRAINT service_accounts_name_key UNIQUE (name);
ALTER TABLE service_accounts
ADD CONSTRAINT service_accounts_pkey PRIMARY KEY (id);
ALTER TABLE sessions
ADD CONSTRAINT sessions_pkey PRIMARY KEY (id);
ALTER TABLE sessions
ADD CONSTRAINT sessions_token_hash_key UNIQUE (token_hash);
ALTER TABLE settings
ADD CONSTRAINT settings_pkey PRIMARY KEY (key);
ALTER TABLE team_members
ADD CONSTRAINT team_members_pkey PRIMARY KEY (team_id, user_id);
ALTER TABLE teams
ADD CONSTRAINT teams_name_key UNIQUE (name);
ALTER TABLE teams
ADD CONSTRAINT teams_external_id_key UNIQUE (external_id);
ALTER TABLE teams
ADD CONSTRAINT teams_pkey PRIMARY KEY (id);
ALTER TABLE user_identities
ADD CONSTRAINT user_identities_issuer_subject_key UNIQUE (issuer, subject);
ALTER TABLE user_identities
ADD CONSTRAINT user_identities_pkey PRIMARY KEY (id);
ALTER TABLE users
ADD CONSTRAINT users_email_key UNIQUE (email);
ALTER TABLE users
ADD CONSTRAINT users_pkey PRIMARY KEY (id);
ALTER TABLE users
ADD CONSTRAINT users_username_key UNIQUE (username);
CREATE INDEX alerts_archived_at_idx ON alerts USING btree (archived_at);
CREATE INDEX alerts_integration_idx ON alerts USING btree (integration_id, received_at) WHERE (integration_id IS NOT NULL);
CREATE INDEX alerts_name_idx ON alerts USING btree (name);
CREATE INDEX alerts_received_at_idx ON alerts USING btree (received_at DESC);
CREATE INDEX alerts_status_idx ON alerts USING btree (status);
CREATE UNIQUE INDEX alerts_team_fingerprint_idx ON alerts USING btree (team_id, fingerprint);
CREATE INDEX alerts_team_received_idx ON alerts USING btree (team_id, received_at DESC);
CREATE INDEX deadman_switches_team_idx ON deadman_switches USING btree (team_id);
CREATE INDEX device_logins_expires_idx ON device_logins USING btree (expires_at);
CREATE INDEX escalation_targets_level_idx ON escalation_targets USING btree (level_id);
CREATE INDEX idx_sessions_user ON sessions USING btree (user_id);
CREATE INDEX incident_ack_tokens_expires_idx ON incident_ack_tokens USING btree (expires_at);
CREATE INDEX incident_alerts_alert_id_idx ON incident_alerts USING btree (alert_id);
CREATE INDEX incident_events_actor_service_account_id_idx ON incident_events USING btree (actor_service_account_id);
CREATE INDEX incident_events_actor_user_id_idx ON incident_events USING btree (actor_user_id);
CREATE INDEX incident_events_incident_idx ON incident_events USING btree (incident_id, created_at);
CREATE INDEX incident_events_service_account_id_idx ON incident_events USING btree (service_account_id);
CREATE INDEX incidents_acknowledged_by_service_account_id_idx ON incidents USING btree (acknowledged_by_service_account_id);
CREATE INDEX incidents_archived_at_idx ON incidents USING btree (archived_at);
CREATE INDEX incidents_escalation_idx ON incidents USING btree (escalation_level_at) WHERE ((resolved_at IS NULL) AND (status = 'triggered'::text));
CREATE UNIQUE INDEX incidents_open_group_key_idx ON incidents USING btree (team_id, group_key) WHERE (resolved_at IS NULL);
CREATE INDEX incidents_signature_idx ON incidents USING btree (team_id, signature, triggered_at DESC);
CREATE INDEX incidents_status_idx ON incidents USING btree (status);
CREATE INDEX incidents_team_triggered_idx ON incidents USING btree (team_id, triggered_at DESC);
CREATE INDEX incidents_triggered_at_idx ON incidents USING btree (triggered_at DESC);
CREATE INDEX integrations_team_idx ON integrations USING btree (team_id);
-- A name identifies an integration (and a switch) within its team, so a client
-- that manages them declaratively can look one up by name instead of listing
-- and matching.
CREATE UNIQUE INDEX integrations_team_name_key ON integrations (team_id, name);
CREATE UNIQUE INDEX deadman_switches_team_name_key ON deadman_switches (team_id, name);
CREATE INDEX invites_team_idx ON invites USING btree (team_id);
CREATE INDEX notifications_incident_idx ON notifications USING btree (incident_id, id DESC);
CREATE INDEX notifications_pending_idx ON notifications USING btree (send_after) WHERE (sent_at IS NULL);
CREATE INDEX oidc_logins_expires_idx ON oidc_logins USING btree (expires_at);
CREATE INDEX schedule_entries_date_idx ON schedule_entries USING btree (date);
CREATE UNIQUE INDEX schedule_entries_team_date_idx ON schedule_entries USING btree (team_id, date);
CREATE INDEX service_account_keys_service_account_id_idx ON service_account_keys USING btree (service_account_id);
CREATE INDEX service_accounts_team_id_idx ON service_accounts USING btree (team_id);
CREATE INDEX team_members_user_idx ON team_members USING btree (user_id);
CREATE INDEX user_identities_user_idx ON user_identities USING btree (user_id);
CREATE INDEX users_is_admin_idx ON users USING btree (is_admin) WHERE is_admin;
ALTER TABLE alerts
ADD CONSTRAINT alerts_integration_id_fkey FOREIGN KEY (integration_id) REFERENCES integrations(id) ON DELETE SET NULL;
ALTER TABLE alerts
ADD CONSTRAINT alerts_team_id_fkey FOREIGN KEY (team_id) REFERENCES teams(id) ON DELETE CASCADE;
ALTER TABLE api_keys
ADD CONSTRAINT api_keys_user_id_fkey FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE;
ALTER TABLE deadman_switches
ADD CONSTRAINT deadman_switches_team_id_fkey FOREIGN KEY (team_id) REFERENCES teams(id) ON DELETE CASCADE;
ALTER TABLE device_logins
ADD CONSTRAINT device_logins_user_id_fkey FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE;
ALTER TABLE escalation_levels
ADD CONSTRAINT escalation_levels_team_id_fkey FOREIGN KEY (team_id) REFERENCES escalation_policies(team_id) ON DELETE CASCADE;
ALTER TABLE escalation_policies
ADD CONSTRAINT escalation_policies_team_id_fkey FOREIGN KEY (team_id) REFERENCES teams(id) ON DELETE CASCADE;
ALTER TABLE escalation_targets
ADD CONSTRAINT escalation_targets_level_id_fkey FOREIGN KEY (level_id) REFERENCES escalation_levels(id) ON DELETE CASCADE;
ALTER TABLE escalation_targets
ADD CONSTRAINT escalation_targets_user_id_fkey FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE;
ALTER TABLE incident_ack_tokens
ADD CONSTRAINT incident_ack_tokens_incident_id_fkey FOREIGN KEY (incident_id) REFERENCES incidents(id) ON DELETE CASCADE;
ALTER TABLE incident_ack_tokens
ADD CONSTRAINT incident_ack_tokens_user_id_fkey FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE;
ALTER TABLE incident_alerts
ADD CONSTRAINT incident_alerts_alert_id_fkey FOREIGN KEY (alert_id) REFERENCES alerts(id) ON DELETE CASCADE;
ALTER TABLE incident_alerts
ADD CONSTRAINT incident_alerts_incident_id_fkey FOREIGN KEY (incident_id) REFERENCES incidents(id) ON DELETE CASCADE;
ALTER TABLE incident_events
ADD CONSTRAINT incident_events_actor_service_account_id_fkey FOREIGN KEY (actor_service_account_id) REFERENCES service_accounts(id) ON DELETE SET NULL;
ALTER TABLE incident_events
ADD CONSTRAINT incident_events_actor_user_id_fkey FOREIGN KEY (actor_user_id) REFERENCES users(id) ON DELETE SET NULL;
ALTER TABLE incident_events
ADD CONSTRAINT incident_events_alert_id_fkey FOREIGN KEY (alert_id) REFERENCES alerts(id) ON DELETE SET NULL;
ALTER TABLE incident_events
ADD CONSTRAINT incident_events_incident_id_fkey FOREIGN KEY (incident_id) REFERENCES incidents(id) ON DELETE CASCADE;
ALTER TABLE incident_events
ADD CONSTRAINT incident_events_service_account_id_fkey FOREIGN KEY (service_account_id) REFERENCES service_accounts(id) ON DELETE SET NULL;
ALTER TABLE incident_events
ADD CONSTRAINT incident_events_user_id_fkey FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE SET NULL;
ALTER TABLE incidents
ADD CONSTRAINT incidents_acknowledged_by_fkey FOREIGN KEY (acknowledged_by) REFERENCES users(id) ON DELETE SET NULL;
ALTER TABLE incidents
ADD CONSTRAINT incidents_acknowledged_by_service_account_id_fkey FOREIGN KEY (acknowledged_by_service_account_id) REFERENCES service_accounts(id) ON DELETE SET NULL;
ALTER TABLE incidents
ADD CONSTRAINT incidents_assigned_to_fkey FOREIGN KEY (assigned_to) REFERENCES users(id) ON DELETE SET NULL;
ALTER TABLE incidents
ADD CONSTRAINT incidents_team_id_fkey FOREIGN KEY (team_id) REFERENCES teams(id) ON DELETE CASCADE;
ALTER TABLE integrations
ADD CONSTRAINT integrations_team_id_fkey FOREIGN KEY (team_id) REFERENCES teams(id) ON DELETE CASCADE;
ALTER TABLE invites
ADD CONSTRAINT invites_created_by_fkey FOREIGN KEY (created_by) REFERENCES users(id) ON DELETE SET NULL;
ALTER TABLE invites
ADD CONSTRAINT invites_team_id_fkey FOREIGN KEY (team_id) REFERENCES teams(id) ON DELETE CASCADE;
ALTER TABLE notifications
ADD CONSTRAINT notifications_incident_id_fkey FOREIGN KEY (incident_id) REFERENCES incidents(id) ON DELETE CASCADE;
ALTER TABLE notifications
ADD CONSTRAINT notifications_user_id_fkey FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE SET NULL;
ALTER TABLE schedule_entries
ADD CONSTRAINT schedule_entries_team_id_fkey FOREIGN KEY (team_id) REFERENCES teams(id) ON DELETE CASCADE;
ALTER TABLE schedule_entries
ADD CONSTRAINT schedule_entries_user_id_fkey FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE;
ALTER TABLE service_account_keys
ADD CONSTRAINT service_account_keys_service_account_id_fkey FOREIGN KEY (service_account_id) REFERENCES service_accounts(id) ON DELETE CASCADE;
ALTER TABLE service_accounts
ADD CONSTRAINT service_accounts_created_by_fkey FOREIGN KEY (created_by) REFERENCES users(id) ON DELETE SET NULL;
ALTER TABLE service_accounts
ADD CONSTRAINT service_accounts_team_id_fkey FOREIGN KEY (team_id) REFERENCES teams(id) ON DELETE CASCADE;
ALTER TABLE sessions
ADD CONSTRAINT sessions_user_id_fkey FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE;
ALTER TABLE team_members
ADD CONSTRAINT team_members_team_id_fkey FOREIGN KEY (team_id) REFERENCES teams(id) ON DELETE CASCADE;
ALTER TABLE team_members
ADD CONSTRAINT team_members_user_id_fkey FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE;
ALTER TABLE user_identities
ADD CONSTRAINT user_identities_user_id_fkey FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE;
ALTER TABLE users
ADD CONSTRAINT users_invited_via_fkey FOREIGN KEY (invited_via) REFERENCES invites(id) ON DELETE SET NULL;