CORS configuration
cyoda-go version 0.7.1
config.cors
Section titled “config.cors”config.cors — Cross-Origin Resource Sharing (CORS) controls for the public HTTP surface.
SYNOPSIS
Section titled “SYNOPSIS”cyoda supports four CORS modes: disabled, loopback (default), wildcard, and allowlist.
Configure the mode via CYODA_CORS_ENABLED and CYODA_CORS_ALLOWED_ORIGINS.
OPTIONS
Section titled “OPTIONS”CYODA_CORS_ENABLED— enable CORS middleware; set tofalseto disable and handle CORS at an upstream ingress/proxy layer (default:true)CYODA_CORS_ALLOWED_ORIGINS— comma-separated list of allowed origins, or*for wildcard mode (default: empty — loopback mode)
The effective mode is determined by the combination of the two env vars:
CYODA_CORS_ENABLED=false— disabled (regardless ofCYODA_CORS_ALLOWED_ORIGINS)CYODA_CORS_ENABLED=true,CYODA_CORS_ALLOWED_ORIGINSempty — loopback (default)CYODA_CORS_ENABLED=true,CYODA_CORS_ALLOWED_ORIGINS=*— wildcardCYODA_CORS_ENABLED=true,CYODA_CORS_ALLOWED_ORIGINS=https://example.com,...— allowlist
disabled
Section titled “disabled”CORS middleware is not installed. No Access-Control-* headers are emitted. OPTIONS requests
return chi’s default 405. Use this when CORS is handled at your ingress layer (nginx, Envoy,
cloud load balancer).
loopback (default)
Section titled “loopback (default)”Only loopback origins are permitted: http(s)://localhost, http(s)://127.0.0.1, and
http(s)://[::1] on any port. Suitable for local development. Set
CYODA_CORS_ALLOWED_ORIGINS to permit additional origins.
wildcard
Section titled “wildcard”Access-Control-Allow-Origin: * is emitted for all cross-origin requests. Credentials
(cookies, Authorization header) cannot be used with wildcard mode. Appropriate only for
fully public, stateless read APIs.
allowlist
Section titled “allowlist”Only the origins listed in CYODA_CORS_ALLOWED_ORIGINS are permitted. Exact scheme+host+port
matching (no wildcards in individual entries). Origins must be absolute URIs with scheme and
host; paths and query strings are not permitted.
BEHAVIOUR
Section titled “BEHAVIOUR”The following headers are emitted by the CORS middleware when it is installed
(CYODA_CORS_ENABLED=true):
On every response from the installed middleware (preflight, CORS request, or no-Origin pass-through):
Vary: Origin— always appended (never overwrites an existingVaryvalue). This instructs intermediate caches to key byOriginso that a mode change does not cause a stale no-Originresponse to be served to anOrigin-bearing request.
Access-Control-Allow-Origin:
- loopback mode: the matched origin is echoed literally; omitted if no match.
- allowlist mode: the matched origin is echoed literally; omitted if no match.
- wildcard mode: literal
*for every request, never reflective ofOrigin. - disabled mode: not emitted.
On preflight responses only (OPTIONS with Origin and
Access-Control-Request-Method):
Access-Control-Allow-Methods: GET, POST, PUT, PATCH, DELETE, OPTIONS(static)Access-Control-Allow-Headers: Authorization, Content-Type, traceparent, tracestate(static)Access-Control-Max-Age: 86400(static)
These three headers are emitted on every preflight regardless of whether
the origin matched the policy. Only Access-Control-Allow-Origin is
omitted when the origin is rejected — a deployer debugging an allowlist
miss will see the static three present alongside the absent ACAO.
Access-Control-Allow-Credentials is NOT emitted in v1. Authentication is
bearer-in-Authorization; cookies and HTTP-auth are not used. Credentials mode
adds attack surface without functional benefit for this auth model.
TENANT ISOLATION
Section titled “TENANT ISOLATION”CORS is a browser-side defence against unauthorized cross-origin reads. It is not a tenant-isolation control. JWT claims and per-request authorization checks in the data path enforce tenant boundaries. An allowlisted SPA serving multiple tenants relies entirely on the auth layer, not CORS, to prevent cross-tenant access. No CORS rule substitutes for or displaces JWT-based authz.
DEPLOYMENT
Section titled “DEPLOYMENT”Local dev / docker compose
No configuration needed. Loopback mode allows http://localhost, http://127.0.0.1,
and http://[::1] on any port by default. Suitable for Vite/webpack dev servers
and local docker-compose SPAs.
Behind an ingress that handles its own CORS
Set CYODA_CORS_ENABLED=false and configure CORS at the ingress (nginx, Envoy,
cloud load balancer). Do not let both the ingress and cyoda-go emit
Access-Control-Allow-Origin: a browser receiving two Access-Control-Allow-Origin
values will reject the response.
Behind a reverse proxy with no CORS handling
Set CYODA_CORS_ALLOWED_ORIGINS=https://your.spa.host. The proxy forwards
requests unchanged; cyoda-go’s allowlist middleware emits the correct header
for the matching origin.
PNA AND CSRF
Section titled “PNA AND CSRF”Private Network Access (PNA): cyoda-go does not handle
Access-Control-Request-Private-Network / Access-Control-Allow-Private-Network.
Deployers needing browsers on a public origin to reach cyoda-go on a private
network should configure PNA at the ingress.
CSRF: CSRF is not a threat for bearer-in-header authentication. The SPA explicitly attaches the bearer on each request rather than relying on ambient credentials. No anti-CSRF token is required or provided.
TOGGLING CORS_ENABLED
Section titled “TOGGLING CORS_ENABLED”Toggling CYODA_CORS_ENABLED between true and false requires a
downstream-cache flush. Responses cached during the disabled period lack
Vary: Origin and could be served to origins for which the post-toggle policy
disagrees.
TROUBLESHOOTING
Section titled “TROUBLESHOOTING”-
Browser logs a CORS error but the service logs the request as
200— the origin was rejected by the allowlist. The middleware omitsAccess-Control-Allow-Originand the browser blocks reading the body. Add the origin toCYODA_CORS_ALLOWED_ORIGINSwith exact scheme+host+port. -
Multi-valued
Access-Control-Allow-Origin— both the ingress and cyoda-go are emitting the header. SetCYODA_CORS_ENABLED=falseand handle CORS entirely at the ingress. -
Startup failure:
cors: origin "..." has non-ASCII host; convert to punycode— IDN host names must be supplied in punycode form (xn--...). -
Startup failure: default port rejected — drop the port from the origin: use
https://example.com, nothttps://example.com:443; usehttp://example.com, nothttp://example.com:80. -
Startup WARN about wildcard mode — if wildcard is unintended, set a specific allowlist with
CYODA_CORS_ALLOWED_ORIGINS=https://your.app.host. -
Local SPA on
file://cannot reach cyoda-go —file://producesOrigin: null, which is not auto-allowed in any mode (in wildcard mode,nullreceivesAccess-Control-Allow-Origin: *, which browsers honour for non-credentialed requests). Serve the SPA via a local HTTP server (e.g.python3 -m http.server) so a normalhttp://localhostorigin is used instead.
EXAMPLES
Section titled “EXAMPLES”Loopback only (local dev, default):
# nothing to set — loopback is the default when CYODA_CORS_ENABLED=trueSingle production origin:
CYODA_CORS_ENABLED=trueCYODA_CORS_ALLOWED_ORIGINS=https://app.example.comMultiple origins:
CYODA_CORS_ENABLED=trueCYODA_CORS_ALLOWED_ORIGINS=https://app.example.com,https://admin.example.comWildcard (public read API):
CYODA_CORS_ENABLED=trueCYODA_CORS_ALLOWED_ORIGINS=*Disabled (CORS at ingress):
CYODA_CORS_ENABLED=falseSEE ALSO
Section titled “SEE ALSO”- config
- run
See also
Section titled “See also”cyoda help config— Environment variables beat default values. The_FILEsuffix variant takes precedence over the plain variable when both are set — for example,CYODA_POSTGRES_URL_FILE=/etc/secrets/db-urlwins overCYODA_POSTGRES_URL. There are no command-line flags for configuration values; env vars are the sole configuration surface.cyoda help run— cyoda-go is a single-process, multi-tenant REST and gRPC API server. It starts in serving mode when invoked with no subcommand. All configuration is via environment variables with aCYODA_prefix. The binary, Docker image, and Helm chart run the same binary; only the environment configuration differs across run modes.
Raw formats
Section titled “Raw formats”/help/config/cors.json— full descriptor (matchesGET /help/{topic}envelope)/help/config/cors.md— body only