Browse all documentation

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:

terminal
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:

terminal
RIDU_ALLOWED_ORIGINS='http://localhost:5173' ridu dev

ridu 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:

cmd/server/main.go
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:

cmd/server/main.go
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:

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:

browser.ts
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:

terminal
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.