Cross-Origin Resource Sharing (CORS) is a security mechanism that allows a web application running at one origin to access resources from a different origin. Without CORS, browsers block cross-origin HTTP requests by default as a security measure.
You’ll need to configure CORS when:
- Local Development: You’re developing locally and your client runs on a different port than your actor service
- Different Domain: Your frontend application is hosted on a different domain than your actor service
Registry-Level CORS
Configure CORS directly in your registry setup for simple cases:
import { setup } from "@rivetkit/actor";
import counter from "./counter";
const registry = setup({
use: { counter },
cors: {
origin: "https://yourdomain.com",
credentials: true
}
});
This approach works well for basic setups but has limitations for complex scenarios.
Router-Level CORS (Recommended)
For production applications, configure CORS at the router level for maximum control:
import { registry } from "./registry";
import { Hono } from "hono";
import { cors } from "hono/cors";
import { ALLOWED_PUBLIC_HEADERS } from "@rivetkit/actor";
const { serve } = registry.createServer();
const app = new Hono();
app.use("*", cors({
origin: ["http://localhost:3000", "https://myapp.com"],
allowHeaders: [
"Authorization",
"Content-Type",
...ALLOWED_PUBLIC_HEADERS
],
allowMethods: ["POST", "GET", "OPTIONS"],
exposeHeaders: ["Content-Length"],
maxAge: 600,
credentials: true,
}));
serve(app);
RivetKit requires specific headers for communication. Always include ALLOWED_PUBLIC_HEADERS
:
import { ALLOWED_PUBLIC_HEADERS } from "@rivetkit/actor";
// ALLOWED_PUBLIC_HEADERS includes:
// - "Content-Type"
// - "User-Agent"
// - "X-RivetKit-Query"
// - "X-RivetKit-Encoding"
// - "X-RivetKit-Conn-Params"
// - "X-RivetKit-Actor"
// - "X-RivetKit-Conn"
// - "X-RivetKit-Conn-Token"
const corsConfig = {
allowHeaders: [
"Authorization", // For your auth tokens
...ALLOWED_PUBLIC_HEADERS // Required RivetKit headers
]
};
Without ALLOWED_PUBLIC_HEADERS
, RivetKit clients won’t be able to communicate with your actors from the browser.
Framework-Specific Examples
Express.js
import express from "express";
import cors from "cors";
import { ALLOWED_PUBLIC_HEADERS } from "@rivetkit/actor";
import { registry } from "./registry";
const { handler } = registry.createServer();
const app = express();
app.use(cors({
origin: ["http://localhost:3000", "https://myapp.com"],
allowedHeaders: [
"Authorization",
"Content-Type",
...ALLOWED_PUBLIC_HEADERS
],
credentials: true,
}));
app.use("/registry", handler);
app.listen(8080);
Elysia
import { Elysia } from "elysia";
import { cors } from "@elysiajs/cors";
import { ALLOWED_PUBLIC_HEADERS } from "@rivetkit/actor";
import { registry } from "./registry";
const { handler } = registry.createServer();
const app = new Elysia()
.use(cors({
origin: ["http://localhost:3000", "https://myapp.com"],
allowedHeaders: [
"Authorization",
"Content-Type",
...ALLOWED_PUBLIC_HEADERS
],
credentials: true,
}))
.mount("/registry", handler)
.listen(8080);
Configuration Options
origin
string | string[] | (origin: string) => boolean | string
Specifies which domains can access your resources:
// Single domain
origin: "https://example.com"
// Multiple domains
origin: ["https://app.com", "https://admin.com"]
// Dynamic validation
origin: (origin) => {
return origin?.endsWith('.example.com') ? origin : false;
}
// All domains (not recommended for production)
origin: "*"
allowMethods
string[]
HTTP methods clients are allowed to use:
allowMethods: ["GET", "POST", "OPTIONS"] // Common for RivetKit
string[]
Headers that clients can send in requests:
allowHeaders: [
"Authorization", // Your auth headers
"Content-Type", // Standard content type
"X-API-Key", // Custom API key header
...ALLOWED_PUBLIC_HEADERS // Required RivetKit headers
]
credentials
boolean
Whether to allow credentials (cookies, auth headers):
credentials: true // Required for authentication
When credentials: true
, you cannot use origin: "*"
. Specify exact origins instead.
maxAge
number
How long browsers cache CORS preflight responses (in seconds):
maxAge: 600 // Cache for 10 minutes
string[]
Server headers that browsers can access:
exposeHeaders: ["Content-Length", "X-Request-Id"]
Development vs Production
Development Setup
For local development, allow localhost origins:
const isDev = process.env.NODE_ENV !== "production";
const corsConfig = {
origin: isDev
? ["http://localhost:3000", "http://localhost:5173"]
: ["https://myapp.com"],
allowHeaders: ["Authorization", ...ALLOWED_PUBLIC_HEADERS],
credentials: true,
};
Production Setup
For production, be restrictive with origins:
const corsConfig = {
origin: [
"https://myapp.com",
"https://www.myapp.com",
"https://admin.myapp.com"
],
allowHeaders: ["Authorization", ...ALLOWED_PUBLIC_HEADERS],
credentials: true,
maxAge: 3600, // Cache for 1 hour
};
Troubleshooting
Common CORS Errors
“Access to fetch blocked by CORS policy”
- Add your frontend’s origin to the
origin
list
- Ensure
ALLOWED_PUBLIC_HEADERS
are included in allowHeaders
“Request header not allowed”
- Add the missing header to
allowHeaders
- Include
ALLOWED_PUBLIC_HEADERS
in your configuration
“Credentials mode mismatch”
- Set
credentials: true
in CORS config
- Cannot use
origin: "*"
with credentials
Debug CORS Issues
Enable CORS logging to debug issues:
// Log CORS requests in development
if (process.env.NODE_ENV === "development") {
app.use("*", async (c, next) => {
console.log("CORS request:", {
origin: c.req.header("Origin"),
method: c.req.method,
headers: Object.fromEntries(c.req.headers.entries())
});
await next();
});
}
Best Practices
- Use Router-Level CORS: More flexible than registry-level configuration
- Include ALLOWED_PUBLIC_HEADERS: Required for RivetKit communication
- Specify Exact Origins: Avoid wildcards in production
- Enable Credentials: Needed for authentication
- Cache Preflight Requests: Use appropriate
maxAge
values
- Environment-Specific Config: Different settings for dev/prod