create table if not exists app_private.platform_secrets ( id uuid primary key default gen_random_uuid(), secret_scope text not null check (secret_scope in ('webhook', 'sms', 'email', 'payment', 'system', 'ai')), secret_key text not null, secret_value text, secret_json jsonb not null default '{}'::jsonb, provider text, last_rotated_at timestamptz, created_at timestamptz not null default now(), updated_at timestamptz not null default now(), unique (secret_scope, secret_key) ); comment on table app_private.platform_secrets is 'Private platform-level secrets for SaaS operations. API responses must expose only secretRef/masked metadata.'; create table if not exists public.platform_audit_notification_channels ( id uuid primary key default gen_random_uuid(), channel_code text not null unique, name text not null, description text, enabled boolean not null default true, provider text not null default 'generic' check (provider in ('generic', 'dingtalk', 'feishu', 'wecom')), webhook_url text not null, secret_ref text, min_severity text not null default 'medium' check (min_severity in ('low', 'medium', 'high', 'critical')), status_filter text[] not null default array['open']::text[], action_patterns text[] not null default '{}'::text[], tenant_ids uuid[] not null default '{}'::uuid[], timeout_sec integer not null default 10 check (timeout_sec between 1 and 60), metadata jsonb not null default '{}'::jsonb, created_at timestamptz not null default now(), updated_at timestamptz not null default now(), constraint platform_audit_notification_status_filter_check check (status_filter <@ array['open', 'acknowledged', 'resolved', 'ignored']::text[]) ); create table if not exists public.platform_audit_notification_events ( id uuid primary key default gen_random_uuid(), channel_id uuid not null references public.platform_audit_notification_channels(id) on delete cascade, alert_id uuid not null references public.platform_audit_alerts(id) on delete cascade, audit_log_id uuid references public.audit_logs(id) on delete set null, provider text not null default 'generic' check (provider in ('generic', 'dingtalk', 'feishu', 'wecom')), status text not null default 'pending' check (status in ('pending', 'processing', 'sent', 'retrying', 'failed', 'discarded')), attempts integer not null default 0 check (attempts >= 0), scheduled_at timestamptz not null default now(), next_attempt_at timestamptz, last_attempt_at timestamptz, sent_at timestamptz, last_error text, last_http_code integer, last_response_summary text, request_payload jsonb not null default '{}'::jsonb, metadata jsonb not null default '{}'::jsonb, created_at timestamptz not null default now(), updated_at timestamptz not null default now(), unique (channel_id, alert_id) ); create index if not exists idx_platform_audit_notification_channels_enabled on public.platform_audit_notification_channels(enabled, min_severity, channel_code); create index if not exists idx_platform_audit_notification_events_due on public.platform_audit_notification_events(status, coalesce(next_attempt_at, scheduled_at), created_at); create index if not exists idx_platform_audit_notification_events_alert on public.platform_audit_notification_events(alert_id, status, created_at desc); create index if not exists idx_platform_audit_notification_events_channel on public.platform_audit_notification_events(channel_id, status, created_at desc); create index if not exists idx_platform_secrets_lookup on app_private.platform_secrets(secret_scope, secret_key); alter table app_private.platform_secrets enable row level security; alter table public.platform_audit_notification_channels enable row level security; alter table public.platform_audit_notification_events enable row level security; drop policy if exists platform_admin_platform_secrets on app_private.platform_secrets; create policy platform_admin_platform_secrets on app_private.platform_secrets for all using (app.is_platform_admin()) with check (app.is_platform_admin()); drop policy if exists platform_admin_audit_notification_channels on public.platform_audit_notification_channels; create policy platform_admin_audit_notification_channels on public.platform_audit_notification_channels for all using (app.is_platform_admin()) with check (app.is_platform_admin()); drop policy if exists platform_admin_audit_notification_events on public.platform_audit_notification_events; create policy platform_admin_audit_notification_events on public.platform_audit_notification_events for all using (app.is_platform_admin()) with check (app.is_platform_admin()); drop trigger if exists set_updated_at on app_private.platform_secrets; create trigger set_updated_at before update on app_private.platform_secrets for each row execute function app.touch_updated_at(); drop trigger if exists set_updated_at on public.platform_audit_notification_channels; create trigger set_updated_at before update on public.platform_audit_notification_channels for each row execute function app.touch_updated_at(); drop trigger if exists set_updated_at on public.platform_audit_notification_events; create trigger set_updated_at before update on public.platform_audit_notification_events for each row execute function app.touch_updated_at();