This document explains how the StellarKraal backend handles Cross-Origin Resource Sharing (CORS), which origins are permitted, and how to configure them correctly for each environment.
CORS is applied by backend/src/middleware/cors.ts as the first middleware in the Express stack (after Helmet). It reads allowed origins from environment variables at startup and validates them immediately β misconfigured origins cause the process to exit rather than silently accept or reject requests at runtime.
The middleware uses two env vars, with ALLOWED_ORIGINS taking precedence:
ALLOWED_ORIGINS (comma-separated list of HTTP(S) URLs)
β if not set
FRONTEND_URL (single HTTP(S) URL)
β if not set in production
CORS blocked (warning logged at startup)
Comma-separated list of origins to allow. Parsed and validated once at module load.
ALLOWED_ORIGINS=https://app.stellarkraal.example.com,https://staging.stellarkraal.example.comRules enforced at startup:
| Check | Development / Test | Production |
|---|---|---|
Wildcard * |
β Allowed | β Startup error |
| HTTP origins | β Allowed | β Allowed |
| HTTPS origins | β Allowed | β Allowed |
Invalid pattern (e.g. no scheme, ftp://) |
β Startup error | β Startup error |
Whitespace around each entry is stripped automatically.
Fallback when ALLOWED_ORIGINS is not set. Accepts a single origin.
FRONTEND_URL=https://app.stellarkraal.example.comIn development, if neither variable is set, non-auth routes accept any origin (*) and auth routes use credentials: true (which implicitly restricts to the request origin). In production, neither variable being set logs a warning and blocks all cross-origin requests.
The credentials: true CORS option (which allows cookies and Authorization headers to be sent cross-origin) is only enabled for API routes (/api/* except /api/health). Static paths and the health endpoint use credentials: false.
This means:
- Auth routes (
/api/auth/*,/api/v1/auth/refresh) support credentialed requests β browsers will send therefreshTokencookie and JWT header. - Non-credentialed routes cannot be widened to
*whilecredentials: trueis active (this is a browser security constraint, not an application limit).
Preflight responses are cached by the browser for 600 seconds (10 minutes). This reduces OPTIONS request overhead for clients that make many cross-origin calls.
# Allow all origins β convenient for dev tools and local frontends
ALLOWED_ORIGINS=*
# or leave unset (same effect in development)ALLOWED_ORIGINS=https://staging.stellarkraal.example.com,https://app.stellarkraal.example.comOr use the single-origin fallback:
FRONTEND_URL=https://staging.stellarkraal.example.com# Must be HTTPS; wildcard is rejected
ALLOWED_ORIGINS=https://app.stellarkraal.example.comIf you deploy multiple frontend domains (e.g. A/B environments, white-label), list them all in ALLOWED_ORIGINS.
- Check that
ALLOWED_ORIGINSorFRONTEND_URLis set and matches the origin in the request exactly (scheme + hostname + port). - Check the startup logs β an invalid pattern causes an immediate crash before any request is served.
- Confirm the request origin is not using HTTP when only HTTPS is listed.
A wildcard * was used on a route that sends credentials (Authorization header or cookie). Set a specific origin in ALLOWED_ORIGINS instead.
The preflight hits the same CORS middleware. Ensure the origin is in the allowed list and that the requested method/headers are standard (Content-Type, Authorization).
# Confirm what the process sees at startup
docker compose logs backend | grep -i "cors\|ALLOWED_ORIGINS\|FRONTEND_URL"- Middleware implementation:
backend/src/middleware/cors.ts ALLOWED_ORIGINSformat and examples:ALLOWED_ORIGINS/README.md- All env var defaults and validation:
backend/src/config.ts - Troubleshooting CORS runtime errors:
docs/troubleshooting.md