Skip to content

Architecture

packages/protocol Zod entities, WebSocket and REST schemas, search parser, ULID helpers
packages/sdk Typed REST and WebSocket client, plus bot examples
apps/server Hono API, ws gateway, static app, background jobs. One process
apps/web React 19, TanStack Router, Zustand store, sync engine
apps/desktop Tauri 2 shell
site This documentation and the landing page
docker Dockerfile and compose files

Import direction is enforced: protocol then sdk then web. The server never imports from web; it serves the built output.

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.

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.

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.

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.

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.

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.

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.