--- title: "Security" description: "Backend-first auth and access control patterns." icon: "shield" --- As covered in [Architecture](/architecture), run the Sandbox Agent client on your backend, not in the browser. This keeps sandbox credentials private and gives you one place for authz, rate limiting, and audit logging. ## Auth model Implement auth however it fits your stack (sessions, JWT, API keys, etc.), but enforce it before any sandbox-bound request. Minimum checks: - Authenticate the caller. - Authorize access to the target workspace/sandbox/session. - Apply request rate limits and request logging. ## Examples ### Rivet ```ts Actor (server) import { UserError, actor } from "rivetkit"; import { SandboxAgent } from "sandbox-agent"; type ConnParams = { accessToken: string; }; type WorkspaceClaims = { sub: string; workspaceId: string; role: "owner" | "member" | "viewer"; }; async function verifyWorkspaceToken( token: string, workspaceId: string, ): Promise { // Validate JWT/session token here, then enforce workspace scope. // Return null when invalid/expired/not a member. if (!token) return null; return { sub: "user_123", workspaceId, role: "member" }; } export const workspace = actor({ state: { events: [] as Array<{ userId: string; prompt: string; createdAt: number }>, }, onBeforeConnect: async (c, params: ConnParams) => { const claims = await verifyWorkspaceToken(params.accessToken, c.key[0]); if (!claims) { throw new UserError("Forbidden", { code: "forbidden" }); } }, createConnState: async (c, params: ConnParams) => { const claims = await verifyWorkspaceToken(params.accessToken, c.key[0]); if (!claims) { throw new UserError("Forbidden", { code: "forbidden" }); } return { userId: claims.sub, role: claims.role, workspaceId: claims.workspaceId, }; }, actions: { submitPrompt: async (c, prompt: string) => { if (!c.conn) { throw new UserError("Connection required", { code: "connection_required" }); } if (c.conn.state.role === "viewer") { throw new UserError("Insufficient permissions", { code: "forbidden" }); } // Connect to Sandbox Agent from the actor (server-side only). // Sandbox credentials never reach the client. const sdk = await SandboxAgent.connect({ baseUrl: process.env.SANDBOX_URL!, token: process.env.SANDBOX_TOKEN, }); const session = await sdk.createSession({ agent: "claude", sessionInit: { cwd: "/workspace" }, }); session.onEvent((event) => { c.broadcast("session.event", { userId: c.conn!.state.userId, eventIndex: event.eventIndex, sender: event.sender, payload: event.payload, }); }); const result = await session.prompt([ { type: "text", text: prompt }, ]); c.state.events.push({ userId: c.conn.state.userId, prompt, createdAt: Date.now(), }); return { stopReason: result.stopReason }; }, }, }); ``` ```ts Client (browser) import { createClient } from "rivetkit/client"; import type { registry } from "./actors"; const client = createClient({ endpoint: process.env.NEXT_PUBLIC_RIVET_ENDPOINT!, }); const handle = client.workspace.getOrCreate(["ws_123"], { params: { accessToken: userJwt }, }); const conn = handle.connect(); conn.on("session.event", (event) => { console.log(event.sender, event.payload); }); const result = await conn.submitPrompt("Plan a refactor for auth middleware."); console.log(result.stopReason); ``` Use [onBeforeConnect](https://rivet.dev/docs/actors/authentication), [connection params](https://rivet.dev/docs/actors/connections), and [actor keys](https://rivet.dev/docs/actors/keys) together so each actor enforces auth per workspace. ### Hono ```ts import { Hono } from "hono"; import { bearerAuth } from "hono/bearer-auth"; const app = new Hono(); app.use("/sandbox/*", bearerAuth({ token: process.env.APP_API_TOKEN! })); app.all("/sandbox/*", async (c) => { const incoming = new URL(c.req.url); const upstreamUrl = new URL(process.env.SANDBOX_URL!); upstreamUrl.pathname = incoming.pathname.replace(/^\/sandbox/, "/v1"); upstreamUrl.search = incoming.search; const headers = new Headers(); headers.set("authorization", `Bearer ${process.env.SANDBOX_TOKEN ?? ""}`); const accept = c.req.header("accept"); if (accept) headers.set("accept", accept); const contentType = c.req.header("content-type"); if (contentType) headers.set("content-type", contentType); const body = c.req.method === "POST" || c.req.method === "PUT" || c.req.method === "PATCH" ? await c.req.text() : undefined; const upstream = await fetch(upstreamUrl, { method: c.req.method, headers, body, }); return new Response(upstream.body, { status: upstream.status, headers: upstream.headers, }); }); ```