Issue tokens from your backend
Use @folkways/realtime/server to sign tokens for your users and publish events from your backend.
@folkways/realtime/server is the backend half of the SDK. It signs tokens with your segment's signing key and
publishes events over HTTP. It runs on Node.js 20+, Deno, Bun and edge runtimes, and refuses to run in a browser.
Create the server client
import { Folkways } from '@folkways/realtime/server';
export const rte = new Folkways.Rest({
url: process.env.RTE_URL!, // the node URL, e.g. https://rte.example.com
segment: process.env.RTE_SEGMENT_ID!, // the segment ID (a UUID)
signingKey: {
kid: process.env.RTE_KEY_ID!, // the key's ID from the Signing keys tab
privateKey: process.env.RTE_PRIVATE_KEY!,
},
});privateKey is one of:
- the private key the dashboard showed you once, or your own private key file, as a PKCS#8 PEM
(
-----BEGIN PRIVATE KEY-----). Ed25519 and P-256 keys are detected automatically; - the shared secret of an HS256 key.
Keep these values in your backend's secrets, never in your app or your repository. If a private key leaks, add a new key and delete the old one: that ends every connection still using it.
If your environment stores the PEM on one line with \n, restore the line breaks:
privateKey: process.env.RTE_PRIVATE_KEY!.replace(/\\n/g, '\n').
Sign a token
const { token, expiresAt } = await rte.createToken({
identity: 'user:42',
capabilities: [
{ topics: 'chat/>', ops: ['subscribe', 'publish'] },
{ topics: 'typing/>', ops: ['subscribe', 'signal'] },
],
});| Option | Default | Meaning |
|---|---|---|
identity | required | Who the token is for, e.g. user:42. At most 256 bytes; must not start with anon: |
kind | 'user' | 'user', 'agent', 'service' or 'anonymous' (no identity; for read-only visitors) |
capabilities | [] | Topic patterns and the operations allowed on them. With none, the token can only connect |
ttlMs | 600000 (10 min) | Lifetime. At most 15 minutes |
Signing happens in your process: createToken() makes no network call, so you can call it on every request. It
resolves with { token, expiresAt, identity, kind, capabilities }.
Add the token endpoint
Your app's authUrl points at this endpoint. Put it behind your normal login, so only signed-in users get a token,
and grant each user only the topics they need.
app.post('/api/realtime-token', requireLogin, async (req, res) => {
const { token } = await rte.createToken({
identity: `user:${req.user.id}`,
capabilities: [{ topics: `chat/team-${req.user.teamId}/>`, ops: ['subscribe', 'publish'] }],
});
res.json({ token });
});The SDK calls the endpoint with POST, a JSON body { "reason": "initial" | "expiring" | "rejected" } and your
page's cookies (same origin). Answer with { "token": "…" } as JSON, or the token as plain text. Any status other
than 2xx counts as "not signed in".
Not on Node.js?
Any JWT library works. Sign with the segment's key and put the segment ID in seg, the identity in sub,
permissions in cap and the key ID in the kid header. See What a token contains.
The dashboard's Connect tab has ready-made examples for Python and Go.
Publish from your backend
Send stream or kv events from your server, for example a notice after a payment or an updated score:
const receipt = await rte.publish('chat/lobby', 'notice', { text: 'Payment received' });
await rte.publish('scores/match-7', 'score', { home: 2, away: 1 }, { delivery: 'kv', key: 'match' });rte.publish(topic, event, data, options?) takes the same options as a client publish (delivery, key,
ttlMs, remove, headers, contentType, idempotencyKey), except that signals can't be sent from a server.
It resolves with the node's receipt.
The server publishes as the identity server (change it with the identity option) with its own service token,
which it signs and refreshes itself. That token may publish on every topic by default; narrow it with
capabilities. The topic's rule must still allow publish.
To retry safely after a timeout, pass your own idempotencyKey (a UUID): the node applies each key at most once
within 10 minutes.
Failures are RealtimeErrors, as in the client. isRealtimeError and Code are exported by both entry points:
import { Code, isRealtimeError } from '@folkways/realtime/server';
try {
await rte.publish('chat/lobby', 'notice', { text: 'Payment received' });
} catch (err) {
if (isRealtimeError(err) && err.code === Code.Forbidden) console.error('no topic rule allows publish here');
else throw err;
}See the full list of Rest options and methods.