Skip to main content
ASoc
Comparison

Supabase vs Vercel: Both, and 42 Lines Where They Meet

Not alternatives. Vercel builds and serves 584 pages, Supabase holds the schema and the policies, and one 42-line proxy can take the whole site down.

The ASoc Team10 min read

This storefront runs on both. Of thirteen runtime dependencies in package.json, two are Supabase (@supabase/ssr, @supabase/supabase-js) and one is Vercel (@vercel/analytics); Vercel builds and serves 584 prerendered pages, Supabase holds eight SQL migrations' worth of schema, row-level security and a private storage bucket. They are not two answers to one question. The only place they meet is a 42-line file, and that file is also the one way either of them can take the whole site down.

The short answer

Vercel is a deployment platform: it builds your app from a Git push, serves it from a CDN, and runs your server functions. Supabase is a backend-as-a-service: hosted Postgres with row-level security, auth, storage and server-side functions. They overlap at the edges — both sell a Postgres, both run functions — but a typical project uses one of each rather than choosing between them.

The division of labour, as this repo assigns it

JobHandled byWhere it lives
Build + prerender 584 pagesVercelnext build, no vercel.json needed
Serving static HTML, images, WebP variantsVercel CDNpublic/, build-time -card.webp / -view.webp derivatives
Security response headers + CSPVercel (via the framework)next.config.tsasync headers()
Running Server Actions + 2 Route HandlersVercel functionssrc/lib/actions/* (8 modules), src/app/api/*/route.ts
Identity, sessions, OAuthSupabase Authsrc/lib/supabase/{client,server,admin}.ts
Orders, entitlement slots, download auditSupabase Postgressupabase/migrations/0001…0008
Authorisation on that dataSupabase RLSread-own-only policies, enforced in the database
Release zips behind a paywallSupabase Storageprivate releases bucket, signed URLs
Conversion analyticsVercel Web Analyticssrc/lib/analytics.ts, five named events, cookieless

Nothing in the left column is contested. Vercel never sees the database; Supabase never serves a page. The reason the comparison gets asked at all is that both vendors' marketing pages have grown into each other's territory — Vercel sells a Postgres, Supabase runs Edge Functions — and from the outside the stacks look like they should collide.

Where they actually touch: 42 lines

There is exactly one file where "the host" and "the backend" are the same request:

// src/proxy.ts — Next 16 proxy (formerly middleware)
export async function proxy(request: NextRequest) {
  let response = NextResponse.next({ request });

  const supabase = createServerClient(
    process.env.NEXT_PUBLIC_SUPABASE_URL!,
    process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!,
    {
      cookies: {
        getAll: () => request.cookies.getAll(),
        setAll: (cookiesToSet) => {
          /* rotate auth cookies onto the response */
        },
      },
    },
  );

  await supabase.auth.getClaims();
  return response;
}

It refreshes the Supabase session on each request so Server Components see a valid one, and writes the rotated cookies onto the response. It calls getClaims(), which validates the JWT, and never getSession(), which returns whatever the cookie says without verifying it.

Its config.matcher excludes static assets, image files, sitemap.xml, robots.txt and the Search Console verification file — nothing that needs a session. Everything else pays for it.

The failure mode that makes them one system

Because the proxy runs on effectively every request and dereferences two environment variables with !, a deploy to Vercel that is missing NEXT_PUBLIC_SUPABASE_URL or NEXT_PUBLIC_SUPABASE_ANON_KEY does not degrade the account pages. It errors site-wide — on the marketing home page, on a blog post, on /pricing, none of which have anything to do with Supabase.

That is a real constraint in this repository, written into its CLAUDE.md as a do-not-deploy note: the commerce-enabled main must not ship to production until those variables exist in the Vercel project, because the Phase A build currently in production has no proxy and does not care.

The general lesson survives the specifics. A backend dependency in your middleware is a dependency of every route, including the ones that never call the backend. If that is not what you want, gate the proxy's matcher to the authenticated surface area rather than to everything-but-assets — and know that you are trading a site-wide blast radius for the risk of a stale session on a page you forgot to include.

The coupling runs one more level down, into the build. The CSP in next.config.ts derives its connect-src from the Supabase URL at build time:

const supabaseOrigin = (() => {
  try {
    return new URL(process.env.NEXT_PUBLIC_SUPABASE_URL!).origin;
  } catch {
    return "https:";
  }
})();

Get that env var wrong in the build environment and the policy is wrong in a way no local test reproduces: the pages render, and the browser silently blocks the auth calls.

The 68 KiB tax, and taking it off the critical path

Adding a backend to a mostly-static site has a cost the architecture diagram does not show. @supabase/ssr plus auth-js is roughly 68 KiB over the wire, 255 KiB parsed, and it was reaching every page: Header renders site-wide, useOwnedProducts renders behind every templates grid, and both imported the browser client at module scope. Pages with no account UI whatsoever — /, /blog, /docs, /pricing — were downloading and parsing the entire auth stack before they could settle.

Both call sites only touch the client inside an effect, so the import can wait for the effect too:

// src/lib/supabase/lazyClient.ts
export function hasAuthCookie(): boolean {
  if (typeof document === "undefined") return false;
  return /(?:^|;\s*)sb-.+?-auth-token/.test(document.cookie);
}

export async function loadSupabaseClient(): Promise<BrowserClient> {
  const { createClient } = await import("@/lib/supabase/client");
  return createClient();
}

