Create docs/CORS.md with allowed origins, step-by-step pattern for adding CORS to new edge functions, and guidance on adding new origins. Link from docs/ARCHITECTURE.md under API & Data Pipeline.
3.5 KiB
CORS
How cross-origin request protection works and what to do when adding new API endpoints.
Overview
Every API response must include CORS headers so browsers allow the frontend to read it. Two parallel implementations exist — one for standalone edge functions, one for the sebuf gateway — but they share the same origin allowlist and logic.
| File | Used by | Methods |
|---|---|---|
api/_cors.js |
Standalone edge functions (api/*.js) |
GET, OPTIONS (configurable) |
server/cors.ts |
Sebuf gateway (api/[domain]/v1/[rpc].ts) |
GET, POST, OPTIONS |
Allowed Origins
Both files use the same regex patterns:
| Pattern | Matches |
|---|---|
(*.)?worldmonitor.app |
Production + subdomains (tech., finance., etc.) |
worldmonitor-*-elie-*.vercel.app |
Vercel preview deploys |
localhost:* / 127.0.0.1:* |
Local development |
tauri.localhost:* / *.tauri.localhost:* |
Desktop app (Tauri v2) |
tauri://localhost / asset://localhost |
Desktop app (Tauri v1 / asset protocol) |
Requests from any other origin receive a 403 response. Requests with no Origin header (server-to-server, curl) are allowed through — the isDisallowedOrigin check only blocks when an origin is present and not on the allowlist.
Adding CORS to a New Edge Function
Every standalone edge function in api/ must handle CORS manually. Follow this pattern:
import { getCorsHeaders, isDisallowedOrigin } from './_cors.js';
export default async function handler(req) {
const cors = getCorsHeaders(req);
// 1. Block disallowed origins
if (isDisallowedOrigin(req)) {
return new Response(JSON.stringify({ error: 'Forbidden' }), {
status: 403,
headers: { 'Content-Type': 'application/json', ...cors },
});
}
// 2. Handle preflight
if (req.method === 'OPTIONS') {
return new Response(null, { status: 204, headers: cors });
}
// 3. Spread cors into every response
return new Response(JSON.stringify(data), {
headers: { 'Content-Type': 'application/json', ...cors },
});
}
Key rules:
- Every response must include
...corsin its headers — including errors, rate-limit 429s, and 500s. - Preflight (
OPTIONS) must return204with CORS headers and no body. getCorsHeaders(req, methods)— pass a custom methods string if the endpoint supports more thanGET, OPTIONS(e.g.,'POST, OPTIONS').
Sebuf Gateway (RPC Endpoints)
RPC endpoints defined in .proto files do not need manual CORS handling. The gateway (server/gateway.ts) calls getCorsHeaders() and isDisallowedOrigin() from server/cors.ts automatically for every request. CORS headers are injected into all responses including error boundaries.
Adding a New Allowed Origin
To allow a new origin:
- Add a regex pattern to
ALLOWED_ORIGIN_PATTERNSin bothapi/_cors.jsandserver/cors.ts. - Update the test in
api/_cors.test.mjs. - If the origin is a new production subdomain, also add it to the Cloudflare R2 CORS rules (see MEMORY.md notes on R2 CORS).
Allowed Headers
Both implementations allow these request headers:
Content-TypeAuthorizationX-WorldMonitor-Key(API key for desktop/third-party access)
To allow additional headers, update Access-Control-Allow-Headers in both files.
Railway Relay CORS
The Railway relay (scripts/ais-relay.cjs) has its own CORS handling with the ALLOW_VERCEL_PREVIEW_ORIGINS env var. See RELAY_PARAMETERS.md for details.