Skip to main content
Sign In
Communication & Networking

Access Control

Authorize actions, queue publishes, and event subscriptions with explicit hooks.

Use access control to decide what authenticated clients are allowed to do.

This is authorization, not authentication:

  • Use authentication to identify who is calling.
  • Use access-control rules to decide what they can do after connecting.

Permission Surfaces

RivetKit authorization is explicit per surface:

  • onBeforeConnect rejects unauthenticated or malformed connections.
  • Action handlers (actions.*) enforce action permissions.
  • queues.<name>.canPublish allows or denies inbound queue publishes.
  • events.<name>.canSubscribe allows or denies event subscriptions.

Fail By Default

Use deny-by-default rules everywhere:

  1. Keep onBeforeConnect strict and reject invalid credentials.
  2. In each action, explicitly allow expected roles and throw forbidden otherwise.
  3. In canPublish and canSubscribe, return true only for allowed roles and end with return false.
import { actor, event, queue, UserError } from "rivetkit";

type ConnParams = {
  authToken: string;
};

type ConnState = {
  userId: string;
  role: "member" | "admin";
};

async function authenticate(
  authToken: string,
): Promise<ConnState | null> {
  if (authToken === "admin-token") {
    return { userId: "admin-1", role: "admin" };
  }
  if (authToken === "member-token") {
    return { userId: "member-1", role: "member" };
  }
  return null;
}

export const chatRoom = actor({
  state: { messages: [] as Array<{ userId: string; text: string }> },

  onBeforeConnect: async (_c, params: ConnParams) => {
    if (!params.authToken) {
      throw new UserError("Forbidden", { code: "forbidden" });
    }

    const session = await authenticate(params.authToken);
    if (!session) {
      throw new UserError("Forbidden", { code: "forbidden" });
    }
  },

  createConnState: async (_c, params: ConnParams): Promise<ConnState> => {
    const session = await authenticate(params.authToken);
    if (!session) {
      throw new UserError("Forbidden", { code: "forbidden" });
    }
    return session;
  },

  events: {
    messages: event<{ userId: string; text: string }>(),
    moderationLog: event<{ entry: string }>({
      canSubscribe: (c) => {
        if (c.conn?.state.role === "admin") {
          return true;
        }
        return false;
      },
    }),
  },

  queues: {
    moderationJobs: queue<{ action: "ban"; userId: string }>({
      canPublish: (c) => {
        if (c.conn?.state.role === "admin") {
          return true;
        }
        return false;
      },
    }),
  },

  actions: {
    sendMessage: (c, text: string) => {
      const role = c.conn?.state.role;
      const userId = c.conn?.state.userId;

      if (!userId || (role !== "member" && role !== "admin")) {
        throw new UserError("Forbidden", { code: "forbidden" });
      }

      const message = { userId, text };
      c.state.messages.push(message);
      c.broadcast("messages", message);
    },
  },
});

Return Value Contract

canPublish and canSubscribe must return a boolean:

  • true: allow
  • false: deny with forbidden

Returning undefined, null, or any non-boolean throws an internal error.

Notes

  • canPublish only applies to queue names defined in queues.
  • Incoming queue messages for undefined queues are ignored and logged as warnings.
  • canSubscribe only applies to event names defined in events.
  • Broadcasting an event not defined in events logs a warning but still publishes.