Sign In
Communication

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.

Authentication Connections

Authentication is configured through either:

  • onBeforeConnect for simple pass/fail validation
  • createConnState when you need to access user data in your actions via c.conn.state

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;
}

const chatRoom = actor({
  state: { messages: [] },

  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() });
    },
  },
});
TypeScript

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;
}

const chatRoom = actor({
  state: { messages: [] },

  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 });
    },
  },
});
TypeScript

Available Auth Data

Authentication hooks have access to several properties:

PropertyDescription
paramsCustom data passed by the client when connecting (see connection params)
c.requestThe underlying HTTP request object
c.request.headersRequest headers for tokens, API keys (does not work for .connect())
c.stateActor state for authorization decisions (see state)
c.keyThe 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();

Handling Errors

Authentication errors use the same system as regular errors. See errors for more details.

import { ActorError } from "rivetkit/client";

const conn = actor.connect();
conn.onError((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");
  }
});

Examples

JWT

Validate JSON Web Tokens and extract user claims:

import { actor, UserError } from "rivetkit";
import jwt from "jsonwebtoken";

interface ConnParams {
  token: string;
}

interface ConnState {
  userId: string;
  role: string;
  permissions: string[];
}

const jwtActor = actor({
  state: {},

  createConnState: (c, params: ConnParams): ConnState => {
    try {
      const payload = jwt.verify(params.token, process.env.JWT_SECRET!) as any;
      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 };
    },
  },
});
TypeScript

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";
    },
  },
});
TypeScript

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 }),
  },
});
TypeScript

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[];
}

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...
    },
  },
});
TypeScript

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;
}

const rateLimitedActor = actor({
  state: {},
  vars: { 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 }),
  },
});
TypeScript

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;
  };
}

const cachedAuthActor = actor({
  state: {},
  vars: { 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 }),
  },
});
TypeScript

API Reference

Suggest changes to this page