# What You'll Build#
This guide gives you a practical, opinionated Next.js Supabase Edge Functions architecture for modern SaaS: what code lives where, how data flows, and how to deploy without tripping over runtime limits.
You will learn when to use Supabase Edge Functions vs Next.js API routes vs Server Actions, with concrete examples for auth hooks, webhooks, and background tasks. If you are still deciding which auth pattern to use, start with our Next.js authentication guide and come back here for the architecture and deployment blueprint.
# Why This Architecture Works for SaaS#
SaaS apps typically have three classes of traffic:
- 1App traffic from logged-in users, often requiring sessions, RLS-protected queries, and UI-driven workflows.
- 2Provider traffic from third parties like Stripe, Postmark, Sentry, GitHub, or custom enterprise systems via webhooks.
- 3Automation traffic from scheduled tasks, retries, and back-office workflows.
A clean split reduces complexity and production incidents. In practice:
- Next.js excels at UI, Server Actions, and session-aware requests.
- Supabase excels at Postgres, RLS, realtime, storage, and globally accessible Edge Functions.
- Supabase Edge Functions shine for external-facing endpoints that must not depend on your Next.js app deployment, and for flows that benefit from being close to the database.
🎯 Key Takeaway: Put UI-coupled logic in Next.js, provider-facing endpoints in Supabase Edge Functions, and enforce data access with Postgres RLS as the default guardrail.
# Decision Framework: Edge Functions vs API Routes vs Server Actions#
The fastest way to break this architecture is to treat all server code as interchangeable. They are not interchangeable, especially once you factor in runtime constraints, session models, and deployment topology.
Quick Comparison Table#
| Use case | Best default | Why | Concrete example |
|---|---|---|---|
| UI form submission that writes to DB | Server Actions | Co-located with UI, easy validation, uses user session | Create project, invite member |
| UI needs a custom JSON endpoint | API route or Route Handler | Works with fetch, caching, middleware, can stream | Search endpoint, file proxy |
| Third-party webhooks | Supabase Edge Function | Stable URL, independent deploys, can use service role safely | Stripe invoice.paid webhook |
| Auth provider hooks | Supabase Edge Function | Runs close to Supabase auth, minimal latency and coupling | Post-signup provisioning |
| Lightweight public API near users | Supabase Edge Function | Global distribution, Deno runtime, low overhead | Public status endpoint |
| Long-running background jobs | Dedicated worker or automation tool | Timeouts and retries require queue semantics | Billing reconciliation |
| Scheduled maintenance | Edge Function with scheduler or automation | Simple cron-like triggers | Daily usage rollup |
For runtime trade-offs and where Edge is truly beneficial, read our deep dive: Next.js Edge runtime vs Node.js runtime on Vercel and Cloudflare.
Use Supabase Edge Functions When#
- 1
The caller is not your Next.js frontend
Webhooks and provider callbacks should not depend on your Next.js deployment lifecycle. - 2
You need the Supabase service role key
Edge Functions are a safe place for service role operations like privileged writes, cross-tenant administrative tasks, and bypassing RLS when needed. - 3
You want a stable, globally reachable endpoint
Many SaaS teams deploy Next.js to Vercel and still need provider endpoints that are independent of Vercel build rollouts.
Prefer Next.js Server Actions When#
- 1The request originates from your UI and is naturally part of the route tree.
- 2You need tight coupling with React component state and server-side validation.
- 3RLS is enough and you do not need service role privileges.
Prefer Next.js API Routes or Route Handlers When#
- 1You need a traditional endpoint consumed by multiple clients, including mobile apps.
- 2You need streaming, file proxying, or special headers.
- 3You are integrating with Node-only libraries that are awkward in Edge runtimes.
⚠️ Warning: Do not build your entire backend behind Supabase Edge Functions just because they exist. If a request is UI-driven and RLS-protected, Next.js Server Actions are simpler, cheaper to maintain, and easier to test end-to-end.
# Reference Architecture Overview#
At a high level, your SaaS should look like this:
- Next.js app handles rendering, session-aware actions, and UI workflows.
- Supabase Postgres is the source of truth with RLS policies that enforce tenant boundaries.
- Supabase Edge Functions implement:
- Webhooks from providers
- Auth lifecycle hooks
- Scheduled rollups and cleanup jobs
- Optional: n8n automations for workflow orchestration, retries, notifications, and integration glue.
If you want a broader starter blueprint, compare with our complementary post: Next.js + Supabase SaaS starter architecture.
# Opinionated Folder and Module Structure#
The goal is to make ownership obvious: UI code in Next.js, privileged code in Edge Functions, shared logic in packages that compile cleanly for both runtimes.
Monorepo Layout#
| Path | Purpose | Runtime constraints |
|---|---|---|
apps/web | Next.js app | Node.js or Edge depending on route |
supabase/functions | Supabase Edge Functions | Deno runtime |
packages/shared | Pure utilities and types | Must be runtime-agnostic |
packages/db | SQL migrations, typed queries | No runtime, build-time tooling |
packages/security | Signature verification, hashing helpers | Must avoid Node-only APIs if shared |
Next.js App Structure#
Use the App Router and keep all server-only modules in a clear boundary.
| Path | Responsibility | Notes |
|---|---|---|
apps/web/app | Routes and layouts | Keep route-level server actions near pages |
apps/web/app/(app)/actions | UI-driven Server Actions | Only call DB directly, or call Edge Functions when needed |
apps/web/app/api | Route Handlers for client fetch | Prefer for mobile clients and JSON endpoints |
apps/web/lib/supabase | Browser and server clients | Split into server-only vs browser-safe modules |
apps/web/lib/auth | Session helpers | Depends on your auth approach |
apps/web/modules/* | Domain modules | Each module owns UI, actions, and queries |
Domain Module Pattern#
Make every domain module include at least:
| File | Purpose |
|---|---|
modules/billing/service.ts | Pure domain logic, no framework imports |
modules/billing/actions.ts | Server Actions for UI workflows |
modules/billing/queries.ts | Data access methods (RLS by default) |
modules/billing/validators.ts | Zod or Valibot schemas |
modules/billing/events.ts | Event names and payload types |
This scales better than dumping everything into lib/ and guessing ownership later.
💡 Tip: If a function needs the Supabase service role key, it should not live in
apps/web. Put it in an Edge Function and call it via a signed request.
Supabase Edge Functions Structure#
Supabase Edge Functions run on Deno. Avoid Node-specific dependencies, and keep each function small and single-purpose.
| Path | Purpose |
|---|---|
supabase/functions/_shared/env.ts | Env parsing and assertions |
supabase/functions/_shared/supabase.ts | Create service role client |
supabase/functions/_shared/verify.ts | Signature verification utilities |
supabase/functions/stripe-webhook/index.ts | Stripe webhook handler |
supabase/functions/auth-post-signup/index.ts | Auth hook provisioning |
supabase/functions/daily-rollup/index.ts | Scheduled aggregation |
# Concrete Examples: What Goes Where#
This section maps common SaaS requirements to the correct execution environment.
Example 1: Post-signup Provisioning with Auth Hooks#
Problem: On signup, you need to create a tenant workspace, seed default data, and attach the user as owner.
Best place: Supabase Edge Function as an auth hook.
Why it matters: provisioning needs privileged writes and must be consistent. If you rely on the client to create the workspace, you will see partial state when users close tabs or mobile networks drop.
What the Edge Function does:
- Validates the auth event payload
- Creates tenant workspace row
- Inserts default settings
- Adds membership row
// supabase/functions/auth-post-signup/index.ts
import { createClient } from "https://esm.sh/@supabase/supabase-js@2";
Deno.serve(async (req) => {
const payload = await req.json();
const userId = payload?.user?.id;
if (!userId) return new Response("Missing user id", { status: 400 });
const supabase = createClient(
Deno.env.get("SUPABASE_URL") ?? "",
Deno.env.get("SUPABASE_SERVICE_ROLE_KEY") ?? ""
);
const { data: workspace, error } = await supabase
.from("workspaces")
.insert({ owner_id: userId, name: "My Workspace" })
.select()
.single();
if (error) return new Response(error.message, { status: 500 });
await supabase.from("memberships").insert({
user_id: userId,
workspace_id: workspace.id,
role: "owner",
});
return new Response("ok", { status: 200 });
});When not to do this in Next.js: If signup happens through Supabase Auth and not through your Next.js code path, you cannot reliably guarantee the Next.js hook will execute.
Example 2: Stripe Webhooks#
Problem: Stripe sends events like invoice.paid and customer.subscription.updated. You must verify signatures and update billing state.
Best place: Supabase Edge Function.
Why it matters: you want a stable endpoint independent of Next.js deployments, and you want to update billing tables with privileged access. You also want to respond within provider timeouts, typically measured in seconds, not minutes.
Stripe signature verification relies on raw request body handling. In Edge Functions, you control it consistently without worrying about framework middleware altering the payload.
// supabase/functions/stripe-webhook/index.ts
import Stripe from "https://esm.sh/stripe@15?target=deno";
Deno.serve(async (req) => {
const sig = req.headers.get("stripe-signature");
const secret = Deno.env.get("STRIPE_WEBHOOK_SECRET") ?? "";
if (!sig) return new Response("Missing signature", { status: 400 });
const raw = await req.text();
const stripe = new Stripe(Deno.env.get("STRIPE_SECRET_KEY") ?? "", {
apiVersion: "2024-06-20",
});
let event;
try {
event = stripe.webhooks.constructEvent(raw, sig, secret);
} catch (_e) {
return new Response("Invalid signature", { status: 400 });
}
// Update your DB using the service role client here.
return new Response("ok", { status: 200 });
});Example 3: UI-driven Billing Portal Link#
Problem: Logged-in users click "Manage billing" to get a Stripe portal URL.
Best place: Next.js Server Action, optionally calling an Edge Function.
Why it matters: this is a UI-driven request, bound to a session. If you already have server access to Stripe in Next.js Node runtime, keep it close to the UI.
Minimal Server Action pattern:
// apps/web/app/(app)/settings/billing/actions.ts
"use server";
import { redirect } from "next/navigation";
export async function createBillingPortalAction() {
// 1) Get the user and workspace from session
// 2) Create Stripe portal session
// 3) Redirect
redirect("/settings/billing");
}If you want all Stripe credentials isolated from Next.js, call an Edge Function from the Server Action. Sign the request using a shared secret and validate it in the function.
ℹ️ Note: A good default is: Stripe webhooks in Edge Functions, UI billing actions in Next.js. Move UI billing actions to Edge only if you want a single blast radius for Stripe secrets.
Example 4: Background Tasks and Scheduled Rollups#
Problem: You need a daily usage rollup per workspace, plus cleanup of expired invites.
Best place: Edge Function with a scheduler for simple jobs, or automation tooling for workflows requiring retries, branching, and notifications.
A typical rollup does:
- Aggregate yesterday’s events into a
usage_dailytable - Mark duplicates with unique constraints
- Keep the function idempotent
If your job needs:
- retry policies per step
- calling multiple providers
- sending Slack alerts
- human-in-the-loop approvals
then a workflow tool is a better fit than stuffing everything into one function.
# Data Access: RLS First, Service Role Only When Needed#
In a SaaS, RLS is not optional. It is your primary defense against tenant data leaks.
Practical rules:
- UI reads and writes should happen with the user session and RLS.
- Provider webhooks often require privileged writes because there is no user session.
- Cross-tenant administration belongs in Edge Functions using the service role.
A Simple Tenant Pattern#
Most B2B SaaS apps use tables like:
workspacesmembershipswithrole- domain tables with
workspace_id
Then RLS ensures users can only access rows for workspaces they belong to.
If you want more patterns and pitfalls, our starter guide covers it in detail: Next.js + Supabase SaaS starter architecture.
# How Next.js Talks to Supabase Edge Functions Safely#
Do not treat Edge Functions as internal-only just because they are yours. Assume any public URL will be hit by scanners and bots.
Recommended patterns:
Pattern A: Provider-signed requests#
For webhooks, the provider signs the payload. You verify it, then proceed. This is the best-case scenario.
Pattern B: Shared-secret HMAC for internal calls#
When Next.js calls an Edge Function, include:
- timestamp header
- signature header
- body
Then validate in the function and reject old timestamps to prevent replay attacks.
// apps/web/modules/security/sign.ts
import crypto from "crypto";
export function signPayload(secret: string, payload: string) {
return crypto.createHmac("sha256", secret).update(payload).digest("hex");
}Keep the shared secret server-only. Never expose it in client code.
⚠️ Warning: Never call an Edge Function that uses the service role from the browser directly, even if it feels convenient. Proxy through a Server Action or an API route and add signature validation.
# Deployment Considerations: Vercel vs Cloudflare#
Deployment choice changes runtime defaults and constraints. The most common production setup is:
- Next.js on Vercel
- Supabase Edge Functions managed by Supabase
But some teams deploy Next.js on Cloudflare Pages or Workers for edge rendering. Here is what you should plan for.
Runtime and Compatibility#
| Topic | Vercel Node.js runtime | Vercel Edge runtime | Cloudflare Workers | Supabase Edge Functions |
|---|---|---|---|---|
| Node APIs | Full | Limited | Limited | Deno, limited Node compatibility |
| Cold starts | Usually low | Very low | Very low | Low, globally distributed |
| Best for | Stripe SDK, PDFs, heavy libs | Personalization, lightweight APIs | Edge-first apps | Webhooks, hooks, privileged DB ops |
| Common pitfall | Large serverless bundles | Using Node-only libs | Node module incompatibilities | Assuming long-running jobs |
For detailed trade-offs and migration gotchas, see: Next.js Edge runtime vs Node.js runtime on Vercel and Cloudflare.
Environment Variables and Secrets#
Practical guidelines:
- Put
SUPABASE_SERVICE_ROLE_KEYonly in Supabase Edge Functions environment, not in Next.js unless absolutely required. - In Next.js, keep:
NEXT_PUBLIC_SUPABASE_URLNEXT_PUBLIC_SUPABASE_ANON_KEYin public env only.
- Store provider webhook secrets where the webhook runs. Stripe webhook secret belongs to the Edge Function that receives it.
Deployment Workflow#
A reliable workflow for teams:
- 1Database migrations deployed first
- 2Edge Functions deployed second
- 3Next.js app deployed third
This prevents Next.js calling endpoints or tables that do not exist yet, and it prevents webhooks writing to tables missing new columns.
Versioning and Backward Compatibility#
Webhooks can arrive at any time, including during deployments. Your webhook handler must tolerate old payload shapes for a short window.
Two tactics that reduce incidents:
- Make DB changes backward compatible for at least one deploy cycle.
- Gate new behavior with a feature flag stored in DB, not only environment variables.
# A Practical Request Flow: End-to-End Example#
Consider a typical "invite teammate" feature.
- User clicks Invite in the UI.
- Next.js Server Action validates email and role.
- Server Action inserts an invite row under RLS.
- A DB trigger or scheduled job handles expiration cleanup.
- An automation or Edge Function sends the email via provider.
Where to send the email:
- If the email provider posts webhooks back, keep that webhook in Edge Functions.
- If you need retries and branching, push to automation.
- If it is a simple one-off, you can send it directly in the Server Action with a provider SDK, but be mindful of latency.
A common compromise:
- Server Action writes an
outbox_messagesrow. - A scheduled Edge Function processes the outbox and marks rows as sent. This gives you retries and idempotency without introducing a full queue.
# Observability and Failure Handling#
SaaS reliability is usually lost in the cracks between systems.
Minimum observability checklist:
- Log webhook event IDs and store them in a table with a unique constraint to prevent duplicate processing.
- Track retries and last error message for background tasks.
- Emit structured logs from Edge Functions with consistent fields like
event_type,workspace_id, andrequest_id.
A concrete pattern for idempotency:
stripe_eventstable withevent_idunique- Handler does: insert event row first, if conflict then return 200
This prevents double-charging and duplicated provisioning, especially because providers retry on non-200 responses.
# Common Pitfalls in Next.js Supabase Edge Functions Architecture#
- 1
Using service role in Next.js route handlers
It works until a misconfiguration exposes it. Keep privileged keys in Edge Functions. - 2
Skipping RLS because Edge Functions can bypass it
RLS reduces the amount of code you must audit. Use bypass only when necessary. - 3
Doing long-running work in Edge Functions
Edge Functions are excellent for short-lived compute. Use a worker or automation for heavy jobs. - 4
Treating webhooks as “just another API”
Webhooks need raw body signature verification, idempotency, and safe failure modes. - 5
Mixing Node-only libraries into Edge runtimes
Decide per endpoint. If a library is Node-only, place it in a Node runtime route.
# Key Takeaways#
- Use Supabase Edge Functions for provider-facing endpoints like webhooks and auth hooks, and for privileged operations that require the service role key.
- Use Next.js Server Actions for UI-driven writes and workflows where RLS can enforce authorization and you benefit from co-locating logic with React routes.
- Keep an opinionated structure: domain modules in Next.js, shared pure utilities in packages, and small single-purpose Edge Functions with
_sharedhelpers. - Deploy in order: migrations first, then Edge Functions, then Next.js, and keep webhook handlers idempotent to survive provider retries.
- Choose runtime intentionally: Node runtime for heavy SDKs, Edge runtime for low-latency lightweight endpoints, and keep secrets in the environment where they are used.
# Conclusion#
A modern SaaS backend does not need a huge bespoke API layer. With the right Next.js Supabase Edge Functions architecture, you can keep UI workflows simple with Server Actions, keep provider integrations robust with Edge Functions, and let Postgres RLS enforce tenant boundaries by default.
If you want us to review your current architecture or help you implement a production-ready Next.js and Supabase SaaS foundation, contact Samioda and we will map your domains, runtimes, and deployment pipeline to a plan you can ship confidently.
FAQ
Founder & Senior Developer at Samioda. 8+ years building React, Next.js, Flutter and n8n automation solutions for clients across Europe.
More in Web Development
All →React Error Handling in 2026: Error Boundaries, Retries, and UX Patterns That Scale
A practical layered guide to React error handling patterns in 2026: component and route error boundaries, TanStack Query and SWR retries, and UX messaging that scales across teams.
React Component Contract Testing: MSW + Storybook as Living API Mocks
A practical guide to React component contract testing using shared MSW handlers across Storybook and automated tests to prevent mock drift, flaky UI, and regressions in CI.
React Data Table Patterns at Scale: Virtualization, Column Pinning, Filters, and Export (TanStack Table + Virtual)
Production-ready React data table patterns using TanStack Table and TanStack Virtual: state architecture, server-side pagination and sorting, debounced filters, column pinning, exports, and performance pitfalls.
Need help with your project?
We build custom solutions using the technologies discussed in this article. Senior team, fixed prices.
Related Articles
Next.js Multi‑Region Deployments: Patterns for Lower Latency on Vercel and Cloudflare
A practical guide to Next.js multi-region deployment in 2026: edge rendering, regional SSR, and data locality patterns on Vercel and Cloudflare, including database caveats and a latency validation checklist.
Next.js Edge Runtime vs Node.js Runtime (Vercel and Cloudflare): What to Run Where
A practical decision framework for choosing Next.js Edge Runtime vs Node.js Runtime in 2026, with real examples, limitations, and a final use-case matrix.
Next.js + Supabase SaaS Starter Architecture (App Router): Auth, RLS, Billing, and Multi-Tenancy
A production-ready blueprint for a Next.js App Router + Supabase SaaS starter architecture: auth, Postgres data model, RLS policies, Stripe billing, and multi-tenant organization design with concrete examples.