# Configuration

The server is configured in two layers:

1. **Environment variables**: `configFromEnv()` builds a complete config
   from the environment (this is what the CLI `run.js` and the Docker image
   use).
2. **Programmatic overrides**: `start(partial)` merges your
   `Partial<ServerConfig>` **over** the env-derived config, so anything you
   pass explicitly wins:

```js
const config = { ...configFromEnv(), ...partial };
```

## Environment variables

| Variable                         | Default               | Maps to                       | Description                                                                                                                                                                                                                                                                                                                                                                                                             |
| -------------------------------- | --------------------- | ----------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `PORT`                           | `8083`                | `port`                        | HTTP/WS listen port.                                                                                                                                                                                                                                                                                                                                                                                                    |
| `ROUTE_PREFIX`                   | `/collab`             | `routePrefix`                 | URL prefix for everything the server owns: `${prefix}/ws`, `${prefix}/health`, `${prefix}/docs/:id/history`.                                                                                                                                                                                                                                                                                                            |
| `STATIC_DIR`                     | _(unset)_             | `staticDir`                   | Serve this directory at `/` (the demo page in the Docker image). Unset = no static hosting.                                                                                                                                                                                                                                                                                                                             |
| `ALLOW_ANONYMOUS`                | `false`               | `auth.allowAnonymous`         | Demo mode: token-less connections get generated guest identities. Truthy values: `1` or `true` (case-insensitive). Anything else is off.                                                                                                                                                                                                                                                                                |
| `GUEST_ROLE`                     | _(unset → `writer`)_  | `auth.guestRole`              | Role assigned to generated guest identities in anonymous mode: `owner` \| `writer` \| `commenter` \| `reader` (any other value is ignored). `commenter` demos the review workflow; `owner` makes every guest a comment moderator (can delete any thread); demo use only. See [Authentication](authentication.md#anonymous-demo-mode).                                                                                   |
| `SANITIZE_ENABLED`               | `true`                | `sanitize.enabled`            | The [content firewall](security.md#the-content-firewall) on incoming patches: an **allowlist** (via `sanitize-html`) of tags, attributes and URL schemes, plus depth and text-size limits. Only the literal value `false` disables it; disable only when a trusted-content pipeline of your own sits in front of the server.                                                                                            |
| `SESSION_WINDOW`                 | `512`                 | `sessionWindow`               | Per-room [sliding OT window](protocol.md#base_seq_too_old-and-the-sliding-memory-window): how many recent entries (with their intermediate document states) stay in memory to transform late submits. Bigger = more tolerance for laggy clients but more memory (≈ window × document size per active room); smaller = leaner rooms, but a client falling further behind gets `base_seq_too_old` and reloads a snapshot. |
| `STORAGE`                        | `memory`              | `storage`                     | Storage adapter: `memory` or `postgres`. Any other value throws at startup.                                                                                                                                                                                                                                                                                                                                             |
| `DATABASE_URL`                   | _(unset)_             | `storage`                     | Postgres connection string; **required** when `STORAGE=postgres` (startup error otherwise).                                                                                                                                                                                                                                                                                                                             |
| `SNAPSHOT_EVERY`                 | `200`                 | `snapshotEvery`               | Store a snapshot every N sequenced entries.                                                                                                                                                                                                                                                                                                                                                                             |
| `RETENTION_SNAPSHOTS`            | `0` (keep everything) | `retentionSnapshots`          | History retention: after each new snapshot, prune storage down to the newest N snapshots plus the entries after the oldest kept one. `0` keeps the full append-only history (complete audit/replay); a positive value bounds database growth but discards older history permanently.                                                                                                                                    |
| `RATE_LIMIT_ENABLED`             | `true`                | `rateLimit.enabled`           | Per-connection flood protection (token buckets; see [Security](security.md#rate-limiting)). Only the literal value `false` disables it.                                                                                                                                                                                                                                                                                 |
| `RATE_LIMIT_SUBMIT_PER_MINUTE`   | `600`                 | `rateLimit.submitPerMinute`   | Sustained rate for `submit` **and** `comment.*` operations (one shared bucket per connection). Honest typing stays far below this (one in-flight batch per round trip); over the limit → `error {rate_limited}`.                                                                                                                                                                                                        |
| `RATE_LIMIT_SUBMIT_BURST`        | `100`                 | `rateLimit.submitBurst`       | Burst capacity of the submit/comment bucket.                                                                                                                                                                                                                                                                                                                                                                            |
| `RATE_LIMIT_PRESENCE_PER_SECOND` | `25`                  | `rateLimit.presencePerSecond` | Presence (caret) updates per second; over-limit updates are silently dropped (ephemeral; the next one supersedes them).                                                                                                                                                                                                                                                                                                 |
| `EMPTY_ROOM_TTL_MS`              | `600000` (10 min)     | `emptyRoomTtlMs`              | Drop an empty in-memory room after this many ms of inactivity (a sweeper runs every 60 s). The document itself stays in storage.                                                                                                                                                                                                                                                                                        |
| `MAX_ROOMS`                      | `10000`               | `maxRooms`                    | Max distinct documents held in memory at once (`0` = unlimited). Flood backstop: once reached, a `hello` for a **new** document is rejected with `error {rate_limited}` and the connection is closed; existing rooms always load. See [Security](security.md#memory-bounds).                                                                                                                                            |

> **Warning: Fail-closed by default**
>
> With no env at all, the server listens on `:8083` with in-memory
> storage and **rejects every connection** (`auth_required`): there is no
> `checkAuthentication` hook in the env path and `ALLOW_ANONYMOUS` is off.
> Set `ALLOW_ANONYMOUS=true` for a demo, or pass `auth` programmatically.

## Programmatic `ServerConfig`

```ts
import { start, type ServerConfig } from '@jodit/collab-server';

const server = await start({
	/* ...Partial<ServerConfig> ... */
});
```

| Field                | Type                                       | Env-derived default                     | Description                                                                                                                                                                                                                                                                                                                                                                                |
| -------------------- | ------------------------------------------ | --------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `port`               | `number`                                   | `PORT` / `8083`                         | Listen port. Pass `0` to get a random free port (`server.port` reports the real one, which is handy in tests).                                                                                                                                                                                                                                                                             |
| `routePrefix`        | `string`                                   | `ROUTE_PREFIX` / `/collab`              | Prefix for the WS endpoint and REST routes.                                                                                                                                                                                                                                                                                                                                                |
| `staticDir`          | `string?`                                  | `STATIC_DIR`                            | Directory served at `/`.                                                                                                                                                                                                                                                                                                                                                                   |
| `auth`               | `AuthOptions`                              | `{ allowAnonymous: ALLOW_ANONYMOUS }`   | The identity contract: `checkAuthentication`, `authorize`, `allowAnonymous`, `guestIdentity`, `guestRole`. See [Authentication](authentication.md).                                                                                                                                                                                                                                        |
| `storage`            | `CollabStorage`                            | from `STORAGE`/`DATABASE_URL`           | Persistence adapter instance (`MemoryStorage`, `PostgresStorage`, or your own). See [Storage](storage.md).                                                                                                                                                                                                                                                                                 |
| `rateLimit`          | `RateLimitConfig`                          | from `RATE_LIMIT_*`                     | Per-connection token buckets: `{ enabled, submitPerMinute, submitBurst, presencePerSecond }`.                                                                                                                                                                                                                                                                                              |
| `sanitize`           | `SanitizeConfig`                           | `DEFAULT_SANITIZE` + `SANITIZE_ENABLED` | The content firewall: `{ enabled, maxDepth, maxTextInsert }`. `enabled` toggles it; `maxDepth` (64) and `maxTextInsert` (200 000) are resource guards. The tag/attribute allowlists (`STRICT_TAGS`, `ALLOWED_ATTRS`) are module constants in `sanitize.ts`, **not** fields on this object; they are not configurable through the config. See [Security](security.md#the-content-firewall). |
| `sessionWindow`      | `number`                                   | `SESSION_WINDOW` / `512`                | Sliding in-memory OT window per room (how far behind a `baseSeq` may lag).                                                                                                                                                                                                                                                                                                                 |
| `retentionSnapshots` | `number`                                   | `RETENTION_SNAPSHOTS` / `0`             | Keep only the last N snapshots + their entries in storage (`0` = full history).                                                                                                                                                                                                                                                                                                            |
| `snapshotEvery`      | `number`                                   | `SNAPSHOT_EVERY` / `200`                | Snapshot cadence (every N entries).                                                                                                                                                                                                                                                                                                                                                        |
| `createInitialDoc`   | `(docId: string) => SerializedElementNode` | `defaultInitialDoc`                     | Seed document for a room that does not exist in storage yet. The default is a small welcome text; return e.g. `el('root', 'div', {}, [])` for empty documents, or load per-doc templates.                                                                                                                                                                                                  |
| `emptyRoomTtlMs`     | `number`                                   | `EMPTY_ROOM_TTL_MS` / `600000`          | How long an empty room stays warm in memory before being unloaded.                                                                                                                                                                                                                                                                                                                         |
| `maxRooms`           | `number`                                   | `MAX_ROOMS` / `10000`                   | Max distinct documents held in memory at once (`0` = unlimited); a flood backstop against unbounded unique docIds. See [Security](security.md#memory-bounds).                                                                                                                                                                                                                              |

> **Note**
>
> `auth` is replaced as a whole, not deep-merged: if you pass
> `auth: { checkAuthentication }`, the env's `ALLOW_ANONYMOUS` no longer
> applies: include `allowAnonymous: true` yourself if you want both.

A custom initial document:

```js
import { start } from '@jodit/collab-server';
import { el, text } from '@jodit/collab-protocol';

await start({
	createInitialDoc: docId =>
		el('root', 'div', {}, [
			el('h0', 'h1', {}, [text('h1', `Document ${docId}`)])
		])
});
```

## Fixed behaviors (not configurable)

For reference when reasoning about deployments:

- WS handshake timeout: the first message must be `hello` within **10 s**;
- maximum WS message size: **1 MiB**;
- a `submit` batch is limited to **1024 patches**; `token` to 8192 chars;
  `docId` to 256 chars; node ids to 128 chars; comment bodies to
  10 000 chars and quotes to 2000 chars (schema limits; see
  [Protocol](protocol.md));
- keepalive reaper: the server pings every socket every **30 s** and
  terminates connections that miss two rounds (see
  [Security](security.md#transport-hardening));
- the empty-room sweeper runs every **60 s**;
- health endpoint: `GET ${routePrefix}/health` → `{"success":true,"protocol":1}`.

## Webhooks

Outbound webhooks (`WEBHOOK_URL`, `WEBHOOK_SECRET`, `WEBHOOK_EVENTS`,
`WEBHOOK_TIMEOUT_MS`, `WEBHOOK_DOC_THROTTLE_MS`) are documented on their own
page: see [Webhooks](webhooks.md).
