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,96 @@
|
||||
import { z } from 'zod';
|
||||
|
||||
const baseSchema = z.object({
|
||||
NODE_ENV: z.enum(['development', 'test', 'production']).default('development'),
|
||||
PORT: z.coerce.number().int().min(1).max(65535).default(3003),
|
||||
DATABASE_URL: z.string().url(),
|
||||
SUPABASE_AUTH_URL: z.string().url().optional(),
|
||||
SUPABASE_AUTH_ISSUER: z.string().url().optional(),
|
||||
SUPABASE_AUTH_JWKS_URL: z.string().url().optional(),
|
||||
SUPABASE_AUTH_AUDIENCE: z.string().min(1).default('authenticated'),
|
||||
AUTH_TEST_JWKS: z.string().optional(),
|
||||
MEDIA_ROOT: z.string().min(1).default('/data/media'),
|
||||
MEDIA_SIGNING_SECRET: z.string().min(32).optional(),
|
||||
MEDIA_PUBLIC_BASE_URL: z.string().url().optional(),
|
||||
SUPABASE_AUTH_SERVICE_ROLE_KEY: z.string().min(1).optional(),
|
||||
REVENUECAT_API_KEY: z.string().min(1).optional(),
|
||||
REVENUECAT_ENTITLEMENT_ID: z.string().min(1).default('Sauci Pro'),
|
||||
REVENUECAT_WEBHOOK_SECRET: z.string().min(1).optional(),
|
||||
ADMIN_API_SERVICE_TOKEN: z.string().min(32).optional(),
|
||||
ADMIN_API_SERVICE_USER_ID: z.string().uuid().optional(),
|
||||
ADMIN_PRIVATE_KEY_JWK: z.string().optional(),
|
||||
});
|
||||
|
||||
export interface AppConfig {
|
||||
nodeEnv: 'development' | 'test' | 'production';
|
||||
port: number;
|
||||
databaseUrl: string;
|
||||
authIssuer: string;
|
||||
authJwksUrl?: string;
|
||||
authAudience: string;
|
||||
authTestJwks?: string;
|
||||
mediaRoot: string;
|
||||
mediaSigningSecret: string;
|
||||
mediaPublicBaseUrl: string;
|
||||
supabaseAuthServiceRoleKey?: string;
|
||||
revenueCatApiKey?: string;
|
||||
revenueCatEntitlementId: string;
|
||||
revenueCatWebhookSecret?: string;
|
||||
adminApiServiceToken?: string;
|
||||
adminApiServiceUserId?: string;
|
||||
adminPrivateKeyJwk?: string;
|
||||
}
|
||||
|
||||
export function loadConfig(env: NodeJS.ProcessEnv = process.env): AppConfig {
|
||||
const parsed = baseSchema.parse(env);
|
||||
if (Boolean(parsed.ADMIN_API_SERVICE_TOKEN) !== Boolean(parsed.ADMIN_API_SERVICE_USER_ID)) {
|
||||
throw new Error('ADMIN_API_SERVICE_TOKEN and ADMIN_API_SERVICE_USER_ID must be configured together');
|
||||
}
|
||||
if (parsed.ADMIN_PRIVATE_KEY_JWK) {
|
||||
try { JSON.parse(parsed.ADMIN_PRIVATE_KEY_JWK); } catch { throw new Error('ADMIN_PRIVATE_KEY_JWK must be valid JSON'); }
|
||||
}
|
||||
if (parsed.AUTH_TEST_JWKS && parsed.NODE_ENV !== 'test') {
|
||||
throw new Error('AUTH_TEST_JWKS is forbidden outside NODE_ENV=test');
|
||||
}
|
||||
|
||||
const authUrl = parsed.SUPABASE_AUTH_URL?.replace(/\/$/, '');
|
||||
const authIssuer = parsed.SUPABASE_AUTH_ISSUER ?? (authUrl ? `${authUrl}/auth/v1` : undefined);
|
||||
const authJwksUrl = parsed.SUPABASE_AUTH_JWKS_URL ??
|
||||
(authUrl ? `${authUrl}/auth/v1/.well-known/jwks.json` : undefined);
|
||||
|
||||
if (!authIssuer || (!authJwksUrl && !parsed.AUTH_TEST_JWKS)) {
|
||||
throw new Error('Supabase Auth issuer and JWKS configuration are required');
|
||||
}
|
||||
if (parsed.NODE_ENV === 'production' &&
|
||||
(new URL(authIssuer).protocol !== 'https:' ||
|
||||
(authJwksUrl && new URL(authJwksUrl).protocol !== 'https:'))) {
|
||||
throw new Error('Supabase Auth issuer and JWKS URLs must use HTTPS in production');
|
||||
}
|
||||
if (parsed.NODE_ENV === 'production' && !parsed.MEDIA_SIGNING_SECRET) {
|
||||
throw new Error('MEDIA_SIGNING_SECRET is required in production');
|
||||
}
|
||||
const mediaPublicBaseUrl = parsed.MEDIA_PUBLIC_BASE_URL ?? `http://127.0.0.1:${parsed.PORT}`;
|
||||
if (parsed.NODE_ENV === 'production' && new URL(mediaPublicBaseUrl).protocol !== 'https:') {
|
||||
throw new Error('MEDIA_PUBLIC_BASE_URL must use HTTPS in production');
|
||||
}
|
||||
|
||||
return {
|
||||
nodeEnv: parsed.NODE_ENV,
|
||||
port: parsed.PORT,
|
||||
databaseUrl: parsed.DATABASE_URL,
|
||||
authIssuer,
|
||||
authJwksUrl,
|
||||
authAudience: parsed.SUPABASE_AUTH_AUDIENCE,
|
||||
authTestJwks: parsed.AUTH_TEST_JWKS,
|
||||
mediaRoot: parsed.MEDIA_ROOT,
|
||||
mediaSigningSecret: parsed.MEDIA_SIGNING_SECRET ?? 'local-media-signing-secret-change-me-123456',
|
||||
mediaPublicBaseUrl: mediaPublicBaseUrl.replace(/\/$/, ''),
|
||||
supabaseAuthServiceRoleKey: parsed.SUPABASE_AUTH_SERVICE_ROLE_KEY,
|
||||
revenueCatApiKey: parsed.REVENUECAT_API_KEY,
|
||||
revenueCatEntitlementId: parsed.REVENUECAT_ENTITLEMENT_ID,
|
||||
revenueCatWebhookSecret: parsed.REVENUECAT_WEBHOOK_SECRET,
|
||||
adminApiServiceToken: parsed.ADMIN_API_SERVICE_TOKEN,
|
||||
adminApiServiceUserId: parsed.ADMIN_API_SERVICE_USER_ID,
|
||||
adminPrivateKeyJwk: parsed.ADMIN_PRIVATE_KEY_JWK,
|
||||
};
|
||||
}
|
||||
Reference in new issue
Block a user