← Workshops Β· Part 2 Β· Foundations

Platforms for Shipping: Vercel, Supabase & the AWS Alternatives

You built it in Part 1 β€” now ship it. What Vercel and Supabase actually do, how to deploy, and the AWS services that do the same jobs, so the rest of the series can just say “deploy it.”

πŸ“ Test yourself ↓
20 min readFoundationsRead or present

You built it β€” now ship it

In Part 1 you got an app running on your machine. This part is the second foundation: the platforms that put it online, and the AWS services that do the same jobs. The backend parts (3–5) just say "now deploy it" β€” because the how lives here. Read it once; the others link back.

πŸŽ›οΈ

Everything below is something you can hand to Claude Code β€” "deploy this to Vercel," "provision a Supabase project," "put it behind CloudFront." This article is so you know what to ask for.

Every web app needs two things

πŸ–₯️

A place to serve the frontend

Takes your built site and delivers it to browsers over HTTPS, fast, worldwide. "Hosting" / "deploy".

πŸ—„οΈ

A backend

Stores data, signs users in, runs logic the browser can't be trusted with. Your API + database.

Vercel answers the first. Supabase answers the second. AWS does both β€” more pieces, more control.

Vercel β€” your deploy platform

Vercel hosts frontends (from the team behind Next.js). Connect a GitHub repo; every push builds and deploys, with a preview URL per branch and a global CDN.

npm i -g vercel
vercel            # preview deploy
vercel --prod     # production deploy
⚑

Why people reach for it: zero-config for Next.js, instant preview links, git-push deploys. Live in minutes.

Static files vs Next.js pages on Vercel

"Deploying to Vercel" hides a distinction worth understanding, because it decides what your site costs to run and what can break at 3am. Three different things can answer a request, and only one of them runs code:

What it isWhen it's producedWho serves it
Static file β€” anything in public/Never. You wrote it; it's copied as-is.The CDN. No compute, nothing to fail.
Prerendered page β€” a Next.js route with no per-request dataAt next build, once.The CDN, same as a static file.
Dynamic page or route handlerOn every request.A Vercel Function β€” real Node.js, per-request cost.

The build tells you which one you got. Run next build and read the route table β€” β—‹ is prerendered, and Ζ’ means a function runs on every hit:

$ next build

Route (app)
β”Œ β—‹ /                     ← prerendered at build, served from the CDN
β”” β—‹ /_not-found

β—‹  (Static)   prerendered as static content
Ζ’  (Dynamic)  server-rendered on demand

A page goes dynamic the moment it reads something that only exists per request β€” cookies(), headers(), searchParams, or an uncached fetch. That's not a failure; it's the correct answer for a logged-in dashboard. It's only a problem when it happens by accident to a page that could have been free.

πŸ“„

This page is the example. The workshop you're reading is a plain HTML file in public/. It's never rendered, never touches a function, and can't break at runtime β€” the CDN just hands it over. The site's homepage next door is a Next.js route that happens to prerender. Same deploy, three different economics.

πŸ”

Static with a shelf life. Between the two extremes sits revalidation (ISR): prerender the page, then let it refresh every N seconds or on demand. Readers still get a CDN response; the data just isn't frozen at build time. Next 16 also has an opt-in Cache Components model that makes this per-component rather than per-page.

⚠️

Don't reach for the Edge runtime. It's a common reflex and usually the wrong one β€” it's a restricted runtime without full Node.js. Vercel's default (Fluid Compute) runs in the same regions at the same price, and streaming, SSE and AI token streaming all work there with no config. Stay on Node.js unless you have a specific reason not to.

Supabase β€” your backend platform

Supabase wraps a managed Postgres database with the services an app needs, so the database itself becomes your backend β€” no server to run.

🐘

Postgres + auto APIs

A real SQL database, reachable via REST, GraphQL, and RPC.

πŸ”‘

Auth

Email, magic links, and social sign-in, tied to the database.

πŸ“‘

Realtime

Subscribe to row changes over WebSockets β€” live updates in one line.

πŸ›‘οΈ

Row Level Security

Rules enforced per row, so it's safe to talk to from the browser.

The key idea: the database is your API (RPC)

With Supabase you often don't write API endpoints at all. You put logic in a Postgres function and call it by name β€” an RPC. The function is the endpoint:

