Life of a request

Lesson 1 · about 15 minutes · follow GET /functions/v1/stocks/details?ticker=AAPL from the phone to Postgres and back.

Everything else in this backend is a variation on this path. Cron jobs, webhooks and triggers all end up in the same place: a Deno function reading a request and a Postgres role deciding what it may see. Learn this one path cold and the other 200 handlers become predictable.

The path, step by step

1The app sends the request with two headers: apikey: <anon key> and, if a user is signed in, Authorization: Bearer <user JWT>. The anon key is public and ships inside the app binary. config.toml [auth] · api keys from `supabase start`
2Kong, the gateway, receives it. It checks the apikey header, then routes by prefix: /functions/v1/ to the edge runtime, /rest/v1/ to PostgREST, /auth/v1/ to GoTrue. If the function has verify_jwt = true the platform also rejects requests without a valid JWT here, before any of our code runs. Every function in this repo except submit-feedback sets it to false and does its own auth. config.toml [functions.*]
3The edge runtime hands the request to Deno.serve. One Deno isolate per function, one callback that receives a standard Request and must return a Response. There is no Express, no framework: the Web platform fetch types are the API. stocks/index.ts:52
4Gate one: OPTIONS. Browsers send a preflight before a cross-origin call. handleCorsOptions answers 200, with the Access-Control-Allow-Origin header only when the origin is on the allow-list. No header means the browser drops the real request itself. _shared/cors.ts
5Gate two: Origin check. validateCorsOrigin returns either {valid:true, headers} or {valid:false, response}, a discriminated union, so the router can do if (!cors.valid) return cors.response. A missing Origin is a 403. That single decision is why every internal caller must fake Origin: https://api.wallstreetbets.com. _shared/cors.ts · migration 20251218000000
6Routing by last path segment. new URL(req.url).pathname.split('/').filter(Boolean).pop() yields details, and a switch picks handleStockDetailsRequest. Taking the last segment makes the router indifferent to whatever prefix sits in front of it. stocks/index.ts:66-72
7The handler validates input at the trust boundary. validateTicker from _shared/validation.ts runs before anything touches the database. Types for the response live in types/stockDetailsTypes.ts, extending the base types in types/types.ts, exactly as CLAUDE.md prescribes. stocks/handlers/stockDetails.ts
8The handler builds a Supabase client with the anon key. createClient(SUPABASE_URL, SUPABASE_ANON_KEY). Both values come from Deno.env.get. The client speaks PostgREST over HTTP to Postgres, so the edge function is itself a client of the same REST API the app could call. stocks/handlers/stockDetails.ts:24-25
9Postgres runs the query as role anon. RLS policies on Stock_Current_Quotes, Companies and the price-history partitions decide which rows exist for that role. This is the reason step 1 was safe: the key does not grant access, the policies do. migrations/20251119213019_add_anonymous_read_policy_to_companies.sql
10The response goes back out as JSON with the CORS headers from step 5 merged in. createSuccessResponse and createErrorResponse in _shared/utils.ts are the canonical envelope; the stocks handlers still carry local copies that add CORS headers, a leftover the router comment calls "the REFACTOR phase". _shared/utils.ts
Why so many gates before the handler? Each one fails fast for a different attacker. Kong stops requests with no key at all. CORS stops other websites' JavaScript from using our API in a user's browser. Validation stops malformed tickers reaching SQL. RLS stops a leaked anon key from reading private rows. Removing any one of them does not break the happy path, which is exactly why they get removed by accident.

The Deno you need for step 3 and 8

ThingWhat it isWhere you see it
Deno.serve(handler)Built-in HTTP server. Handler is (req: Request) => Response | Promise<Response>. No port config needed on the edge runtime.every index.ts
Deno.env.get(name)Reads an environment variable, returns string | undefined. Locally from functions/.env; in prod from the dashboard's function secrets. The name is the contract.SUPABASE_URL, SLACK_ALERT_WEBHOOK_URL
jsr:@supabase/supabase-js@2Import from the JSR registry. Version-pinned by the specifier, hashed in deno.lock._shared/jwt/default.ts
npm:react@19.2.4Import from npm. Deno installs it into its own cache; no node_modules in the function.functions/deno.json (JSX for email templates)
https://esm.sh/@supabase/supabase-js@2.39.0Raw-URL import. Older style still present in stocks handlers. Same library, different resolution path; mixing them is why CLAUDE.md warns the npm: and jsr: builds have incompatible types.stocks/handlers/stockDetails.ts
deno.jsonProject config: compiler options, lint rules, optionally an imports map. The functions-level file here only sets JSX; submit-feedback has its own and declares it via import_map in config.toml.functions/deno.json
PermissionsDeno is deny-by-default (--allow-net, --allow-env…). The edge runtime grants what functions need; locally the test command uses --allow-all.deno test --allow-all

The mental shortcut: an edge function is a web-standard fetch handler with a filesystem-free module system. If it would work in a browser service worker, it works here.

Check yourself

Answer from memory before scrolling back up. Wrong answers are the point.

Do this in the repo

  1. Start the stack from src/: npx supabase start then npx supabase functions serve --no-verify-jwt.
  2. Run the request three times and predict each status before you hit enter:
    curl -i 'http://localhost:54321/functions/v1/stocks/details?ticker=AAPL' -H "apikey: $ANON_KEY"
    curl -i 'http://localhost:54321/functions/v1/stocks/details?ticker=AAPL' -H "apikey: $ANON_KEY" -H 'Origin: http://localhost:5173'
    curl -i 'http://localhost:54321/functions/v1/stocks/nope' -H "apikey: $ANON_KEY" -H 'Origin: http://localhost:5173'
  3. Open src/supabase/functions/stocks/index.ts and find the line where each of the three outcomes is decided.

Primary source

Read Supabase: Edge Functions overview and skim Deno: Modules and dependencies. Together they cover steps 2, 3 and 8 from the vendor's side.