mirror of
https://github.com/jjbrunton/Sauci.git
synced 2026-10-31 18:19:43 +00:00
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:
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();
|
||||
Reference in new issue
Block a user