Architecture
packages/protocol Zod entities, WebSocket and REST schemas, search parser, ULID helperspackages/sdk Typed REST and WebSocket client, plus bot examplesapps/server Hono API, ws gateway, static app, background jobs. One processapps/web React 19, TanStack Router, Zustand store, sync engineapps/desktop Tauri 2 shellsite This documentation and the landing pagedocker Dockerfile and compose filesImport direction is enforced: protocol then sdk then web. The server
never imports from web; it serves the built output.
One process
Section titled “One process”HTTP API, WebSocket gateway, static file serving, and background jobs all run in a single Node process. Boot order is configuration, open database, migrate, listen. This is why the deployment is one container and one volume.
The cost is that scaling out means running more than one node, which needs the
event bus swapped for something shared. The design keeps that door open: the
bus only ever needs to say “workspace W has events up to sequence S”, so
Postgres LISTEN/NOTIFY or Redis drops in without touching application code.
Two guardrails protect it, no un-invalidated in-memory caches, and background
jobs take a lock.
The event log
Section titled “The event log”Covered in Realtime protocol, and it is the heart of the system. The important property: the log is written in the same transaction as the change, so the two cannot disagree. Nearly every hard bug in a chat app is some version of the two disagreeing.
SQLite by default
Section titled “SQLite by default”WAL mode, a five second busy timeout, and a single serialized writer. This handles far more throughput than the team sizes Slick targets will produce. The schema is deliberately dialect portable through Kysely, so Postgres is a migration rather than a rewrite.
Sessions, not JWTs
Section titled “Sessions, not JWTs”Opaque 256-bit tokens, stored hashed, with a sliding expiry. On a single server the ability to revoke instantly is worth more than distributed verification. Bot tokens ride the same middleware, which is what makes bots first-class rather than a bolted-on special case.
Identifiers
Section titled “Identifiers”ULIDs everywhere, as TEXT. They sort by time, which makes unread comparison a string comparison and pagination a range scan. They can be generated by the client, which is what makes optimistic sending idempotent.
Frontend
Section titled “Frontend”A normalized Zustand store rather than a fetch cache, because event streams and cache invalidation fight each other. One sync engine owns the socket, reconnection, and event application; the UI never touches the socket. Sends go through a persisted outbox so a retry after a flaky network cannot duplicate.
Testing
Section titled “Testing”The server suite runs against a real in-process app with a temporary database, so tests exercise routes, transactions, and the event log together rather than mocking them. The sync protocol additionally has a property test that replays randomized event and disconnect schedules and asserts the client’s view matches the database.