Architecture
Backlog Hold'em is a browser application backed by a single Go process. The frontend sends planning-poker commands and receives authoritative room snapshots in real time.
Main decisions
- Ports and adapters: business rules are kept separate from HTTP, SSE, WebSocket, and database details.
- Authoritative server state: clients render server snapshots and do not decide whether a command, including ending a session, is valid.
- Two transport modes: HTTP commands with SSE events is the default; WebSocket supports commands and events on one connection.
- SQLite first: a file-backed SQLite database is the default. PostgreSQL can be selected through configuration.
- Aggregate persistence: each room, its lifetime revealed-round history, and its final summary are persisted as one JSON document, making the room the consistency boundary.
- Process-local fan-out: active subscribers are connected through an in-memory event hub. This intentionally limits the current deployment to one backend replica.
Container view
In production-shaped deployments, one Go process routes all browser traffic:
| Path | Destination | Purpose |
|---|---|---|
/ and static assets | Go web adapter | Prerendered SvelteKit pages, assets, and dynamic-route fallback |
/api/* | Go backend | HTTP commands and snapshots |
/events/* | Go backend | SSE event streams |
/ws/* | Go backend | Bidirectional WebSocket sessions |
The final image copies the static SvelteKit build to /app/public and sets STATIC_DIR to that location. During hot-reload development, Vite serves the frontend on port 5173 and proxies backend paths to Go on port 8080. SvelteKit prerenders public pages while room routes remain client-rendered; Go serves the generated 200.html fallback for direct room links.
The project documentation is a separately deployable VitePress site. Its static container is not part of the Backlog Hold'em application runtime or its trust boundary.
Backend dependency direction
Dependencies point inward:
transport/database adapters → application and ports → domainThe core does not depend on transport or persistence implementations. cmd/server is the composition root: it chooses adapters from environment variables, optionally wraps the API with the static-web adapter, and wires the object graph.
Command lifecycle
- An HTTP handler or WebSocket adapter decodes a transport message.
- The adapter calls the shared application service.
- The application loads a room through
RoomRepository. - A domain method validates and applies the operation.
- The application increments the room version and saves the aggregate.
PublicCopystrips unrevealed vote values and current-round revision counts.- The public snapshot is published through
RoomEvents. - SSE and WebSocket subscribers deliver the snapshot to browsers.
Mutations are currently serialized by a mutex in the application service. This is simple and safe for one process, but a multi-instance design would require database-level concurrency control and a distributed event bus. Ending a session is a facilitator-only terminal mutation: the final summary is persisted and subsequent joins and room mutations are rejected.