Skip to main content

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 by pg_cron/pg_net with a shared secret.
  • PDF generation and all third-party integrations → src/server/services/ and src/server/integrations/, imported server-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-only modules and the src/server/env.ts loader; nothing sensitive reaches the browser.
  • Hosting must provide a Node runtime with enough memory/time for Puppeteer (raised maxDuration on 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/maxDuration for 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:
    1. swap the single node:crypto createHash in src/server/actions/checks.ts to Web Crypto (crypto.subtle.digest);
    2. enable nodejs_compat for Buffer usage (PDF + file uploads);
    3. verify @react-pdf/renderer fits the Worker bundle-size limit, or move PDF rendering to a separate Node service / Supabase Edge Function if it doesn't.
  • The remaining edge concern is therefore @react-pdf bundle size, not a hard Node-only blocker.