-- ============================================================ -- whatsapp_config: enforce one user per phone_number_id -- -- The webhook routes inbound messages by `phone_number_id` and uses -- `.single()` to find the owning config row. If two users have saved -- the same `phone_number_id`, `.single()` errors PGRST116 ("multiple -- rows returned") and the webhook silently drops every inbound -- message — see issue #136. -- -- wacrm is single-tenant per WhatsApp number by design (RLS on -- conversations / messages is `auth.uid() = user_id`, so another user -- physically cannot read a conversation routed to a different owner). -- A UNIQUE constraint at the DB level makes that intent enforceable -- and stops races between the app-level check and the insert. -- -- ─── On existing data ─────────────────────────────────────────── -- If duplicates already exist in production, this migration FAILS -- LOUDLY rather than silently dropping rows. Auto-deduping would -- destroy user data (encrypted tokens, connection state) — the -- operator has to choose which user keeps the number. To resolve: -- -- SELECT phone_number_id, array_agg(user_id) AS owners -- FROM whatsapp_config -- GROUP BY phone_number_id -- HAVING count(*) > 1; -- -- Then DELETE the duplicate rows you don't want to keep and re-run -- migrations. -- -- Idempotent — safe to run multiple times once the constraint is in -- place. -- ============================================================ -- 1. Fail loudly if duplicates exist. Spelling out the conflicting -- phone_number_id and the user_ids that own it gives the operator -- a copy-pasteable starting point. DO $$ DECLARE conflict_count INT; sample TEXT; BEGIN SELECT count(*) INTO conflict_count FROM ( SELECT phone_number_id FROM whatsapp_config GROUP BY phone_number_id HAVING count(*) > 1 ) dupes; IF conflict_count > 0 THEN SELECT string_agg( phone_number_id || ' -> [' || array_to_string(owners, ', ') || ']', E'\n ' ) INTO sample FROM ( SELECT phone_number_id, array_agg(user_id::text) AS owners FROM whatsapp_config GROUP BY phone_number_id HAVING count(*) > 1 ) dupe_detail; RAISE EXCEPTION E'Cannot add UNIQUE(phone_number_id) on whatsapp_config — % phone_number_id value(s) are claimed by more than one user:\n %\nDelete the duplicate rows you do not want to keep (see migration comment), then re-run migrations.', conflict_count, sample; END IF; END $$; -- 2. Add the UNIQUE constraint. PostgreSQL has no "ADD CONSTRAINT IF -- NOT EXISTS", so guard via pg_constraint. DO $$ BEGIN IF NOT EXISTS ( SELECT 1 FROM pg_constraint WHERE conname = 'whatsapp_config_phone_number_id_key' AND conrelid = 'whatsapp_config'::regclass ) THEN ALTER TABLE whatsapp_config ADD CONSTRAINT whatsapp_config_phone_number_id_key UNIQUE (phone_number_id); END IF; END $$;