Skip to main content
Guides

Authenticate Users End to End

Wire a login endpoint to Rivet Actors: authenticate the user, mint a scoped JWT on your backend, connect from the browser, and authorize actions inside the actor.

This guide connects both halves of Rivet authentication in one working flow. A user logs in, your backend mints a JWT that reaches only that user’s actor, the browser connects with it, and the actor authorizes what the user may do once connected.

Read Authentication first for the two-layer model. This guide is the runnable version of it.

What You Are Building

A per-user profile actor. Alice can reach user:alice and nothing else, enforced by the control plane before her request touches your code. Inside the actor, an admin flag decides who may edit.

Steps

Keep the admin token on the backend

Only your server holds a credential that can mint tokens. Set it once:

export RIVET_ENDPOINT="http://localhost:6420"
export RIVET_NAMESPACE="production"
export RIVET_ADMIN_TOKEN="$(openssl rand -hex 32)"

The admin token reaches every actor in every namespace, so it never leaves your backend. See Configuration.

Mint a scoped token at login

Authenticate the user with your existing session system, resolve the actor they own, then request a JWT that grants actor_gateway read on that single actor ID.

Resolving the ID on the backend is what makes the grant tight. The client never names an actor, so it cannot ask for someone else’s.

Connect from the browser

Give the client getToken instead of a static token. RivetKit calls it when it needs a credential and again when one expires, so a 15 minute token supports a connection that stays open for hours.

import { createClient } from "rivetkit/client";
import type { registry } from "./registry";

// Calls your own backend, never the Rivet control plane directly.
async function fetchToken(): Promise<{ actorId: string; token: string }> {
	const response = await fetch("/token", {
		method: "POST",
		cache: "no-store",
	});
	if (!response.ok) throw new Error("could not get a Rivet token");
	return (await response.json()) as { actorId: string; token: string };
}

const { actorId } = await fetchToken();

const client = createClient<typeof registry>({
	endpoint: "https://api.rivet.dev",
	namespace: "production",

	// Called whenever RivetKit needs a credential, including after one expires.
	getToken: async () => (await fetchToken()).token,
});

const profile = client.userProfile.getForId(actorId);
const conn = profile.connect();

await conn.recordVisit();

Authorize inside the actor

The token decides which actor Alice reaches. It cannot decide what she may do there, because the control plane does not know what your actions mean. Check that in the actor.

import { actor, setup, UserError } from "rivetkit";

interface ConnParams {
	authToken: string;
}

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

// Replace this with your session store or auth provider.
async function verifySession(authToken: string): Promise<ConnState | null> {
	if (authToken === "admin-token") return { userId: "u_1", role: "admin" };
	if (authToken === "member-token") return { userId: "u_2", role: "member" };
	return null;
}

export const document = actor({
	state: { body: "" },

	// 1. Identify the caller once, at connect time.
	createConnState: async (_c, params: ConnParams): Promise<ConnState> => {
		const session = await verifySession(params.authToken);
		if (!session) {
			throw new UserError("Invalid token", { code: "invalid_token" });
		}
		return session;
	},

	actions: {
		read: (c) => c.state.body,

		// 2. Gate the operation on the identity you established.
		edit: (c, body: string) => {
			if (c.conn.state.role !== "admin") {
				throw new UserError("Admins only", { code: "forbidden" });
			}
			c.state.body = body;
		},
	},
});

export const registry = setup({ use: { document } });

Confirm both layers reject

Point the token at another actor and the control plane refuses before your code runs:

HTTP/1.1 403 Forbidden
x-rivet-error: auth.insufficient_permissions

Call an admin-only action as a member and the actor refuses:

ActorError: Admins only

Where to Go Next

  • JWTs for the full grant vocabulary, expiry behavior, and renewal.
  • Permissions for queue and event gating and deny-by-default patterns.
  • examples/jwt-counter for a complete project with a smoke test covering renewal and rejection.