Room tokens

We never learn who your users are, and you never send them to us to find out. Your application decides, and says so in a signed token the server can check on its own.

The format

Base64url JSON, a full stop, and an HMAC-SHA256 of that JSON under your signing secret. There is no library to install, because there is nothing to a library:

import { createHmac } from 'node:crypto';

export function roomToken(claims: object, secret: string): string {
  const body = Buffer.from(JSON.stringify(claims)).toString('base64url');
  const signature = createHmac('sha256', secret).update(body).digest('base64url');
  return `${body}.${signature}`;
}

The token travels as a token query parameter on the WebSocket URL, which is where every y-websocket client already puts arbitrary parameters.

The claims

roomThe room this token opens. It has to match the room being connected to exactly, so a token for one document cannot open another.
expWhen it stops working, in seconds since the epoch. Short is safer; see reconnects below.
projectYour project id. It decides which secrets could verify this token, and namespaces the room, so your notes is nobody else's.
subjectOptional. An opaque id for the reader, used to count people instead of connections and passed nowhere else. Make it an internal id, never a name or an address: we store a hash of it salted per project, and the less it means the better.
readOnlyOptional. The connection receives every change and writes none. Useful for a public link or a viewer role.

How it is checked, in that order

The project is read out of the token before anything is trusted, because a server holding a secret per customer has to know whose secret to fetch before it can check a signature. That read decides nothing. The signature is then checked against that project's secrets, and a token claiming a project it cannot sign for fails there, which is the only step that decides anything.

Expiry and reconnects

A y-websocket provider keeps the parameters it was constructed with, so every reconnect carries the same token. Give those tokens hours rather than minutes, or a dropped connection after the expiry will be refused.

The purpose-built client takes a function instead, and calls it on each attempt, which makes a short expiry free:

import { CollabProvider } from 'quoin-collab-client';

// A function rather than a string: it runs again on every reconnect, so a short
// expiry costs nothing and a revoked session stops at the next attempt.
const provider = new CollabProvider({
  url: 'wss://quoincloud.com/api/collab',
  room: 'design-notes',
  token: () => fetch('/api/room-token/design-notes').then((r) => r.text()),
});

Rotating a secret

Rotating on the keys page makes a new secret and retires the old one for 24 hours rather than deleting it. Both verify during that window, so nobody is signed out mid sentence while your deployment picks up the new value. Revoking ends the window at once, which is what you want for a secret you believe has leaked, and you should expect every token signed with it to stop immediately.

Three things not to do

Next: running it yourself.