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

+7 -20
View File
@@ -1,8 +1,9 @@
'use client'
import { useState } from 'react'
import Link from 'next/link'
import { ArrowLeft, Gift, Mail, Key, CheckCircle, AlertCircle, Loader2 } from 'lucide-react'
import Link from 'next/link'
import { ArrowLeft, Gift, Mail, Key, CheckCircle, AlertCircle, Loader2 } from 'lucide-react'
import { redeemCode } from './redemptionApi'
export default function RedeemPage() {
const [email, setEmail] = useState('')
@@ -32,24 +33,10 @@ export default function RedeemPage() {
return
}
setIsLoading(true)
try {
const response = await fetch(
`${process.env.NEXT_PUBLIC_SUPABASE_URL}/functions/v1/redeem-code`,
{
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
email: email.trim(),
code: code.trim(),
}),
}
)
const data = await response.json()
setIsLoading(true)
try {
const data = await redeemCode(email.trim(), code.trim())
if (data.success) {
setIsRedeemed(true)
+34
View File
@@ -0,0 +1,34 @@
import { afterEach, describe, expect, it, vi } from 'vitest'
import { redeemCode } from './redemptionApi'
describe('redeemCode', () => {
afterEach(() => {
vi.unstubAllEnvs()
vi.unstubAllGlobals()
})
it('posts only the public redemption fields to the standalone API', async () => {
vi.stubEnv('NEXT_PUBLIC_API_URL', 'http://127.0.0.1:3003/')
const request = vi.fn(async () => Response.json({ success: true, message: 'redeemed' }))
vi.stubGlobal('fetch', request)
await expect(redeemCode('alice@example.test', 'PROMO')).resolves.toEqual({
success: true,
message: 'redeemed',
})
expect(request).toHaveBeenCalledWith('http://127.0.0.1:3003/public/v1/redemptions', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ email: 'alice@example.test', code: 'PROMO' }),
})
})
it('fails closed instead of falling back to hosted Supabase', async () => {
vi.stubEnv('NEXT_PUBLIC_API_URL', '')
const request = vi.fn()
vi.stubGlobal('fetch', request)
await expect(redeemCode('alice@example.test', 'PROMO')).rejects.toThrow('NEXT_PUBLIC_API_URL')
expect(request).not.toHaveBeenCalled()
})
})
+16
View File
@@ -0,0 +1,16 @@
export type RedemptionResponse =
| { success: true; message: string }
| { success: false; error: string }
export async function redeemCode(email: string, code: string): Promise<RedemptionResponse> {
const apiUrl = process.env.NEXT_PUBLIC_API_URL?.replace(/\/$/, '')
if (!apiUrl) throw new Error('NEXT_PUBLIC_API_URL is not configured')
const response = await fetch(`${apiUrl}/public/v1/redemptions`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ email, code }),
})
return response.json() as Promise<RedemptionResponse>
}