PolynomialHQdocs
Getting started

React quickstart

Connect a React application to a room with presence, durable storage, and a secure token endpoint.

This guide adds a shared counter and collaborator presence to a Next.js application. The same provider and hook setup works in Vite, Remix, or another client-rendered React application.

Here is what you are building. Both windows below run the same components you are about to write:

Loading the storage demo…

Install the SDKs

pnpm add @polynomialhq/react @polynomialhq/server

@polynomialhq/react runs in the browser. @polynomialhq/server uses node:crypto and must only run in trusted server code.

Add your credentials

Create a project and secret key in the PolynomialHQ dashboard, then add them to the server environment:

.env.local
POLYNOMIAL_PROJECT_ID=project_your_id
POLYNOMIAL_SECRET_KEY=sk_your_secret_key

Never expose the secret key through a NEXT_PUBLIC_ variable or send it to the browser. Anyone holding it can mint a token for any room in the project.

Create an authentication endpoint

The client posts { room } to this endpoint. Your application should authenticate the current user before issuing a token.

app/api/polynomial-auth/route.ts
import { PolynomialServer } from "@polynomialhq/server";
import { NextResponse } from "next/server";

const polynomial = new PolynomialServer({
  secretKey: process.env.POLYNOMIAL_SECRET_KEY!,
});

export async function POST(request: Request) {
  // Replace this with the user from your auth provider.
  const user = {
    id: "user_123",
    name: "Ada",
  };
  const { room } = (await request.json()) as { room: string };

  const token = polynomial.authorizeRoom({
    projectId: process.env.POLYNOMIAL_PROJECT_ID!,
    role: "write",
    roomId: room,
    userId: user.id,
    userInfo: {
      name: user.name,
    },
  });

  return NextResponse.json({ token });
}

authorizeRoom() signs the token locally and returns a string; it does not call the API, so there is nothing to await.

The endpoint must verify that the signed-in user is allowed to enter the requested room. See Authentication for a production authorization pattern.

Create the client

Create the client once, outside React rendering, and pass it to PolynomialProvider.

app/collaboration-provider.tsx
"use client";

import { createClient, PolynomialProvider } from "@polynomialhq/react";
import type { ReactNode } from "react";

const client = createClient({
  authEndpoint: "/api/polynomial-auth",
});

export function CollaborationProvider({ children }: { children: ReactNode }) {
  return <PolynomialProvider client={client}>{children}</PolynomialProvider>;
}

Always pass a client. PolynomialProvider will build a default one if you omit the prop, but a default client has no authEndpoint or publicApiKey, so entering a room by id throws createClient requires authEndpoint, publicApiKey, or an enterRoom token.

Add the provider near the root of the part of your application that uses collaboration:

app/layout.tsx
import { CollaborationProvider } from "./collaboration-provider";

export default function RootLayout({ children }: { children: React.ReactNode }) {
  return (
    <html lang="en">
      <body>
        <CollaborationProvider>{children}</CollaborationProvider>
      </body>
    </html>
  );
}

Enter a room

Declare the presence, storage, and event types used by your room.

app/counter-room.tsx
"use client";

import { RoomProvider } from "@polynomialhq/react";
import type { ReactNode } from "react";

export type Presence = {
  name: string;
  online: boolean;
};

export type Storage = {
  count: number;
};

export type Events = {
  celebrated: {
    emoji: string;
  };
};

export function CounterRoom({ children }: { children: ReactNode }) {
  return (
    <RoomProvider<Presence, Storage, Events>
      id="counter:demo"
      initialPresence={{ name: "Ada", online: true }}
      initialStorage={{ count: 0 }}
    >
      {children}
    </RoomProvider>
  );
}

The room connects when RoomProvider mounts and leaves when it unmounts. Each unique id identifies a separate collaboration boundary.

Read and update shared state

app/shared-counter.tsx
"use client";

import { useMutation, useOthers, useRoomStatus, useStorage } from "@polynomialhq/react";
import type { Events, Presence, Storage } from "./counter-room";

export function SharedCounter() {
  const count = useStorage<Presence, Storage, Events, number>((storage) => storage.count);
  const others = useOthers<Presence>();
  const status = useRoomStatus();

  const increment = useMutation<Presence, Storage>(({ storage }) => {
    storage.updateAt<number>(["count"], (current = 0) => current + 1);
  }, []);

  return (
    <section>
      <p>{status === "joined" ? "Live" : "Connecting…"}</p>
      <strong>{count}</strong>
      <button type="button" onClick={increment}>
        Add one
      </button>
      <p>{others.length + 1} people here</p>
    </section>
  );
}

The four type parameters on useStorage are presence, storage, broadcast events, and the type your selector returns. Reuse the same types you gave RoomProvider.

Render the component inside the room:

app/page.tsx
import { CounterRoom } from "./counter-room";
import { SharedCounter } from "./shared-counter";

export default function Page() {
  return (
    <CounterRoom>
      <SharedCounter />
    </CounterRoom>
  );
}

Verify it

Open the page in two browser windows. Both windows should show the presence count, and incrementing the counter in one window should update the other. Reload one window: the count should come back from durable storage rather than resetting to the initialStorage seed.

Next steps

  • Model cursor or selection state with Presence.
  • Structure durable data and mutations with Storage.
  • Send reactions and one-off signals with Broadcasts.
  • Add undo and redo with History.

On this page