CORS
Allow cross-origin browser clients without weakening Ridu’s HTTP boundary.
Ridu accepts same-origin browser requests without extra configuration. Configure CORS only when
JavaScript running on one origin—such as https://app.example.com—calls a Ridu API on another,
such as https://cms.example.com.
CORS is a browser boundary, not authentication or authorization. Ridu still authenticates the caller, applies access rules, validates input, and redacts fields after the browser is allowed to send the request. Server-to-server calls do not need CORS configuration.
Do I need CORS?
An origin is the exact combination of scheme, hostname, and port.
| Client arrangement | Configuration |
|---|---|
| Embedded Ridu admin calling its own API | None; it is same-origin |
| Frontend and API on the same scheme, hostname, and port | None |
| Browser frontend on a different subdomain, domain, scheme, or port | Add the frontend’s exact origin |
| Server-rendered backend or worker calling Ridu | None; authenticate the request instead |
| Live-preview page on another origin | Configure CORS and the separate preview message boundary |
Do not add the CMS origin merely because it is the request destination. AllowedOrigins contains
the origins where calling browser code runs. Ridu also accepts a request whose Origin exactly
matches the API’s effective request origin.
Configure a generated application
Generated servers read a comma-separated RIDU_ALLOWED_ORIGINS value:
export RIDU_ALLOWED_ORIGINS='https://app.example.com,https://staff.example.com'Use complete origins with no path, query, fragment, credentials, or trailing slash. Ridu
canonicalizes host casing and default ports, but it does not support * or wildcard subdomains.
List each trusted browser origin.
For local frontend development, add the exact dev-server port:
RIDU_ALLOWED_ORIGINS='http://localhost:5173' ridu devridu dev automatically allows its admin dev server. A separate application dev
server still needs its own origin. In production, set RIDU_ALLOWED_ORIGINS in the environment
that starts the Ridu binary—for example in your container, service manager, or hosting provider.
Where WithHandlerOptions goes
Projects created by ridu new
Set RIDU_ALLOWED_ORIGINS; the generated cmd/server/main.go already passes its comma-separated
values to HandlerOptions.AllowedOrigins. You do not need to edit the Go entrypoint for ordinary
origin configuration.
Custom ridu.Execute entrypoints
If the application did not come from ridu new, pass WithHandlerOptions directly to
ridu.Execute, beside WithStore and WithAddress:
func main() {
err := ridu.Execute(
content.Config(),
ridu.WithStore(func(ctx context.Context) (store.Store, error) {
return postgres.Open(ctx, os.Getenv("DATABASE_URL"))
}),
ridu.WithAddress(":8080"),
ridu.WithHandlerOptions(ridu.HandlerOptions{
AllowedOrigins: []string{
"https://app.example.com",
},
}),
)
if err != nil {
log.Fatal(err)
}
}WithHandlerOptions is an execute option, not a top-level statement or collection setting.
Application-owned HTTP servers
If you create the application with ridu.New and own http.Server yourself, do not use
WithHandlerOptions. Pass the value to app.Handler where you construct the HTTP handler:
app, err := ridu.New(content.Config(), backend)
if err != nil {
log.Fatal(err)
}
handler := app.Handler(ridu.HandlerOptions{
AllowedOrigins: []string{"https://app.example.com"},
AllowedRequestHeaders: []string{
"X-Workspace-ID",
},
})
log.Fatal(http.ListenAndServe(":8080", handler))When you construct http.Server yourself, check application readiness before accepting traffic;
ridu.Execute performs that check before binding automatically.
AllowedOrigins and AllowedHosts solve different problems. Origins identify browser callers;
hosts restrict the public hostnames accepted by the API. Do not add a hostname to one list merely
because it appears in the other.
See the complete HandlerOptions reference for request,
readiness, proxy, cookie, and worker controls.
Allow a custom request header
Skip this section unless your browser request sends a header outside Ridu’s defaults. Ridu already
allows Accept, Authorization, Content-Type, and If-Match.
For example, if the browser sends X-Workspace-ID, add AllowedRequestHeaders inside the existing
HandlerOptions block in cmd/server/main.go:
ridu.WithHandlerOptions(ridu.HandlerOptions{
AdminAssets: adminassets.FS(),
AllowedOrigins: envList("RIDU_ALLOWED_ORIGINS"),
AllowedRequestHeaders: []string{
"X-Workspace-ID",
},
AllowedHosts: envList("RIDU_ALLOWED_HOSTS"),
})Add only headers that your frontend actually sends. Invalid HTTP header names are ignored, and a
preflight asking for an unlisted header fails closed with cors_header_denied.
Cookies, tokens, and the TypeScript SDK
A frontend on a different site should use the token transport: the SDK sends
Authorization: Session <token> with credentials mode omit, so no cookie crosses sites and
blocked third-party cookies do not matter. @riducms/sveltekit configures this
for SvelteKit. Allowed responses expose X-Request-ID and Content-Disposition to the page.
Without a token store, @riducms/sdk defaults to Fetch credentials mode include, so allowed
cross-origin responses carry Access-Control-Allow-Credentials: true and echo the exact allowed
origin. Plain Fetch calls must opt into credentials when they use Ridu’s session cookie:
const response = await fetch('https://cms.example.com/api/auth/me', {
credentials: 'include'
});Set SecureCookies in production; ridu.Execute does so unless disabled. The session
cookie is SameSite=Lax. Separate origins that remain on the same site—for example two HTTPS
subdomains—can use the cookie flow. A genuinely cross-site application uses the token transport;
CORS alone cannot make the browser send an ineligible cookie.
Preflights and allowed methods
Ridu answers a valid preflight with status 204, credentialed origin headers, and the allowed
methods and headers. Browsers may cache the answer for ten minutes (Access-Control-Max-Age: 600),
so a token client’s writes usually cost one request. You can test the boundary independently of
application code:
curl -i -X OPTIONS 'https://cms.example.com/api/schema' \
-H 'Origin: https://app.example.com' \
-H 'Access-Control-Request-Method: GET' \
-H 'Access-Control-Request-Headers: Authorization, Content-Type'The browser allow-list is GET, HEAD, POST, PUT, PATCH, DELETE, and OPTIONS. That
includes cross-origin preference writes at PUT /api/preferences/{key} when the origin, requested
headers, authentication cookie, and ordinary route authorization all pass.
Proxies and HTTPS
Same-origin comparison uses the API’s effective scheme and host. When TLS terminates at a reverse
proxy you run, configure only that proxy’s immediate network in TrustedProxyCIDRs (or
RIDU_TRUSTED_PROXY_CIDRS). Ridu trusts forwarded scheme and client-address headers only from
those networks, and follows the scheme they report.
A hosting platform’s edge, such as Railway’s, terminates TLS at addresses you can’t list, so the
Go process sees plain HTTP. Ridu still accepts the HTTPS origin of the host the request names: a
request for cms.example.com may come from https://cms.example.com, because only a page on that
host sends that origin. No other origin is accepted this way, a forwarded scheme header from an
untrusted address is still ignored, and an HTTP origin on an HTTPS request is refused. Keep
AllowedHosts aligned with the public API hostname as a separate host-header defense.
When the platform’s proxies that connect to Ridu have addresses you can list, trust them and set
RIDU_TRUSTED_PROXY_HOPS to count the edge in front of them. Ridu then follows the scheme the
proxies report and sees each visitor’s address. The
Railway guide shows this setup.
Diagnose a blocked request
Use the browser network panel to inspect the preflight and the API response, then match the stable Ridu error:
| Error | Meaning |
|---|---|
origin_denied |
The origin is malformed, not listed, or differs from the effective scheme/host |
cors_method_denied |
The preflight requested a method outside the browser allow-list |
cors_header_denied |
The preflight requested an unlisted or invalid header |
Ridu logs the first refusal of each origin, with request_origin, the origin it computed for the
request; compare the two. Common mistakes are including a trailing slash, allowing the API rather
than the browser origin, forgetting a local dev-server port, adding a custom header only on the
frontend, or a trusted proxy that reports the wrong scheme. A failed GET or HEAD may appear only as a browser CORS error because
Ridu withholds the allow-origin response header; mutating requests and rejected preflights also
return a structured 403.
For session-specific failures, continue with Authentication. For preview
iframes and postMessage, also follow Live preview: its
origin, source-window, and channel checks are separate from CORS.