---
sidebar_position: 11
---

# Server reference: `@wordplus/realtime/server`

Node 20 or later, and every edge runtime: it runs on Web Crypto and `fetch`. For PHP and Laravel, see the [PHP reference](php-reference.md); for Python, the [Python reference](python-reference.md). All three take the same options and answer the browser the same way.

## `createRealtime( options )` → `Realtime`

| Option | |
|---|---|
| `site`, `secret` | your app's key and secret (`WORDPLUS_SITE`, `WORDPLUS_SECRET`) |
| `localKey` | 64 hex characters of your own (`WORDPLUS_LOCAL_KEY`) ([encryption](encryption.md#your-local-key)) |
| `user( request )` | the request's user's id, or `null`: what `route` asks |
| `profile( user )` | `{ name, avatar?, url?, … }` or `null`: their directory entry, their presence info and call info by default |
| `hasKeys( user )` | `false` cuts a user off every key |
| `generations` | `{ get( name ), set( name, n ) }`, for `rotateChannel()` and `rotateUser()` |
| `seal` | `false` sends everything plain |
| `base` | where your routes are, `/api/realtime` by default, for `config()` |
| `headers` | headers the browser sends to your routes, such as a CSRF token, for `config()` |
| `worker`, `rooms` | where your app serves the two files, for `config()` |
| `tokenTtl`, `callTokenTtl` | seconds: 6 hours by default; a channels token at most a day, a calls token at most 6 hours |
| `server`, `api` | WordPlus Cloud by default |
| `fetch` | your own, for publishing |

## Owners

| Method | |
|---|---|
| `channel( prefix, ( user, channel ) => answer )` | private and presence channels by the start of their names; `false`, `true`, or `{ id, info }` ([channels](channels.md), [presence](presence.md)) |
| `calls( prefix, ( user, call, withUser ) => answer )` | calls and rooms; `false`, `true`, or `{ info, publish, subscribe, admin, hidden }` ([calls](calls.md)) |
| `kind( kind, ( rest, user, label ) => boolean, newest? )` | keys of your own kind ([encryption](encryption.md#your-own-sealed-values)) |

Callbacks may be async. The longest prefix that fits decides.

## Routes

| | |
|---|---|
| `route` | `( request: Request ) => Promise<Response>`: POST `…/auth`, `…/calls`, `…/keys`, the action from the path's last part; Next.js `export const POST = realtime.route` |
| `node( ( req ) => user )` | `( req, res )` for Node's own servers and Express; a body `express.json()` parsed is taken as it is |
| `handle( action, body, user )` | → `{ status, body }`: one route, for a framework of your own |

They take JSON POSTs only (`415` otherwise): a page on another site can't send one without asking first, which it is never allowed. A refusal is an answer (`denied`), not an HTTP error.

| Method | What it answers |
|---|---|
| `authorize( channels, user )` | `{ token, exp, keys, denied }` |
| `authorizeCall( request, user )` | `{ token, exp, key }` or `{ denied }` |
| `keysAnswer( labels, user )` | `{ keys, identity? }` |
| `identity( user )` | `{ token, exp }` or `null` |
| `token( channels, { user, info, ttl } )` | a channels token, signed as the auth route signs |
| `config( user )` | the browser's settings, with a signed-in user's keys and identity |

## Publishing

| Method | |
|---|---|
| `publish( channels, event, data, { except, seal } )` | ([publishing](publishing.md)) |
| `publishBatch( [ { channels, event, data, except, seal } ] )` | 10 events a request |
| `channelInfo( channel )` | `{ subscriptions, members? }` |
| `rotateChannel( channel )`, `rotateUser( user )` | moves a key on |
| `keys` | the `Keys`: `seal( text, label )`, `sealJson`, `open`, `entry( label )`, `dayLabel()`, `channelLabel( channel )` |

All throw `RealtimeError` (`code`, `status`) for the server's refusal ([errors](errors.md#publishing)).
