PolynomialHQdocs
Getting started

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

  1. createClient({ authEndpoint }) posts { room: roomId } to your endpoint.
  2. Your endpoint resolves the signed-in user.
  3. Your application checks that the user can access that room.
  4. PolynomialServer.authorizeRoom() returns a short-lived token.
  5. 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.

app/api/polynomial-auth/route.ts
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

RoleJoin and readUpdate presenceBroadcastUpdate storage
readYesYesNoNo
writeYesYesYesYes
adminYesYesYesYes

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.

Loading the role demo…

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 401 for unauthenticated requests and 403 for unauthorized rooms.
  • Prefer short token lifetimes and issue a new token when reconnecting.

On this page