feat(backend): add standalone data plane

Replace the Supabase application data plane with the Node/PostgreSQL API, migrate mobile, admin, web, and MCP callers, add controlled cutover tooling, and define hardened Dokploy infrastructure.
This commit is contained in:
jjbrunton committed 2026-08-28 14:42:11 +01:00
1 parent 01bb8225ae
commit 006761d3b0
294 files changed
+16259 -6138

No files matched your search

@@ -0,0 +1,161 @@
ALTER TABLE question_packs
ADD COLUMN IF NOT EXISTS scheduled_release_at timestamptz,
ADD COLUMN IF NOT EXISTS release_notified boolean NOT NULL DEFAULT false;
--> statement-breakpoint
ALTER TABLE profiles
ADD COLUMN IF NOT EXISTS last_unpaired_reminder_at timestamptz;
--> statement-breakpoint
CREATE TABLE IF NOT EXISTS pending_match_notifications (
couple_id uuid PRIMARY KEY REFERENCES couples(id) ON DELETE CASCADE,
active_user_id uuid NOT NULL REFERENCES profiles(id) ON DELETE CASCADE,
match_count integer NOT NULL DEFAULT 0,
response_count integer NOT NULL DEFAULT 1,
latest_match_id uuid REFERENCES matches(id) ON DELETE SET NULL,
notify_at timestamptz NOT NULL,
created_at timestamptz NOT NULL DEFAULT now(),
updated_at timestamptz NOT NULL DEFAULT now()
);
--> statement-breakpoint
CREATE INDEX IF NOT EXISTS pending_match_notifications_due_idx ON pending_match_notifications(notify_at);
--> statement-breakpoint
CREATE TABLE IF NOT EXISTS pending_pack_notifications (
couple_id uuid PRIMARY KEY REFERENCES couples(id) ON DELETE CASCADE,
changed_by_user_id uuid NOT NULL REFERENCES profiles(id) ON DELETE CASCADE,
notify_at timestamptz NOT NULL,
created_at timestamptz NOT NULL DEFAULT now(),
updated_at timestamptz NOT NULL DEFAULT now()
);
--> statement-breakpoint
CREATE INDEX IF NOT EXISTS pending_pack_notifications_due_idx ON pending_pack_notifications(notify_at);
--> statement-breakpoint
CREATE TABLE IF NOT EXISTS catchup_reminder_tracking (
user_id uuid PRIMARY KEY REFERENCES profiles(id) ON DELETE CASCADE,
pending_since timestamptz,
last_reminder_sent_at timestamptz,
reminder_count integer NOT NULL DEFAULT 0 CHECK (reminder_count >= 0)
);
--> statement-breakpoint
CREATE TABLE IF NOT EXISTS operations_outbox (
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
kind text NOT NULL CHECK (kind IN ('expo', 'discord', 'classify')),
dedupe_key text NOT NULL UNIQUE,
recipient_id uuid REFERENCES profiles(id) ON DELETE CASCADE,
payload jsonb NOT NULL,
available_at timestamptz NOT NULL DEFAULT now(),
attempts integer NOT NULL DEFAULT 0 CHECK (attempts >= 0),
locked_at timestamptz,
sent_at timestamptz,
last_error text,
created_at timestamptz NOT NULL DEFAULT now()
);
--> statement-breakpoint
CREATE INDEX IF NOT EXISTS operations_outbox_due_idx
ON operations_outbox(available_at, created_at) WHERE sent_at IS NULL;
--> statement-breakpoint
CREATE OR REPLACE FUNCTION queue_response_digest() RETURNS trigger LANGUAGE plpgsql AS $$
BEGIN
IF current_setting('sauci.suppress_operations',true)='on' THEN RETURN NEW; END IF;
INSERT INTO pending_match_notifications(couple_id, active_user_id, notify_at)
VALUES(NEW.couple_id, NEW.user_id, now() + interval '5 minutes')
ON CONFLICT(couple_id) DO UPDATE SET
active_user_id=excluded.active_user_id,
response_count=pending_match_notifications.response_count+1,
notify_at=excluded.notify_at,
updated_at=now();
RETURN NEW;
END $$;
--> statement-breakpoint
DROP TRIGGER IF EXISTS operations_response_digest ON responses;
CREATE TRIGGER operations_response_digest AFTER INSERT ON responses
FOR EACH ROW EXECUTE FUNCTION queue_response_digest();
--> statement-breakpoint
CREATE OR REPLACE FUNCTION count_digest_match() RETURNS trigger LANGUAGE plpgsql AS $$
BEGIN
IF current_setting('sauci.suppress_operations',true)='on' THEN RETURN NEW; END IF;
UPDATE pending_match_notifications SET match_count=match_count+1,
latest_match_id=NEW.id, updated_at=now() WHERE couple_id=NEW.couple_id;
RETURN NEW;
END $$;
--> statement-breakpoint
DROP TRIGGER IF EXISTS operations_match_digest ON matches;
CREATE TRIGGER operations_match_digest AFTER INSERT ON matches
FOR EACH ROW EXECUTE FUNCTION count_digest_match();
--> statement-breakpoint
CREATE OR REPLACE FUNCTION queue_message_operations() RETURNS trigger LANGUAGE plpgsql AS $$
DECLARE v_couple_id uuid; v_recipient record;
BEGIN
IF current_setting('sauci.suppress_operations',true)='on' THEN RETURN NEW; END IF;
SELECT couple_id INTO v_couple_id FROM matches WHERE id=NEW.match_id;
FOR v_recipient IN
SELECT p.id FROM profiles p
LEFT JOIN notification_preferences np ON np.user_id=p.id
WHERE p.couple_id=v_couple_id AND p.id<>NEW.user_id
AND COALESCE(np.messages_enabled,true)=true
LOOP
INSERT INTO operations_outbox(kind,dedupe_key,recipient_id,payload)
VALUES('expo','message:'||NEW.id||':'||v_recipient.id,v_recipient.id,
jsonb_build_object('title','New message','body','Your partner sent you a message',
'data',jsonb_build_object('type','message','match_id',NEW.match_id,'message_id',NEW.id)))
ON CONFLICT(dedupe_key) DO NOTHING;
END LOOP;
INSERT INTO operations_outbox(kind,dedupe_key,payload)
VALUES('classify','classify:'||NEW.id,jsonb_build_object('message_id',NEW.id))
ON CONFLICT(dedupe_key) DO NOTHING;
RETURN NEW;
END $$;
--> statement-breakpoint
DROP TRIGGER IF EXISTS operations_message_created ON messages;
CREATE TRIGGER operations_message_created AFTER INSERT ON messages
FOR EACH ROW EXECUTE FUNCTION queue_message_operations();
--> statement-breakpoint
CREATE OR REPLACE FUNCTION queue_pack_change(p_couple_id uuid, p_user_id uuid) RETURNS void LANGUAGE plpgsql AS $$
BEGIN
IF NOT EXISTS(SELECT 1 FROM profiles WHERE id=p_user_id AND couple_id=p_couple_id) THEN
RAISE EXCEPTION 'user is not a member of couple';
END IF;
INSERT INTO pending_pack_notifications(couple_id,changed_by_user_id,notify_at)
VALUES(p_couple_id,p_user_id,now()+interval '30 minutes')
ON CONFLICT(couple_id) DO UPDATE SET changed_by_user_id=excluded.changed_by_user_id,
notify_at=excluded.notify_at,updated_at=now();
END $$;
--> statement-breakpoint
CREATE OR REPLACE FUNCTION queue_discord_operations() RETURNS trigger LANGUAGE plpgsql AS $$
BEGIN
IF current_setting('sauci.suppress_operations',true)='on' THEN RETURN NEW; END IF;
IF TG_TABLE_NAME='profiles' AND NEW.onboarding_completed=true
AND (TG_OP='INSERT' OR OLD.onboarding_completed=false) THEN
INSERT INTO operations_outbox(kind,dedupe_key,payload)
VALUES('discord','discord:new_user:'||NEW.id,
jsonb_build_object('event','new_user','user_id',NEW.id,'name',NEW.name,'email',NEW.email,'created_at',NEW.created_at))
ON CONFLICT(dedupe_key) DO NOTHING;
END IF;
IF TG_TABLE_NAME='profiles' AND NEW.couple_id IS NOT NULL
AND (TG_OP='INSERT' OR OLD.couple_id IS DISTINCT FROM NEW.couple_id)
AND EXISTS(SELECT 1 FROM profiles partner WHERE partner.couple_id=NEW.couple_id AND partner.id<>NEW.id) THEN
INSERT INTO operations_outbox(kind,dedupe_key,payload)
VALUES('discord','discord:couple_paired:'||NEW.couple_id,
jsonb_build_object('event','couple_paired','couple_id',NEW.couple_id,'paired_at',now()))
ON CONFLICT(dedupe_key) DO NOTHING;
END IF;
RETURN NEW;
END $$;
--> statement-breakpoint
DROP TRIGGER IF EXISTS operations_profile_discord ON profiles;
CREATE TRIGGER operations_profile_discord AFTER INSERT OR UPDATE OF onboarding_completed,couple_id ON profiles
FOR EACH ROW EXECUTE FUNCTION queue_discord_operations();
--> statement-breakpoint
CREATE OR REPLACE FUNCTION queue_feedback_discord() RETURNS trigger LANGUAGE plpgsql AS $$
BEGIN
IF current_setting('sauci.suppress_operations',true)='on' THEN RETURN NEW; END IF;
INSERT INTO operations_outbox(kind,dedupe_key,payload)
VALUES('discord','discord:feedback:'||NEW.id,
jsonb_build_object('event','feedback_submitted','feedback_id',NEW.id,'user_id',NEW.user_id,
'type',NEW.type,'status',NEW.status,'title',NEW.title,'description',NEW.description,
'question_id',NEW.question_id,'created_at',NEW.created_at))
ON CONFLICT(dedupe_key) DO NOTHING;
RETURN NEW;
END $$;
--> statement-breakpoint
DROP TRIGGER IF EXISTS operations_feedback_discord ON feedback;
CREATE TRIGGER operations_feedback_discord AFTER INSERT ON feedback
FOR EACH ROW EXECUTE FUNCTION queue_feedback_discord();