docs/cors.mdx
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 |
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 v2 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.
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:
...cors in its headers — including errors, rate-limit 429s, and 500s.OPTIONS) must return 204 with CORS headers and no body.getCorsHeaders(req, methods) — pass a custom methods string if the endpoint supports more than GET, OPTIONS (e.g., 'POST, OPTIONS').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.
To allow a new origin:
ALLOWED_ORIGIN_PATTERNS in both api/_cors.js and server/cors.ts.api/_cors.test.mjs.Both implementations allow these request headers:
Content-TypeAuthorizationX-WorldMonitor-Key (API key for desktop/third-party access). See API Key Gating for key management details.To allow additional headers, update Access-Control-Allow-Headers in both files.
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.