Authentication
Secure your actors with authentication and authorization.
Do You Need Authentication?
Actors are private by default on Rivet Cloud. Only requests with the publishable token can interact with actors.
- Backend-only actors: If your publishable token is only included in your backend, then authentication is not necessary.
- Frontend-accessible actors: If your publishable token is included in your frontend, then implementing authentication is recommended.
Actors are public by default on self-hosted Rivet. Anyone can access them without a token.
- Only accessible within private network: If Rivet is only accessible within your private network, then authentication is not necessary.
- Rivet exposed to the public internet: If Rivet is configured to accept traffic from the public internet, then implementing authentication is recommended.
Authentication Connections
Authentication is configured through either:
onBeforeConnectfor simple pass/fail validationcreateConnStatewhen you need to access user data in your actions viac.conn.state
Access Control
After a connection is authenticated, use Access Control to enforce authorization:
- Check permissions in action handlers.
- Use
queues.<name>.canPublishto gate inbound queue publishes. - Use
events.<name>.canSubscribeto gate event subscriptions.
onBeforeConnect
The onBeforeConnect hook validates credentials before allowing a connection. Throw an error to reject the connection.
import { actor, UserError } from "rivetkit";
interface ConnParams {
authToken: string;
}
// Example token validation function
async function validateToken(token: string, roomKey: string[]): Promise<boolean> {
// In production, verify JWT or call auth service
return token.length > 0 && roomKey.length > 0;
}
interface Message {
text: string;
timestamp: number;
}
const chatRoom = actor({
state: { messages: [] as Message[] },
onBeforeConnect: async (c, params: ConnParams) => {
const roomName = c.key;
const isValid = await validateToken(params.authToken, roomName);
if (!isValid) {
throw new UserError("Forbidden", { code: "forbidden" });
}
},
actions: {
sendMessage: (c, text: string) => {
c.state.messages.push({ text, timestamp: Date.now() });
},
},
});
createConnState
Use createConnState to extract user data from credentials and store it in connection state. This data is accessible in actions via c.conn.state. Like onBeforeConnect, throwing an error will reject the connection. See connections for more details.
import { actor, UserError } from "rivetkit";
interface ConnParams {
authToken: string;
}
interface ConnState {
userId: string;
role: string;
}
interface Message {
userId: string;
text: string;
timestamp: number;
}
// Example token validation function
async function validateToken(token: string, roomKey: string[]): Promise<{ sub: string; role: string } | null> {
// In production, verify JWT or call auth service
if (token.length > 0 && roomKey.length > 0) {
return { sub: "user-123", role: "member" };
}
return null;
}
const chatRoom = actor({
state: { messages: [] as Message[] },
createConnState: async (c, params: ConnParams): Promise<ConnState> => {
const roomName = c.key;
const payload = await validateToken(params.authToken, roomName);
if (!payload) {
throw new UserError("Forbidden", { code: "forbidden" });
}
return {
userId: payload.sub,
role: payload.role,
};
},
actions: {
sendMessage: (c, text: string) => {
// Access user data via c.conn.state
const { userId, role } = c.conn.state;
if (role !== "member") {
throw new UserError("Insufficient permissions", { code: "insufficient_permissions" });
}
c.state.messages.push({ userId, text, timestamp: Date.now() });
c.broadcast("newMessage", { userId, text });
},
},
});
Available Auth Data
Authentication hooks have access to several properties:
| Property | Description |
|---|---|
params | Custom data passed by the client when connecting (see connection params) |
c.request | The underlying HTTP request object |
c.request.headers | Request headers for tokens, API keys (does not work for .connect()) |
c.state | Actor state for authorization decisions (see state) |
c.key | The actor’s key (see keys) |
It’s recommended to use params instead of c.request.headers whenever possible since it works for both HTTP & WebSocket connections.
Client Usage
Passing Credentials
Pass authentication data when connecting:
import { createClient } from "rivetkit/client";
const client = createClient();
const chat = client.chatRoom.getOrCreate(["general"], {
params: { authToken: "jwt-token-here" },
});
// Authentication will happen on connect by reading connection parameters
const connection = chat.connect();
import { createClient } from "rivetkit/client";
const client = createClient();
const chat = client.chatRoom.getOrCreate(["general"], {
params: { authToken: "jwt-token-here" },
});
// Authentication will happen when calling the action by reading input
// parameters
await chat.sendMessage("Hello, world!");
import { createClient } from "rivetkit/client";
// This only works for stateless actions, not WebSockets
const client = createClient({
headers: {
Authorization: "Bearer my-token",
},
});
const chat = client.chatRoom.getOrCreate(["general"]);
// Authentication will happen when calling the action by reading headers
await chat.sendMessage("Hello, world!");
Handling Errors
Authentication errors use the same system as regular errors. See errors for more details.
import { actor, setup } from "rivetkit";
import { ActorError, createClient } from "rivetkit/client";
// Define actor with protected action
const myActor = actor({
state: {},
actions: {
protectedAction: (c) => ({ success: true })
}
});
const registry = setup({ use: { myActor } });
const client = createClient<typeof registry>();
const actorHandle = await client.myActor.getOrCreate();
// Helper to show errors
function showError(message: string) {
console.error(message);
}
const conn = actorHandle.connect();
conn.on("error", (error: ActorError) => {
if (error.code === "forbidden") {
window.location.href = "/login";
} else if (error.code === "insufficient_permissions") {
showError("You don't have permission for this action");
}
});
import { actor, setup } from "rivetkit";
import { ActorError, createClient } from "rivetkit/client";
// Define actor with protected action
const myActor = actor({
state: {},
actions: {
protectedAction: (c) => ({ success: true })
}
});
const registry = setup({ use: { myActor } });
const client = createClient<typeof registry>();
const actorHandle = await client.myActor.getOrCreate();
// Helper to show errors
function showError(message: string) {
console.error(message);
}
try {
const result = await actorHandle.protectedAction();
} catch (error) {
if (error instanceof ActorError && error.code === "forbidden") {
window.location.href = "/login";
} else if (error instanceof ActorError && error.code === "insufficient_permissions") {
showError("You don't have permission for this action");
}
}
Examples
JWT
Validate JSON Web Tokens and extract user claims:
import { actor, UserError } from "rivetkit";
interface ConnParams {
token: string;
}
interface ConnState {
userId: string;
role: string;
permissions: string[];
}
interface JwtPayload {
sub: string;
role: string;
permissions?: string[];
}
// Example JWT verification function - in production use a JWT library
function verifyJwt(token: string, secret: string): JwtPayload {
// This is a simplified example - use jsonwebtoken or similar in production
const parts = token.split(".");
if (parts.length !== 3) throw new Error("Invalid token");
const payload = JSON.parse(atob(parts[1])) as JwtPayload;
return payload;
}
const jwtActor = actor({
state: {},
createConnState: (c, params: ConnParams): ConnState => {
try {
const payload = verifyJwt(params.token, process.env.JWT_SECRET || "secret");
return {
userId: payload.sub,
role: payload.role,
permissions: payload.permissions || [],
};
} catch {
throw new UserError("Invalid or expired token", { code: "invalid_token" });
}
},
actions: {
protectedAction: (c) => {
if (!c.conn.state.permissions.includes("write")) {
throw new UserError("Write permission required", { code: "forbidden" });
}
return { success: true };
},
},
});
External Auth Provider
Validate credentials against an external authentication service:
import { actor, UserError } from "rivetkit";
interface ConnParams {
apiKey: string;
}
interface ConnState {
userId: string;
tier: string;
}
const apiActor = actor({
state: {},
createConnState: async (c, params: ConnParams): Promise<ConnState> => {
const response = await fetch(`https://api.my-auth-provider.com/validate`, {
method: "POST",
headers: { "X-API-Key": params.apiKey },
});
if (!response.ok) {
throw new UserError("Invalid API key", { code: "invalid_api_key" });
}
const data = await response.json();
return { userId: data.id, tier: data.tier };
},
actions: {
premiumAction: (c) => {
if (c.conn.state.tier !== "premium") {
throw new UserError("Premium subscription required", { code: "forbidden" });
}
return "Premium content";
},
},
});
Using c.state In Authorization
Access actor state via c.state and the actor’s key via c.key to make authorization decisions:
import { actor, UserError } from "rivetkit";
interface ConnParams {
userId?: string;
}
const userProfile = actor({
state: {
ownerId: "user-123",
isPrivate: true,
},
onBeforeConnect: (c, params: ConnParams) => {
// Use actor state to check access permissions
if (c.state.isPrivate && params.userId !== c.state.ownerId) {
throw new UserError("Access denied to private profile", { code: "forbidden" });
}
},
actions: {
getProfile: (c) => ({ ownerId: c.state.ownerId }),
},
});
Role-Based Access Control
Create helper functions for common authorization patterns:
import { actor, UserError } from "rivetkit";
const ROLE_HIERARCHY = { user: 1, moderator: 2, admin: 3 };
interface ConnState {
role: keyof typeof ROLE_HIERARCHY;
permissions: string[];
}
// Example token validation function
async function validateToken(token: string): Promise<{ role: keyof typeof ROLE_HIERARCHY; permissions: string[] }> {
// In production, verify JWT or call auth service
return { role: "user", permissions: ["read", "edit_posts"] };
}
function requireRole(requiredRole: keyof typeof ROLE_HIERARCHY) {
return (c: { conn: { state: ConnState } }) => {
const userRole = c.conn.state.role;
if (ROLE_HIERARCHY[userRole] < ROLE_HIERARCHY[requiredRole]) {
throw new UserError(`${requiredRole} role required`, { code: "forbidden" });
}
};
}
function requirePermission(permission: string) {
return (c: { conn: { state: ConnState } }) => {
if (!c.conn.state.permissions?.includes(permission)) {
throw new UserError(`Permission '${permission}' required`, { code: "forbidden" });
}
};
}
const forumActor = actor({
state: {},
createConnState: async (c, params: { token: string }): Promise<ConnState> => {
const user = await validateToken(params.token);
return { role: user.role, permissions: user.permissions };
},
actions: {
deletePost: (c, postId: string) => {
requireRole("moderator")(c);
// Delete post...
},
editPost: (c, postId: string, content: string) => {
requirePermission("edit_posts")(c);
// Edit post...
},
},
});
Rate Limiting
Use c.vars to track connection attempts and rate limit by user:
import { actor, UserError } from "rivetkit";
interface ConnParams {
authToken: string;
}
interface RateLimitEntry {
count: number;
resetAt: number;
}
// Example token validation function
async function validateToken(token: string): Promise<{ userId: string }> {
// In production, verify JWT or call auth service
return { userId: "user-123" };
}
const rateLimitedActor = actor({
state: {},
createVars: () => ({ rateLimits: {} as Record<string, RateLimitEntry> }),
onBeforeConnect: async (c, params: ConnParams) => {
// Extract user ID
const { userId } = await validateToken(params.authToken);
// Check rate limit
const now = Date.now();
const limit = c.vars.rateLimits[userId];
if (limit && limit.resetAt > now && limit.count >= 10) {
throw new UserError("Too many requests, try again later", { code: "rate_limited" });
}
// Update rate limit
if (!limit || limit.resetAt <= now) {
c.vars.rateLimits[userId] = { count: 1, resetAt: now + 60_000 };
} else {
limit.count++;
}
},
actions: {
getData: (c) => ({ success: true }),
},
});
The limits in this example are ephemeral. If you wish to persist rate limits, you can optionally replace vars with state.
Caching Tokens
Cache validated tokens in c.vars to avoid redundant validation on repeated connections. See ephemeral variables for more details.
import { actor, UserError } from "rivetkit";
interface ConnParams {
authToken: string;
}
interface ConnState {
userId: string;
role: string;
}
interface TokenCache {
[token: string]: {
userId: string;
role: string;
expiresAt: number;
};
}
// Example token validation function
async function validateToken(token: string): Promise<{ sub: string; role: string } | null> {
// In production, verify JWT or call auth service
if (token.length > 0) {
return { sub: "user-123", role: "member" };
}
return null;
}
const cachedAuthActor = actor({
state: {},
createVars: () => ({ tokenCache: {} as TokenCache }),
createConnState: async (c, params: ConnParams): Promise<ConnState> => {
const token = params.authToken;
// Check cache first
const cached = c.vars.tokenCache[token];
if (cached && cached.expiresAt > Date.now()) {
return { userId: cached.userId, role: cached.role };
}
// Validate token (expensive operation)
const payload = await validateToken(token);
if (!payload) {
throw new UserError("Invalid token", { code: "invalid_token" });
}
// Cache the result
c.vars.tokenCache[token] = {
userId: payload.sub,
role: payload.role,
expiresAt: Date.now() + 5 * 60 * 1000, // 5 minutes
};
return { userId: payload.sub, role: payload.role };
},
actions: {
getData: (c) => ({ userId: c.conn.state.userId }),
},
});
API Reference
AuthIntent- Authentication intent typeBeforeConnectContext- Context for auth checksConnectContext- Context after connection