How authentication works
Tokens, signing keys and publishable keys — how your backend vouches for users, what a token contains and how the SDK keeps it fresh.
Every connection to Folkways presents a token: a short-lived, signed pass that says which segment the connection belongs to, who it is and what it may do. There are two ways to get one.
| Signed by your backend | Traded for a publishable key | |
|---|---|---|
| Who it's for | Signed-in users, agents, your servers | Visitors who aren't signed in |
| Identity | Yes, e.g. user:42 | None (anonymous) |
| Can publish | Yes, if its permissions allow | No, read-only |
| What your app holds | Nothing secret: it asks your backend for a token | The publishable key (rte_pk_…), which is public |
| Set up | A signing key on the segment, and a token endpoint | Anonymous connections on, and a publishable key |
| SDK option | authUrl or token | publishableKey |
Tokens signed by your backend
Your app Your backend Folkways node
│ POST /api/realtime-token │ │
│ (with your login cookie) │ │
│ ──────────────────────────► │ checks the user is signed in, │
│ │ signs a token with the private key │
│ ◄────────────────────────── │ │
│ { token } │ │
│ ─────────────── connect with the token ───────────────────────────► │
│ │ checks the signature with the │
│ ◄──────────────────────────────────────── session ───────────────── │ public key- Your app asks your backend for a token, using your normal login (cookie or header).
- Your backend checks the user and signs a token with the segment's private signing key. With
@folkways/realtime/serverthis is one call, and it makes no network request. - The app connects with the token. The node checks the signature with the segment's public key, which it got from the dashboard.
The private key never leaves your backend, and nothing secret is ever sent over the network.
What a token contains
A token is a standard JWT. rte.createToken() fills it in for you; if you sign tokens with another JWT library,
use these fields:
| Field | Where | Meaning |
|---|---|---|
kid | header | The signing key's ID, as shown on the Signing keys tab |
alg | header | EdDSA (Ed25519), ES256 (P-256) or HS256 (shared secret) |
seg | claims | The segment ID |
sub | claims | The identity, e.g. user:42 (at most 256 bytes; must not start with anon:) |
kind | claims | user (default), agent or service |
cap | claims | Permissions: a list of { "topic": "<pattern>", "ops": [...] } |
iat, exp | claims | Issued and expiry time in seconds. At most 15 minutes apart |
Permissions
A token lists topic patterns and the operations allowed on them:
capabilities: [
{ topics: 'chat/>', ops: ['subscribe', 'publish'] }, // read and write every chat room
{ topics: 'typing/>', ops: ['subscribe', 'signal'] }, // see and send typing indicators
{ topics: 'scores/*', ops: ['subscribe'] }, // read scores only
]| Operation | Allows |
|---|---|
subscribe | Receiving a topic's events |
publish | Publishing events of any type: stream, kv and signal |
set | Publishing and removing kv values only |
signal | Sending signals only |
history | Reading a topic's past events |
presence | Reserved for presence, which is coming soon |
An operation must be allowed by both the token and the topic's rule in the dashboard. If either says no, the
node answers Forbidden.
Grant each user only what they need, for example chat/team-7/> instead of chat/> for a member of team 7. In
your app, session.can('publish', 'chat/lobby') tells you whether to show a send button. The node still enforces
every permission.
Lifetime and refresh
Tokens live at most 15 minutes; createToken() makes them last 10 by default. You never handle expiry yourself:
- The SDK asks for a new token about a minute before the current one expires, and swaps it on the open connection without dropping subscriptions.
- If the node rejects a token, the SDK asks once more, with reason
'rejected'. - A plain string token can't be refreshed: the connection closes with
AuthExpiredwhen it runs out. Use it only for tests.
Signing keys
You add signing keys on the segment's Signing keys tab.
| Algorithm | When to use |
|---|---|
Ed25519 (EdDSA) | The default. Fast and supported by most JWT libraries. |
P-256 (ES256) | When your library or key service doesn't support Ed25519. |
Shared secret (HS256) | Trying things out. One secret both signs and checks, so treat it like a password. Production plans need a key pair. |
Rotating a key without downtime:
- Add a new key with a new ID. Both keys work side by side.
- Switch your backend to sign with the new key and its ID.
- After 15 minutes (the longest a token can live), delete the old key.
Deleting a key also closes every connection that is still using a token signed with it, which is how you cut off access quickly if a key leaks.
Publishable keys
A publishable key (rte_pk_…) is meant to be public: it ships inside your web page or app. The SDK trades it at
the node for an anonymous, read-only token, so a copied key can never write to a topic or pose as a user. The
worst it can do is read what your public page already shows.
- It can grant only
subscribeandhistory, on the topics you chose when creating it. - The segment must allow anonymous connections (Settings → Who can connect).
- You can pause, resume or revoke it on the Publishable keys tab. Pausing or revoking also closes the connections using it.
See Publishable keys.
There are no secret API keys
Folkways has no secret key that your backend sends to get user tokens. Your backend signs tokens itself, which is
faster and never puts a secret on the wire. Keys that start with rte_sk_ are refused by the node and the SDK.
Giving the SDK a token
Pass exactly one of these options to new Folkways.Realtime():
// 1. Your token endpoint. The SDK POSTs { reason } and reads { token } or a plain-text token.
new Folkways.Realtime({ url, authUrl: '/api/realtime-token' });
// 2. Your own function, for custom requests or token caching.
new Folkways.Realtime({
url,
token: async ({ reason }) => { // reason: 'initial' | 'expiring' | 'rejected'
const res = await fetch('/api/realtime-token', { method: 'POST', credentials: 'include' });
return (await res.json()).token;
},
});
// 3. A publishable key, for read-only apps without a backend.
new Folkways.Realtime({ url, publishableKey: 'rte_pk_…' });
// 4. A fixed token string. It can't be refreshed: tests and scripts only.
new Folkways.Realtime({ url, token: 'eyJhbGciOi…' });authUrl sends your page's cookies when the endpoint is on the same origin. For an endpoint on another domain, or
to send a header such as Authorization, use authHeaders or the token function.
Sign-in and sign-out
When the user signs in, signs out or switches account, give the client a new token source:
await client.updateToken(async () => fetchTokenForCurrentUser());- Same identity (for example, new permissions): the connection and subscriptions stay.
- Different identity: the client reconnects as the new user, ends every subscription with
IdentityChangedand wipes the previous user's local data.
To sign out completely, close the client and wipe its local data:
await client.close({ clear: true });Catching a wrong segment
Pass the segment you expect to connect() to fail fast when, say, a staging token reaches production:
await client.connect({ segment: '5faeecf0-db6e-41a3-9448-afe1b096a6d2' }); // SegmentMismatch otherwise