The _shared module
Lesson 5 · about 15 minutes · know what already exists before you write a helper, and what breaks when a handler bypasses it.
Every edge function is its own Deno program, so code they have in common lives in one folder that each function imports by relative path: src/supabase/functions/_shared/. The leading underscore is a Supabase convention: a folder starting with _ is not deployed as a function. Fourteen files, about 2,000 lines. Four of them are imported from dozens of places; the rest exist because two functions had to agree on one behaviour.
The inventory
| File | Importers | What it owns | If you bypass it |
|---|---|---|---|
utils.ts | 39 | createSuccessResponse (204 on null, else 200 JSON) and createErrorResponse ({success:false,error}). | Clients parse two error shapes. The stocks handlers still carry local copies that add CORS headers; that is tolerated debt, not a pattern to copy. |
cors.ts | 33 | Origin allow-list, OPTIONS handling, validateCorsOrigin, publicReadCorsHeaders for the no-Origin crawler surface. cors.ts:115, :157 | A hand-rolled header set silently allows any site, or 403s every cron caller. |
jwt/default.ts | 28 | getUserId (JWKS-verified user JWT) and validateServiceRole (constant-time compare of apikey or bearer to the service key). jwt/default.ts:37, :74 | Timing-leaky string compare, or trusting sub from an unverified token. |
validation.ts | 17 | ValidationResult<T> plus validators for ticker, fiscal period/year, page, pageSize, sortBy, sortDir, timeframe, date. Each normalises then rejects. validation.ts:35 | A ticker like '; DROP TABLE reaches a query string. The regex is ^[A-Z0-9.]{1,10}$ with no leading, trailing or doubled period. |
pagination.ts | few | fetchAllPages: loops .range() until an empty page. pagination.ts:34 | PostgREST caps at max_rows = 1000 with no error. A 1,000-row result that is really 5,400 looks like success. |
audit.ts | 6 | AUDIT_ACTIONS const and writeAuditLog into audit.snaptrade_audit_logs. Failures log but never fail the request. audit.ts:20 | A string typo creates a new action name nobody queries for. |
openaiModeration.ts | 6 | Client for the moderation API. Fail-closed: anything but a clean 2xx with a boolean verdict throws ModerationUnavailableError. openaiModeration.ts:9 | An outage becomes "allowed" and a slur lands in a username. |
canonicalTicker.ts | 2 | pickCanonicalListing: chooses the tradeable US listing from Companies.tickers. Mirrors private.canonical_ticker(jsonb) in SQL. canonicalTicker.ts:89 | Feed rows (resolved in a trigger) and live rows (resolved in TypeScript) name different tickers for one company. |
newsApiKey.ts | 2 | Mint, parse and authenticate wsb_news_<id>_<secret> keys sent in X-API-Key. Secret stored as SHA-256 only. newsApiKey.ts:73 | Overloading Authorization or apikey, both of which Kong already interprets. |
newsRpc.ts | 4 | SQLSTATE to HTTP: 42501 403, 40900 409, P0002 404, 23514/23503 400, else 500. newsRpc.ts:11 | The JWT path and the API-key path answer differently for the same DB error. |
newsMedia.ts | 2 | The only writer to the news-media bucket; the bucket has no write policy. newsMedia.ts:4 | There is no bypass. That is the design. |
image.ts | via newsMedia | inspectImage reads JPEG/PNG headers for real type and size, and strips metadata. image.ts:112 | Trusting the uploader's Content-Type and filename. |
newsFeaturedImage.ts | 2 | Renders a fallback featured image from a pre-built RGBA template plus a company icon, using resvg WASM. One render at a time per worker. newsFeaturedImage.ts:42 | Blowing the 2-second CPU budget on concurrent renders. |
resvg/, newsImageAssets/ | assets | The WASM binary and template pixels. Shipped via static_files in config.toml for the two news functions that need them. | Deploy succeeds, then import.meta.url resolves to a file that is not in the bundle. |
node_modules. The runtime bundles whatever the entrypoint imports, transitively, by path. A relative import into _shared/ is the cheapest possible module system: no publish, no version, no lockfile churn. The cost is that _shared must import its dependencies with the same specifier the handlers use, or two copies of supabase-js end up in one bundle with structurally incompatible SupabaseClient types. stocks/handlers/hydrate.ts:1-3 explains one such collision.Three patterns to recognise
validateTicker returns {valid, value} or {valid, error}. The handler decides the status code. validateCorsOrigin goes further and hands back the finished 403 Response. The type is a discriminated union, so if (!r.valid) return narrows correctly. validation.ts:12, cors.ts:64-66writeAuditLog, which fails open because a missing audit row must not block a user. Each file states which it is in its docblock. Read that before deciding how to handle its error.canonicalTicker.ts and private.canonical_ticker implement one rule twice because one caller is a trigger and the other is TypeScript. The file header says "change one and change the other". When you find a helper with a migration number in its comment, grep for the SQL twin before editing.Not in _shared: the stocks function has its own utils/ with 33 files (Massive client, timezone, retry, concurrency). Those are stocks-only by decision, not accident. Promote one to _shared only when a second function imports it.
Check yourself
Do this in the repo
- Count the importers yourself and see which functions never touch
_shared:cd src/supabase/functions grep -rhoE "_shared/[a-zA-Z/]+\.ts" --include='*.ts' --include='*.tsx' . | sort | uniq -c | sort -rn - Exercise the validator without the stack:
cd src/supabase/functions deno eval "import {validateTicker} from './_shared/validation.ts'; console.log(validateTicker(' brk.b '), validateTicker('.AAPL'), validateTicker(42))" - Open
_shared/cors.tsand find where a valid origin is echoed back rather than wildcarded, then explain whyAllow-Credentials: trueforces that.
Primary source
Supabase: Managing dependencies in Edge Functions covers the _shared convention, import specifiers and why the underscore folder is skipped on deploy.
Next: Lesson 6, the stocks public API, where every one of these helpers is used on the read path. Ask your teacher in the terminal if any row in the inventory table is unclear.