# Access Control

> Source: `src/content/docs/actors/access-control.mdx`
> Canonical URL: https://rivet.dev/docs/actors/access-control
> Description: 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](/docs/actors/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`.

```ts
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.

_Source doc path: /docs/actors/access-control_