// no URL, no route file β€” just call the function by name
const { data, error } = await supabase.rpc('submit_answer', {
  game_id: gameId, player_id: playerId, option_id: optionId,
});

Pair that with Row Level Security and the database can safely be your entire backend β€” the rules travel with the data instead of living in an API layer you have to remember to write.

⚠️

Handle SECURITY DEFINER with care. It's often described as "runs trusted, server-side", which undersells it: the function runs as its creator and bypasses RLS entirely. Worse, Postgres grants EXECUTE to everyone by default, so a SECURITY DEFINER function sitting in public is a public endpoint any anonymous visitor can call. Default to SECURITY INVOKER; when you genuinely need to bypass RLS, keep the function out of the exposed schema and check auth.uid() inside the function body yourself. Never reach for it just to make a permission error go away.

How Supabase Auth actually works

Auth is the part people most often get wrong when they move to Supabase, because it doesn't work like the auth service you're used to. Most auth providers hand you a token and leave enforcement to you. Supabase hands you a token the database itself understands β€” and that one fact explains everything else about it.

The token is a database credential

Sign a user in and Supabase issues a short-lived access token (a JWT) plus a long-lived refresh token. The interesting part is what's inside the access token:

// the claims that matter
{
  "iss":  "https://abcd.supabase.co/auth/v1",
  "sub":  "9f8c…",            // the user id β€” this is auth.uid() in SQL
  "role": "authenticated",    // an actual Postgres role
  "exp":  1893456000
}

role is not a label your code checks β€” it is the Postgres role the query runs as. So when the browser talks to the database, your Row Level Security policies are the thing standing in the way, and they're enforced by Postgres rather than by an API layer you had to remember to write:

create policy "own rows only" on notes
  for select to authenticated
  using ( (select auth.uid()) = user_id );

That's why it's safe to query Supabase straight from the browser, and why "where do I put my API?" often has the answer "you don't need one".

browser ──token──▢ Supabase ──▢ Postgres
                                  β”‚
                                  β–Ό  RLS reads auth.uid() from the token
                            only that user's rows come back
🚨

The trap that bites everyone. A user can edit their own user_metadata, and it shows up in the token. Never make an authorization decision from it β€” no "is_admin" in user_metadata. Put anything that grants power in app_metadata, which users can't touch. Related: deleting a user does not invalidate tokens already issued to them β€” sign them out or revoke the session too.

On the frontend (Next.js)

Install @supabase/supabase-js and @supabase/ssr. The browser gets a client built with your publishable key β€” safe to ship, because RLS is what protects the data:

// lib/supabase/client.ts
import { createBrowserClient } from '@supabase/ssr';

export const createClient = () => createBrowserClient(
  process.env.NEXT_PUBLIC_SUPABASE_URL!,
  process.env.NEXT_PUBLIC_SUPABASE_PUBLISHABLE_KEY!,
);

Server Components can read the session from cookies but can't write them, so refreshing an expiring token is the job of one file at the project root. In Next 16 that file is proxy.ts β€” middleware.ts is the deprecated name for the same thing. It refreshes the token and writes it back to both the request and the response.

Which call you reach for on the server matters, and the answer changed recently:

CallUse it forCost
getClaims()Protecting pages and data. Verifies the token's signature locally against the project's public keys.No network call.
getUser()When you need the freshest profile β€” it asks the Auth server.A round trip per call.
getSession()Only when you need the raw tokens to forward somewhere.Cheap, but the user object on it isn't trustworthy on its own.
πŸ”’

Never gate a page on getSession(). The session comes from cookies, and cookies can be forged. It doesn't re-verify anything. Use getClaims() to decide whether someone gets in.

On a separate backend

You don't have to give up your own API. If you're running the Python service from Part 6, the frontend sends the same token it already has, in the header the whole web already understands:

Authorization: Bearer <supabase access token>

Your backend then verifies it locally. Supabase publishes its public keys, so there's no call back to Supabase on the hot path β€” fetch the key set once, cache it, and check the signature yourself:

# the project's public keys β€” cache these, don't fetch per request
https://<project-ref>.supabase.co/auth/v1/.well-known/jwks.json

# then, per request:
#   1. verify the signature against the matching key
#   2. check exp and iss
#   3. trust sub as the user id β€” never read the id from the body
🧩