The cookie probe is the second half: @supabase/ssr stores its session in a non-HttpOnly cookie precisely so the browser client can read it, so a signed-out visitor can be detected without loading the library at all. It is a rendering shortcut and nothing more — it can never grant anything, because every real gate stays server-side.

Which one enforces what

The split that matters most is not hosting versus database. It is where an authorisation decision is made, and the answer is never Vercel:

-- supabase/migrations/0001_commerce_init.sql
create policy "own orders"    on public.orders            for select using ((select auth.uid()) = user_id);
create policy "own slots"     on public.entitlement_slots for select using ((select auth.uid()) = user_id);
create policy "own downloads" on public.download_events   for select using ((select auth.uid()) = user_id);

They are the whole of this site's user-data model: profiles, orders, entitlement slots and a download audit trail, each readable only by the account that owns the row. Those policies hold whether the query arrives from a Server Component on Vercel, a Server Action, a Route Handler, or curl with a stolen anon key. A platform can route a request to the wrong function; it cannot route a row past a policy. The download route re-runs its own authorizeDownload per request on top of that — the ownership lookup that decorates the UI is a convenience, not a gate.

Where they genuinely compete

CapabilityVercelSupabaseHow this repo picks
PostgresVercel Postgres (marketplace-provisioned)Core product, with RLS, RPCs, migrationsSupabase — the auth and the data are the same system, so auth.uid() works inside a policy
Server-side functionsRoute Handlers + Server Actions, deployed with the appEdge Functions, deployed to the database sideVercel — the two handlers need the framework's request lifecycle and raw body
File storageBlob storageStorage buckets with policy-controlled accessSupabase — release zips must be private and signed per entitlement
AuthPartner integrationsBuilt in, tied to RLSSupabase
AnalyticsWeb Analytics, cookielessVercel — five named conversion events, no PII in props
Hosting a Next.js appThe first-party pathNot a hosting productVercel

The one row worth arguing over is Postgres, and the tiebreaker is not performance. It is that auth.uid() inside a row-level security policy only works when the auth system and the database are the same system. Split them and every policy becomes application code you have to get right on every path. Vercel Postgres vs Supabase takes that argument in full; Supabase vs Firebase covers the other direction, where the backend candidate is a different BaaS entirely.

Troubleshooting

SymptomCauseFix
Every route 500s after a deploy, including static pagesMiddleware/proxy dereferences a missing Supabase env var on every requestSet the vars in the deployment environment before shipping; consider narrowing config.matcher to authenticated routes
Auth works locally, silently fails in productionCSP connect-src was built from a missing or wrong NEXT_PUBLIC_SUPABASE_URLCheck the deployed response headers, not the local ones; the origin is baked in at build time
Session is valid in the browser but absent in a Server ComponentNo session refresh on the request path, or getSession() used where the JWT was never verifiedRefresh in the proxy; use getClaims()/getUser() for any decision
A page with no account UI ships the whole auth libraryThe browser client is imported at module scope by a site-wide componentImport it dynamically inside the effect that uses it, and probe for the session cookie first
RLS returns zero rows for a user who should see themThe query ran with the anon key and no session, or the policy compares the wrong columnCheck auth.uid() in the policy against the table's owner column; verify the request actually carries the session cookie
Service-role key needed in a client componentAn operation was placed on the wrong side of the boundaryMove it into a Server Action or Route Handler; the service-role key bypasses RLS and must never reach a browser

Frequently asked questions

Do I have to choose between Supabase and Vercel? Usually not. They solve different problems, and the common setup is a Next.js app deployed on Vercel talking to a Supabase project. The question is worth asking only for the two capabilities that genuinely overlap — Postgres and server-side functions.

Can I host a Next.js app on Supabase? No. Supabase is not a hosting platform for a frontend framework; it provides the database, auth, storage and Edge Functions your app calls. Your app still needs somewhere to build and run.

Can I skip Supabase and use Vercel's own Postgres and blob storage? You can, and for a project with no user accounts it removes a vendor. The moment you add accounts, you are choosing between an auth system wired into your database's policy engine and one you integrate yourself. This codebase gates downloads on rows a policy protects, which made that an easy call.

What breaks if I move one of them later? Moving hosts is mostly a build-and-DNS exercise. Moving the backend is not: row-level security policies, SECURITY DEFINER functions and storage rules are Postgres and Supabase features, not portable application code. Weigh the two migrations differently when you pick.

Does adding Supabase slow down a static site? It can, and that is a bundle question rather than a database one — see the 68 KiB above. Prerendered pages stay fast as long as the auth client is loaded on demand rather than imported by a component that renders on every page.

Templates in this post

ASoc Quill, ASoc Rally and ASoc Rank are Next.js + Tailwind landing page templates that deploy to Vercel as-is and stay static until you add a backend — no proxy on every request until you need one.

Browse the full sets: Next.js landing page templates, Tailwind landing page templates.

Keep reading

Comparison9 min read

SvelteKit vs. Remix: Same Primitives, Different Component Bill

Both answer routing, data loading and form posts the same way. The divergence is what reaches the browser — and how much of a page you can avoid shipping at all.

Read more
Comparison10 min read

Tailwind CSS v4 vs Bootstrap 5 for Dashboard UIs in 2026

A fair comparison of two mature CSS frameworks for admin UIs — component coverage, customization ceiling, bundle size, and the team each one suits.

Read more
Comparison9 min read

Tailwind vs. CSS: 139 Components, One 133-Line Stylesheet

139 components, 881 classNames, one 133-line stylesheet: the exact 40 lines of hand-written CSS a Tailwind codebase still needs, and why.

Read more