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

@@ -15,9 +15,8 @@ import {
import { Ionicons } from '@expo/vector-icons';
import { LinearGradient } from 'expo-linear-gradient';
import * as ImagePicker from 'expo-image-picker';
import * as FileSystem from 'expo-file-system';
import { decode } from 'base64-arraybuffer';
import { supabase } from '../../lib/supabase';
import { uploadMedia } from '../../lib/mediaApi';
import { profileSettingsApi } from '../../lib/profileSettingsApi';
import { getDeviceInfo } from '../../lib/deviceInfo';
import { useAuthStore } from '../../store';
import { colors, gradients, spacing, radius, typography } from '../../theme';
@@ -72,44 +71,20 @@ export function FeedbackModal({ visible, onClose }: FeedbackModalProps) {
}
};
const uploadScreenshot = async (uri: string, userId: string): Promise<string> => {
let fileBody;
let ext = 'jpg';
const uploadScreenshot = async (uri: string, _userId: string): Promise<string> => {
let ext = 'jpg';
if (Platform.OS === 'web') {
const response = await fetch(uri);
const blob = await response.blob();
fileBody = blob;
if (blob.type === 'image/png') ext = 'png';
else if (blob.type === 'image/jpeg' || blob.type === 'image/jpg') ext = 'jpg';
else if (blob.type === 'image/gif') ext = 'gif';
else if (blob.type === 'image/webp') ext = 'webp';
} else {
const base64 = await FileSystem.readAsStringAsync(uri, {
encoding: FileSystem.EncodingType.Base64,
});
fileBody = decode(base64);
ext = uri.split('.').pop()?.toLowerCase() || 'jpg';
}
const fileName = `${userId}/${Date.now()}_${Math.random().toString(36).substring(7)}.${ext}`;
const { error } = await supabase.storage
.from('feedback-screenshots')
.upload(fileName, fileBody, {
contentType: `image/${ext === 'jpg' ? 'jpeg' : ext}`,
upsert: false,
});
if (error) throw error;
// Get signed URL (private bucket)
const { data } = await supabase.storage
.from('feedback-screenshots')
.createSignedUrl(fileName, 60 * 60 * 24 * 365); // 1 year expiry
return data?.signedUrl ?? '';
ext = uri.split('.').pop()?.toLowerCase() || 'jpg';
}
return (await uploadMedia(uri, { kind: 'feedback', mimeType: `image/${ext === 'jpg' ? 'jpeg' : ext}` })).media.id;
};
const handleSubmit = async () => {
@@ -132,24 +107,19 @@ export function FeedbackModal({ visible, onClose }: FeedbackModalProps) {
try {
const deviceInfo = getDeviceInfo();
let screenshotUrl: string | null = null;
let screenshotMediaId: string | null = null;
// Upload screenshot if provided
if (screenshotUri) {
screenshotUrl = await uploadScreenshot(screenshotUri, user.id);
}
// Insert feedback
const { error } = await supabase.from('feedback').insert({
user_id: user.id,
type: feedbackType,
title: title.trim(),
description: description.trim(),
screenshot_url: screenshotUrl,
device_info: deviceInfo,
});
if (error) throw error;
screenshotMediaId = await uploadScreenshot(screenshotUri, user.id);
}
await profileSettingsApi.submitFeedback({
type: feedbackType,
title: title.trim(),
description: description.trim(),
device_info: deviceInfo,
...(screenshotMediaId ? { screenshot_media_id: screenshotMediaId } : {}),
});
Alert.alert('Thank you!', 'Your feedback has been submitted successfully.', [
{ text: 'OK', onPress: handleClose },