Architecture
Backlog Hold'em is a browser application backed by a Go service. The frontend sends planning-poker commands and receives authoritative room snapshots in real time. SQLite deployments run one backend process; PostgreSQL can coordinate multiple replicas.
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.
- Event-sourced aggregate: immutable room events are the source of truth; bounded current-state and normalized round projections are updated transactionally.
- Driver-aware fan-out: SQLite uses a process-local hub, while PostgreSQL uses transactional database notifications to reach subscribers on every 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 the current room projection and expected sequence through
RoomRepository. - A domain method validates the command, emits a versioned event, and applies it through the aggregate reducer.
- The repository appends the event and updates current/round projections atomically; stale versions are reloaded and revalidated.
Room.Snapshotcreates a credential-free representation and strips unrevealed vote values and current-round revision counts.- The public snapshot is published through
RoomUpdates. - SSE and WebSocket subscribers deliver the snapshot to browsers.
PostgreSQL commits publish a database notification that each replica turns into an authoritative snapshot. SQLite intentionally remains single-process. Ending a session is a facilitator-only terminal event: the final summary is projected and subsequent joins and room mutations are rejected.