This is the same shape as the rest of the series. Verifying a signature at the edge of your service and passing a trusted user id inwards is exactly what the platform's Lambda Authorizer (Part 4) does, and Part 9 covers the header contract in full. Only the issuer changes.

Supabase Auth vs Cognito

They both issue JWTs and both publish a JWKS endpoint. The difference is what the token is for:

Supabase AuthAmazon Cognito
What the token buys youA database identity β€” role and auth.uid() are read by Postgres itself.Proof of identity for your app. Nothing downstream interprets it for you.
Where rules liveRLS policies, next to the data, enforced on every query.In your application code, or an API Gateway authorizer you write.
Tokens issuedAccess + refresh.ID + access + refresh β€” and conflating the first two is the classic Cognito bug.
Can the browser hit the data directly?Yes β€” that's the design.No. Cognito fronts an API you build.
Roles & permissionsapp_metadata and custom claims.Groups, surfaced as the cognito:groups claim.
AWS credentials for a userNo equivalent.Identity Pools vend temporary IAM credentials β€” genuinely useful for direct S3 access.
Setup costMinutes; it's on by default in a new project.A user pool, an app client, domains, flows β€” an afternoon, and easy to misconfigure.
Where it hurtsYou're adopting Postgres-shaped auth; escaping RLS later means rebuilding enforcement.Painful to migrate off (password hashes don't export), and the console is a maze.
🧭

Choosing: if your data lives in Supabase Postgres, use Supabase Auth β€” fighting to put Cognito in front of RLS throws away the main benefit. If you're already on AWS with API Gateway and Lambda, Cognito is the path of least resistance, and Identity Pools are the one capability Supabase has no answer to. Both are fine; mixing them without a reason is how you end up maintaining two user directories.

The AWS alternatives

AWS hands you individual services you assemble. Every job Vercel and Supabase do has an AWS counterpart:

JobVercel / SupabaseAWS equivalent
Host the frontendVercelS3 + CloudFront, or AWS Amplify Hosting
Run backend codeVercel Functions / Supabase RPCLambda + API Gateway, or containers on ECS Fargate
DatabaseSupabase PostgresRDS / Aurora (SQL) or DynamoDB (NoSQL)
AuthSupabase AuthCognito
RealtimeSupabase RealtimeAPI Gateway WebSockets, or AppSync
File storageSupabase StorageS3
Deploy on pushBuilt inAmplify, or GitHub Actions + SAM/CDK/Terraform
πŸͺ£

Quickest AWS deploy: build to static files, aws s3 sync to a bucket, put CloudFront in front. Or use Amplify Hosting for Vercel-like git-push deploys on AWS.

Which should you choose?

If you…Lean toward
Want to ship fast with a small teamVercel + Supabase
Are already on AWS / have compliance needsAll-AWS
Want the shortest path to "it works"Vercel + Supabase

Capture your deploy setup in CLAUDE.md

Write your platform choices where Claude Code sees them every session β€” so "deploy this" just works:

# CLAUDE.md

## Deploy
- Frontend: Vercel, auto-deploys on push to main; preview on every PR.
- Manual:   vercel (preview) / vercel --prod (production).
- Data:     Supabase project ref abcd β€” migrations in supabase/.

## Environments & secrets
- Local env in .env.local (gitignored). Never commit keys.
- anon key = public/client-safe; service-role key = server-only.
- Mirror NEXT_PUBLIC_* vars in Vercel β†’ Settings β†’ Environment Variables.

## AWS alternative
- Host: aws s3 sync ./out s3://<bucket> + CloudFront invalidation, or Amplify.

## Rule
- Every deploy comes from git β€” no manual uploads.

How Claude Code helps

Ask for the platform Skills by name and let Claude Code run the deploy commands:

β–²

vercel

Deploy, env vars, domains, CLI workflows.

🟒

supabase

Database, Auth, Realtime, RLS, migrations.

☁️

aws-serverless / aws-amplify

Lambda, API Gateway, SAM, Amplify hosting.

🧭

"Which should I use?"

Describe your app + constraints; let Claude Code recommend a stack.

Check yourself

Quiz β€” 13 questions

Answer every question, then submit to see your score and the correct answers.

Not sure which stack fits your project?

Vercel, Supabase, AWS β€” the right choice depends on your team and constraints. Happy to help you pick, or run a workshop for your team.

βœ‰οΈ Get in touch ← Back to workshops