diff --git a/scripts/052_notification_prefs_drop_space_fk.sql b/scripts/052_notification_prefs_drop_space_fk.sql new file mode 100644 index 0000000..49be860 --- /dev/null +++ b/scripts/052_notification_prefs_drop_space_fk.sql @@ -0,0 +1,28 @@ +-- ============================================================================= +-- 052: HOTFIX -- drop notification_preferences.space_id -> spaces FK +-- ============================================================================= +-- PRODUCTION OUTAGE FIX. notification_preferences (migration 048) had FKs to +-- BOTH space_members(id) and spaces(id). PostgREST detects a table with FKs to +-- two tables as a many-to-many JUNCTION, so it found TWO ways to embed +-- `spaces` into `space_members` (the direct space_members.space_id FK AND the +-- notification_preferences junction) and refused the ambiguous embed with +-- PGRST201. The app layout resolves membership with +-- `space_members.select('*, spaces(*)')`; that query started erroring, so +-- `member` came back null and EVERY authenticated user was redirected to +-- /signup. +-- +-- Fix: drop the redundant space_id -> spaces FK. space_id stays as a NOT NULL +-- column (PK / RLS / queries use it); the row is still reachable to its space +-- through member_id -> space_members -> spaces, and deleting a space still +-- cascades (space -> space_members -> notification_preferences via the +-- member_id FK). With only one FK left, notification_preferences is no longer +-- a junction and the space_members <-> spaces embed is unambiguous again. +-- +-- Do NOT re-add a spaces FK to this table: it reintroduces the junction and +-- breaks every space_members/spaces embed across the app. +-- +-- Idempotent: DROP CONSTRAINT IF EXISTS. Apply as-is. +-- ============================================================================= + +ALTER TABLE public.notification_preferences + DROP CONSTRAINT IF EXISTS notification_preferences_space_id_fkey; diff --git a/scripts/schema.sql b/scripts/schema.sql index 2ef8a2e..e51531c 100644 --- a/scripts/schema.sql +++ b/scripts/schema.sql @@ -2758,12 +2758,15 @@ CREATE TABLE IF NOT EXISTS public.notifications ( last_error text, dedupe_key text NOT NULL, sent_at timestamptz, + read_at timestamptz, created_at timestamptz NOT NULL DEFAULT now(), updated_at timestamptz NOT NULL DEFAULT now() ); CREATE UNIQUE INDEX IF NOT EXISTS idx_notifications_dedupe ON public.notifications (space_id, dedupe_key); CREATE INDEX IF NOT EXISTS idx_notifications_pending ON public.notifications (created_at) WHERE status = 'pending'; CREATE INDEX IF NOT EXISTS idx_notifications_member ON public.notifications (space_id, member_id); +-- read_at IS NULL = unread; partial index backs the in-app inbox unread count. +CREATE INDEX IF NOT EXISTS idx_notifications_unread ON public.notifications (space_id, member_id) WHERE read_at IS NULL; ALTER TABLE public.notifications ENABLE ROW LEVEL SECURITY; DROP POLICY IF EXISTS notifications_select ON public.notifications; @@ -2784,8 +2787,13 @@ CREATE TRIGGER trg_notifications_touch -- 'skipped' instead of sending; the /me toggle writes through a validated -- service-client action. No client policy: default-deny, same convention as -- notifications / member_billing. +-- space_id has NO FK to spaces ON PURPOSE (migration 052): a table with FKs to +-- both space_members and spaces is read by PostgREST as a junction and makes +-- the space_members<->spaces embed ambiguous (PGRST201), which broke the auth +-- layout. The space is reachable via member_id -> space_members, and space +-- deletion still cascades through that FK. Do NOT add a spaces FK here. CREATE TABLE IF NOT EXISTS public.notification_preferences ( - space_id uuid NOT NULL REFERENCES public.spaces(id) ON DELETE CASCADE, + space_id uuid NOT NULL, member_id uuid NOT NULL REFERENCES public.space_members(id) ON DELETE CASCADE, category text NOT NULL, enabled boolean NOT NULL DEFAULT true,