Documentation Index
Fetch the complete documentation index at: https://mintlify.com/cloudflare/partykit/llms.txt
Use this file to discover all available pages before exploring further.
Authentication in PartyServer happens at the routing layer, before any connection reaches a Server instance. Once the WebSocket upgrade succeeds the connection is live and cannot be rejected from inside lifecycle hooks, so the right time to check credentials is in the onBeforeConnect hook of routePartykitRequest. This page shows patterns for token validation, request modification, and attaching user identity to connections so that onMessage and other hooks can rely on it.
Why auth belongs in routing
The WebSocket protocol does not support HTTP error responses after the upgrade completes. If you try to close an unauthorized connection from inside onConnect, the client will observe an abrupt disconnect rather than a clean 401. The onBeforeConnect hook runs before the upgrade, so returning a Response from it sends a proper HTTP error back to the client.
Do not rely on onConnect for access control. By the time onConnect fires,
the WebSocket handshake is complete and the connection is open.
Validating a token in onBeforeConnect
onBeforeConnect receives the current Request and a lobby object with className and name. Return a Response to block, a Request to pass through (optionally modified), or nothing to let the original request continue unchanged.
import { routePartykitRequest } from "partyserver";
export default {
async fetch(request: Request, env: Env): Promise<Response> {
return (
(await routePartykitRequest(request, env, {
async onBeforeConnect(req, lobby) {
// Accept token from Authorization header or ?token= query param
const authHeader = req.headers.get("Authorization") ?? "";
const token =
authHeader.startsWith("Bearer ")
? authHeader.slice(7)
: new URL(req.url).searchParams.get("token");
if (!token) {
return new Response("Missing token", { status: 401 });
}
const payload = await verifyToken(token, env.JWT_SECRET);
if (!payload) {
return new Response("Invalid token", { status: 403 });
}
// Forward the verified user ID to the Server via a custom header
const next = new Request(req);
next.headers.set("x-user-id", payload.sub);
next.headers.set("x-user-role", payload.role ?? "member");
return next;
}
})) || new Response("Not Found", { status: 404 })
);
}
} satisfies ExportedHandler<Env>;
async function verifyToken(
token: string,
secret: string
): Promise<{ sub: string; role?: string } | null> {
// Replace with your real JWT verification logic
try {
// e.g. use jose or a Workers-compatible JWT library
return JSON.parse(atob(token.split(".")[1])) as {
sub: string;
role?: string;
};
} catch {
return null;
}
}
Attaching user identity to connections
After the request passes through onBeforeConnect, the headers you set are available on the ConnectionContext inside onConnect. Use connection.setState() to persist that identity for the lifetime of the connection:
import { Server } from "partyserver";
import type { Connection, ConnectionContext } from "partyserver";
type UserState = {
userId: string;
role: "admin" | "member";
};
export class ProtectedServer extends Server {
onConnect(connection: Connection<UserState>, ctx: ConnectionContext) {
const userId = ctx.request.headers.get("x-user-id") ?? "anonymous";
const role =
ctx.request.headers.get("x-user-role") === "admin" ? "admin" : "member";
connection.setState({ userId, role });
connection.send(
JSON.stringify({ type: "ready", userId, role })
);
}
onMessage(connection: Connection<UserState>, message: string) {
const { userId, role } = connection.state;
// Only admins can broadcast to everyone
if (role === "admin") {
this.broadcast(message);
} else {
connection.send(
JSON.stringify({ type: "error", text: "Insufficient permissions" })
);
}
}
}
Tagging connections by user role
getConnectionTags runs at connect time and lets you assign labels that can be used later to filter getConnections. Combine tags with onBeforeConnect to route messages only to specific groups:
export class TaggedServer extends Server {
getConnectionTags(
connection: Connection,
ctx: ConnectionContext
): string[] {
const role = ctx.request.headers.get("x-user-role") ?? "member";
return [role];
}
onMessage(connection: Connection<UserState>, message: string) {
const { type } = JSON.parse(message) as { type: string };
if (type === "admin-alert" && connection.state.role === "admin") {
// Send only to other admin connections
for (const adminConn of this.getConnections("admin")) {
if (adminConn.id !== connection.id) {
adminConn.send(message);
}
}
}
}
}
Authenticating HTTP requests with onBeforeRequest
HTTP requests to a Server (handled by onRequest) go through onBeforeRequest instead of onBeforeConnect. The API is identical:
await routePartykitRequest(request, env, {
async onBeforeRequest(req, lobby) {
const apiKey = req.headers.get("x-api-key");
if (apiKey !== env.ADMIN_API_KEY) {
return new Response("Forbidden", { status: 403 });
}
// Let the request through as-is
}
});
onBeforeConnect only fires for WebSocket upgrades. onBeforeRequest only
fires for plain HTTP requests. If your Worker handles both, define both hooks.
Full example: JWT-authenticated chat server
import { routePartykitRequest, Server } from "partyserver";
import type { Connection, ConnectionContext, WSMessage } from "partyserver";
type UserState = { userId: string; displayName: string };
export class ChatServer extends Server {
onConnect(connection: Connection<UserState>, ctx: ConnectionContext) {
const userId = ctx.request.headers.get("x-user-id")!;
const displayName = ctx.request.headers.get("x-display-name") ?? userId;
connection.setState({ userId, displayName });
this.broadcast(
JSON.stringify({ type: "join", user: displayName }),
[connection.id]
);
}
onMessage(connection: Connection<UserState>, message: WSMessage) {
const { displayName } = connection.state;
this.broadcast(
JSON.stringify({ type: "message", user: displayName, text: message })
);
}
onClose(connection: Connection<UserState>) {
this.broadcast(
JSON.stringify({ type: "leave", user: connection.state?.displayName })
);
}
}
export default {
async fetch(request: Request, env: Env): Promise<Response> {
return (
(await routePartykitRequest(request, env, {
async onBeforeConnect(req) {
const token = new URL(req.url).searchParams.get("token");
if (!token) return new Response("Unauthorized", { status: 401 });
const payload = await verifyJwt(token, env.JWT_SECRET);
if (!payload) return new Response("Forbidden", { status: 403 });
const next = new Request(req);
next.headers.set("x-user-id", payload.sub);
next.headers.set("x-display-name", payload.name ?? payload.sub);
return next;
}
})) || new Response("Not Found", { status: 404 })
);
}
} satisfies ExportedHandler<Env>;