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

+13 -60
View File
@@ -17,7 +17,7 @@ import { LinearGradient } from "expo-linear-gradient";
import Animated, { FadeInDown } from "react-native-reanimated";
import * as Clipboard from "expo-clipboard";
import { useAuthStore } from "../../src/store";
import { supabase } from "../../src/lib/supabase";
import { coupleApi } from "../../src/lib/coupleApi";
import { getPairingError } from "../../src/lib/errors";
import { Events } from "../../src/lib/analytics";
import { router } from "expo-router";
@@ -25,7 +25,7 @@ import { GradientBackground, GlassCard, GlassButton } from "../../src/components
import { colors, gradients, spacing, radius, typography, shadows } from "../../src/theme";
export default function PairingScreen() {
const { user, fetchCouple, fetchUser, couple, partner, isLoading: isAuthLoading } = useAuthStore();
const { fetchCouple, fetchUser, couple, partner, isLoading: isAuthLoading } = useAuthStore();
const [inviteCode, setInviteCode] = useState("");
const [isSubmitting, setIsSubmitting] = useState(false);
@@ -36,51 +36,25 @@ export default function PairingScreen() {
}
}, [couple, partner]);
// Subscribe to real-time updates for partner joining + polling fallback
useEffect(() => {
if (!couple || partner) return;
// Poll the standalone API while waiting for a partner. Realtime delivery can
// be added later without coupling product data back to Supabase.
useEffect(() => {
if (!couple || partner) return;
// Poll every 5 seconds as fallback
const pollInterval = setInterval(() => {
fetchCouple();
}, 5000);
// Listen for new profiles joining this couple
const subscription = supabase
.channel(`couple-${couple.id}`)
.on(
"postgres_changes",
{
event: "UPDATE",
schema: "public",
table: "profiles",
filter: `couple_id=eq.${couple.id}`,
},
async (payload) => {
// Someone updated their profile to join this couple
if (payload.new.id !== user?.id) {
await fetchCouple();
}
}
)
.subscribe();
return () => {
clearInterval(pollInterval);
subscription.unsubscribe();
};
}, [couple, partner, user?.id, fetchCouple]);
return () => {
clearInterval(pollInterval);
};
}, [couple, partner, fetchCouple]);
const handleCreateCouple = async () => {
setIsSubmitting(true);
try {
const { data, error } = await supabase.functions.invoke("manage-couple", {
method: "POST",
body: {},
});
if (error) throw error;
if (data.error) throw new Error(data.error);
await coupleApi.create();
await fetchUser(); // Refresh user to get couple_id
await fetchCouple(); // Fetch couple data
@@ -102,23 +76,7 @@ export default function PairingScreen() {
setIsSubmitting(true);
try {
// Refresh session to ensure we have a valid token
const { data: { session }, error: sessionError } = await supabase.auth.getSession();
if (sessionError || !session) {
throw new Error("Please log in again");
}
const { data, error } = await supabase.functions.invoke("manage-couple", {
method: "POST",
body: { invite_code: sanitizedCode },
});
if (error) {
// Extract user-friendly error message
const errorMessage = data?.error || "Unable to join couple. Please try again.";
throw new Error(errorMessage);
}
if (data?.error) throw new Error(data.error);
await coupleApi.join(sanitizedCode);
await fetchUser(); // Refresh user to get couple_id
await fetchCouple(); // Fetch couple data
@@ -186,12 +144,7 @@ export default function PairingScreen() {
onPress: async () => {
setIsSubmitting(true);
try {
const { data, error } = await supabase.functions.invoke("manage-couple", {
method: "DELETE",
});
if (error) throw error;
if (data?.error) throw new Error(data.error);
await coupleApi.cancel();
await fetchUser();
await fetchCouple();