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
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`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.*]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:52handleCorsOptions 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.tsvalidateCorsOrigin 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 20251218000000new 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-72validateTicker 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.tscreateClient(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-25anon. 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.sqlcreateSuccessResponse 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.tsThe Deno you need for step 3 and 8
| Thing | What it is | Where 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@2 | Import from the JSR registry. Version-pinned by the specifier, hashed in deno.lock. | _shared/jwt/default.ts |
npm:react@19.2.4 | Import 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.0 | Raw-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.json | Project 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 |
| Permissions | Deno 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
- Start the stack from
src/:npx supabase startthennpx supabase functions serve --no-verify-jwt. - 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' - Open
src/supabase/functions/stocks/index.tsand 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.
Next lesson: Cron, Vault and pg_net, direction B on the system map. How a migration schedules a job, why private.post_to_edge_function exists, and what net._http_response tells you when a cron silently fails.
Anything unclear, ask your teacher in the terminal: "why does the router take the last path segment?" or "show me the RLS policy for step 9" are both good questions.