Authentication
Issue short-lived room tokens from a trusted backend and enforce room-level authorization.
PolynomialHQ uses signed tokens to connect a user to a room. The browser requests a token from your backend; your backend authenticates the user, checks access to the requested room, and signs the result with a PolynomialHQ secret key.
The request flow
createClient({ authEndpoint })posts{ room: roomId }to your endpoint.- Your endpoint resolves the signed-in user.
- Your application checks that the user can access that room.
PolynomialServer.authorizeRoom()returns a short-lived token.- The client presents the token when joining the realtime WebSocket.
Steps 1 to 4 are ordinary HTTP. Only step 5 involves the realtime protocol, and the token is the only thing the browser ever holds.
Production endpoint
The example below uses placeholder requireUser and canAccessRoom functions. Connect these to
your authentication and authorization layers.
import { PolynomialServer } from "@polynomialhq/server";
const polynomial = new PolynomialServer({
secretKey: process.env.POLYNOMIAL_SECRET_KEY!,
});
export async function POST(request: Request) {
const user = await requireUser(request);
const { room } = (await request.json()) as { room?: unknown };
if (typeof room !== "string" || room.length === 0) {
return Response.json({ error: "A room is required." }, { status: 400 });
}
const permission = await canAccessRoom(user.id, room);
if (!permission) {
return Response.json({ error: "Forbidden." }, { status: 403 });
}
const token = polynomial.authorizeRoom({
expiresInSeconds: 60 * 60,
metadata: {
organizationId: user.organizationId,
},
projectId: process.env.POLYNOMIAL_PROJECT_ID!,
role: permission.canEdit ? "write" : "read",
roomId: room,
userId: user.id,
userInfo: {
avatar: user.avatarUrl,
name: user.name,
},
});
return Response.json({ token });
}Roles
| Role | Join and read | Update presence | Broadcast | Update storage |
|---|---|---|---|---|
read | Yes | Yes | No | No |
write | Yes | Yes | Yes | Yes |
admin | Yes | Yes | Yes | Yes |
Switch the role in the demo below and try each operation. The client sends every message optimistically; the service is what rejects the ones the token does not allow.
A rejected write ends the connection
The service answers an unauthorized broadcast or storage write with a non-retryable forbidden
error, which moves the room to the terminal error status. Hide or disable write affordances
when you have issued a read token instead of relying on the server to reject them.
This also applies to a room that has no document yet: the first connection seeds initialStorage
by writing it, so a read client that opens an empty room fails immediately. Let a writer create
the room's initial document before pointing read-only viewers at it.
Use the least-privileged role your interface needs. Administrative backend operations still use
the secret key through PolynomialServer.
Token fields
authorizeRoom() accepts:
Prop
Type
The token is signed with HMAC-SHA256 and returned synchronously. It is not stored by PolynomialHQ,
so shortening expiresInSeconds is the only way to limit its useful lifetime.
Functional auth endpoints
The browser client also accepts an async token function. This is useful when your application already has a typed API client:
import { createClient } from "@polynomialhq/client";
const client = createClient({
authEndpoint: async (roomId) => {
const result = await api.collaboration.authorize({ roomId });
return result.token;
},
});The function may return a token string or { token: string }. It is called every time the client
opens a connection for a room, which includes each reconnect attempt, so keep it cheap and let it
return a freshly signed token.
Public API keys
createClient accepts publicApiKey for environments where your project explicitly supports
public access. Do not substitute a secret key:
const client = createClient({
publicApiKey: "pk_public_project_key",
});A publishable key only authorizes a connection that also names a room, so use it with
client.enterRoom(roomId, …) rather than a bare createRoom(). Use server-issued room tokens for
authenticated products and private data.
Identity tokens
PolynomialServer.identifyUser() creates a user-scoped identity token with optional group,
organization, project, and user information:
const token = polynomial.identifyUser({
groupIds: ["team_design"],
organizationId: "org_acme",
projectId: "project_123",
userId: "user_123",
userInfo: { name: "Ada" },
});Identity tokens represent a user across a broader project or organization scope. Standard
room-scoped connections should use authorizeRoom().
Security checklist
- Keep secret keys in a server-only environment variable.
- Authenticate every token request.
- Authorize the requested room; do not trust the room ID from the browser.
- Use stable user IDs, not display names.
- Put only non-sensitive display data in
userInfo. - Return
401for unauthenticated requests and403for unauthorized rooms. - Prefer short token lifetimes and issue a new token when reconnecting.