FIRST EDITIONREV. 2026-08
Undertow
A Fluid Framework–compatible collaborative document service in .NET, wire-compatible with the Gleam Floodgate server it was ported from and with the Levee Elixir server for the Phoenix protocol.
- One process
- One port
- Two wire protocols
- + REST
Operating procedure
Obtain the source. Undertow ships as source — there is no package to install.
git clone https://github.com/tylerbutler/undertow.git && cd undertowStart the server. Builds the container from local source and waits for the healthcheck.
docker compose up -d --waitWithout Docker (needs the .NET SDK):d
UNDERTOW_JWT_SECRET=dev-tenant-secret-key dotnet run --project src/Undertow.ServerVerify, then connect. Point an official Fluid/Routerlicious driver or a levee-client at port 3000.
curl http://localhost:3000/health
/socket.io/Engine.IO v4 / Socket.IO v5 — official Fluid & Routerlicious drivers
/socket/websocket Phoenix Channels V2 — levee-driver & levee-client
RESTdocuments · deltas · token mint · git-like Historian storage
Wire formats
4Engine.IO
MESSAGE
2Socket.IO
EVENT
["op", …payload]JSON array — event name, then arguments
42["op",…]frame as the drivers send it. The Fluid clientId is the Engine.IOsid itself.join_refreftopiceventpayloaddocument:{tenant}:{documentId}.Conformance record
Recorded 2026-08-07 against the Gleam Floodgate reference at 2687b5f. The byte-level contract lives in the golden wire fixtures; counts move with the repository, and the fixtures — not this card — are the authority.
| Suite | Gate | Result | Remarks |
|---|---|---|---|
| Routerlicious conformancea | just test-dual-mode | 38 pass | 3 skipped · 1 todo |
| Phoenix + cross-mode conformance | just test-dual-mode | 7 pass | both protocols, one process |
| Drop-in parity vs. Levee suiteb | levee integration suites | 53 / 54 | one intentional 401 |
| Readiness checks | startup probes | 8 / 8 | health, auth, storage |
| levee-example integration | example app suite | 15 pass | levee-client against Undertow |
| Unit + integration tests | dotnet test Undertow.slnx | 187 pass | all tiers |
| todo-list multi-user e2ec | live browser flow | 9 / 9 | real container loader |
Notes — deliberate divergence
Undertow departs from its references only on purpose, and every departure is written down. The full accounting is in manual §6 and tests/fixtures/wire/README.md.
- a
The 3 skipped and 1 todo are the reference suite's own baseline at capture time — Floodgate posts the same counts.
- b
The single non-passing parity case is an intentional 401 response, recorded as ADR-009.
- c
Passing 9/9 required echoing the supplied IClient verbatim rather than key-sorted: the container loader's assert 0x4b2 demands byte-identity with the object the client sent. The Gleam reference tripped this in live browser flows; both servers now also pin
initialSignalsto[], matching Levee. - d
dev-tenant-secret-keyis the development secret used by the example suites. Set your ownUNDERTOW_JWT_SECRETanywhere that is not a laptop. - e
A Phoenix join with
vsn=1.0.0is rejected403before the WebSocket upgrade — the fixtures pin 403, not the 400 an earlier plan assumed.
Parameters
Configuration is read from environment variables at startup. Every UNDERTOW_* key transitionally falls back to its FLOODGATE_* spelling, so one compose file can drive either binary; the source of each value is logged at startup. Full table: manual §3.
| Parameter | Default | Purpose |
|---|---|---|
UNDERTOW_JWT_SECRET● | — required | Tenant JWT signing secret. The server refuses to start without it. |
PORT | 3000 | Listen port. PORT wins over UNDERTOW_PORT. |
UNDERTOW_BIND | localhost | Bind address. |
UNDERTOW_TENANT_ID | fluid | Tenant id used in topics and tokens. |
UNDERTOW_STORAGE_BACKEND | ets | ets / shelf = SQLite (WAL); memory = in-process only. |
UNDERTOW_DATA_DIR | priv/undertow_data | SQLite data directory. |
UNDERTOW_MAX_FRAME_BYTES | 16777216 | Frame cap; also advertised in the Engine.IO open and IConnected. |
UNDERTOW_MAX_CONNECTIONS | 4096 | Connection ceiling. Per-IP ceiling: 256. 0 = unlimited. |
UNDERTOW_MESSAGE_RATE | 1000 | Messages/sec per connection; burst 2000. 0 = unlimited. |
UNDERTOW_HEARTBEAT_INTERVAL_MS | 30000 | Heartbeat cadence; timeout 60000 ms. |
UNDERTOW_DOC_IDLE_MS | 300000 | Idle-document eviction window. |
UNDERTOW_ALLOWED_ORIGINS | (unset) | Origin allow-list for browser clients; * allows all. |
● required — the server refuses to start without it. For rate and connection limits, 0 means unlimited.
The manual
The card states the facts; the manual explains them. Eight chapters, drawn from the same repository the server builds from.
- 1IntroductionWhat Undertow is, what it serves, and what it was ported from.
- 2Operating procedureClone, start with Docker or the .NET SDK, verify, connect a client.
- 3ConfigurationThe full UNDERTOW_* parameter table, precedence, and fallbacks.
- 4Wire protocolsSocket.IO and Phoenix Channels V2 frame formats, topics, and REST.
- 5Conformance & fixturesThe recorded results, the gates, and the golden wire transcripts.
- 6Divergence notesEvery deliberate departure from the reference servers, in full.
- 7Internal organizationThe pure F# tier, the C# runtime, transports, storage, and host.
- 8LineageFloodgate, Levee, and how this repository was extracted.