website/src/content/docs/actors/authentication.mdx
- **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.
- **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 is configured through either:
onBeforeConnect for simple pass/fail validationcreateConnState when you need to access user data in your actions via c.conn.stateAfter a connection is authenticated, use Access Control to enforce authorization:
queues.<name>.canPublish to gate inbound queue publishes.events.<name>.canSubscribe to gate event subscriptions.onBeforeConnectThe 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() });
},
},
});
createConnStateUse 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 });
},
},
});
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.
Pass authentication data when connecting. Use getParams when you need a fresh JWT for every connection or reconnect:
async function getAuthToken(): Promise<string> { return "jwt-token-here"; }
const client = createClient(); const chat = client.chatRoom.getOrCreate(["general"], { getParams: async () => ({ authToken: await getAuthToken(), }), });
// Authentication will happen on connect by reading connection parameters const connection = chat.connect();
```typescript Stateless-Action
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!");
Authentication errors use the same system as regular errors. See errors for more details.
<CodeGroup> ```typescript Connection 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>("http://localhost:6420"); 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"); } });
```typescript Stateless-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>("http://localhost:6420");
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");
}
}
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 };
},
},
});
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";
},
},
});
c.state In AuthorizationAccess 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 }),
},
});
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...
},
},
});
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.
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 }),
},
});
AuthIntent - Authentication intent typeBeforeConnectContext - Context for auth checksConnectContext - Context after connection