3. Backend logic runs in the Next.js Node runtime, not Supabase Edge Functions
Date: 2026-06-06
Status
Accepted — amended 2026-06-08 (the Puppeteer premise is now stale; see "Update" below).
Context
The source spec assumed a React/Vite SPA with the backend in Supabase Edge Functions (Deno). But this app needs Puppeteer for pixel-accurate invoice/payslip PDFs, plus SDK/HTTP integrations (Wise, Gmail, Hubstaff, DocuSeal, Anthropic). Puppeteer cannot run on Deno/Edge Functions, and the repo is already a Next.js (App Router) project per the project conventions. There were three competing stack visions; this records the resolution.
Decision
All server logic lives in the Next.js Node runtime:
- Interactive mutations → server actions (
src/server/actions/), Zod-validated and role-gated. - Inbound webhooks (Hubstaff, Wise, DocuSeal) → route handlers (
src/app/api/webhooks/*) that need the raw body for HMAC verification. - Scheduled jobs → route handlers (
src/app/api/cron/*) invoked bypg_cron/pg_netwith a shared secret. - PDF generation and all third-party integrations →
src/server/services/andsrc/server/integrations/, importedserver-only.
Supabase provides Postgres + RLS + Auth + Storage only. Supabase Edge Functions are not used. The single-file React CDN approach from the global preferences is also rejected for this app (it can't safely hold server secrets across six integrations, generate PDFs, run cron, or carry a money-math test suite).
Consequences
- One language/runtime (TypeScript on Node) for UI and backend; integration SDKs and Puppeteer run natively.
- Secrets stay server-side via
server-onlymodules and thesrc/server/env.tsloader; nothing sensitive reaches the browser. - Hosting must provide a Node runtime with enough memory/time for Puppeteer (raised
maxDurationon PDF/cron handlers; a hosted browser service is the fallback — see the PDF ADR when written).
Update — 2026-06-08
The Puppeteer premise is no longer accurate. Invoice/payslip PDFs are now rendered with
@react-pdf/renderer — a pure-JS renderer, no headless browser (see
src/server/services/invoice/pdf.tsx, which states "Node runtime; no headless browser"). No
Puppeteer/Chromium dependency remains in the project.
This corrects the rationale, not the decision — the Next.js/Node placement still holds, but for the other reasons (full-stack co-location, server-only secrets across six integrations, server actions + route handlers), not because a headless browser forces a Node runtime.
Consequences of the correction:
- The original "Consequences" line about provisioning a Node runtime with memory/
maxDurationfor Puppeteer (and a hosted-browser fallback) no longer applies — there is no browser to run. - Hosting options broaden. Node hosts (Vercel, Railway, Render) run the app unchanged. A
Cloudflare Workers / edge target is now feasible via the OpenNext adapter, with three caveats:
- swap the single
node:cryptocreateHashinsrc/server/actions/checks.tsto Web Crypto (crypto.subtle.digest); - enable
nodejs_compatforBufferusage (PDF + file uploads); - verify
@react-pdf/rendererfits the Worker bundle-size limit, or move PDF rendering to a separate Node service / Supabase Edge Function if it doesn't.
- swap the single
- The remaining edge concern is therefore
@react-pdfbundle size, not a hard Node-only blocker.