Backend
The backend is a Go module under backend/.
Package map
| Path | Responsibility |
|---|---|
cmd/server | Composition root, environment configuration, HTTP server lifecycle |
cmd/migrate | Forward migration, schema status, and version operations |
cmd/rebuild | Offline rebuild of current and reporting projections from room events |
internal/domain | Event-sourced room aggregate, reducer, participant/round types, and planning-poker rules |
internal/ports | Event-store repository and room-notification interfaces |
internal/application | Command orchestration, optimistic retries, and snapshot publication |
internal/adapters/eventstore | Dialect-aware transactional event store, current/round projections, and rebuild tooling |
internal/adapters/httpapi | HTTP/JSON, SSE, and WebSocket adapters |
internal/adapters/webapp | Static assets, cache policy, and SvelteKit dynamic-route fallback |
internal/adapters/postgres | PostgreSQL cross-replica notification listener |
internal/adapters/memory | Process-local publish/subscribe event hub |
internal/database | Connection configuration and embedded Goose migrations |
Domain aggregate
domain.Room is the consistency boundary. It contains:
- room metadata and invite code;
- current topic and card deck;
- participants and their votes;
- named spectators who receive room updates without voting;
- reveal state and monotonic version;
- every revealed result for the room lifetime, hydrated from normalized projections;
- terminal session state and the final statistical summary.
Domain methods enforce these rules:
- selected cards must belong to the configured deck;
- votes cannot change after reveal;
- spectators cannot vote and are excluded from revealed-round records, statistics, and awards;
- only the facilitator can reveal, reset, or change the topic;
- changing the topic resets the current round;
Room.Snapshotproduces the transport-safe representation, omitting credentials, history, and unrevealed values while preservinghasVote.- only the facilitator can end a session, and ending permanently locks room mutations.
The final summary excludes ? and missing votes from numeric calculations. It reports participant averages, median proximity, participation, uncertainty, and pre-reveal card revisions, plus room consensus. Awards include every tied participant. Commands produce versioned domain events and the same reducer is used for live changes and replay.
Each demo-team command adds one named demonstration participant, up to the available pool of 16 names, and schedules that participant's vote. It is not a production presence simulation.
HTTP API
| Method | Path | Identity | Description |
|---|---|---|---|
GET | /api/health | None | Liveness response |
POST | /api/rooms | None | Create a room and voting or spectator facilitator |
POST | /api/rooms/{code}/join | None | Join with a display name and optional spectator role |
GET | /api/rooms/{code} | Participant cookie | Get the room snapshot |
GET | /api/rooms/{code}/session | Participant cookie | Restore the verified participant ID and room snapshot |
POST | /api/rooms/{code}/vote | Participant cookie | Submit or replace a vote |
POST | /api/rooms/{code}/reveal | Participant cookie | Reveal the current round |
POST | /api/rooms/{code}/end | Participant cookie | End and summarize the session |
POST | /api/rooms/{code}/reset | Participant cookie | Start another round |
POST | /api/rooms/{code}/topic | Participant cookie | Change the topic and reset |
POST | /api/rooms/{code}/demo-team | Participant cookie | Add demonstration teammates |
GET | /events/rooms/{code} | Participant cookie | Open the SSE stream |
GET | /ws/rooms/{code} | Participant cookie | Open a WebSocket session |
Create and join return:
{
"room": { "code": "A1B2C3", "version": 1 },
"participantId": "public-participant-id"
}Create and join also set a room-specific HttpOnly, SameSite=Strict participant cookie. It is Secure by default. Browsers include it automatically with commands and WebSocket handshakes; JavaScript and API response bodies never receive the credential. State-changing requests with a mismatched browser Origin are rejected. Public snapshots contain only the separate participant ID used for display and summary references.
Create and join accept "spectator": true for a named spectator. A spectator facilitator still owns all room and session controls but cannot vote and is excluded from results. Omitting spectator, or setting it to false, preserves the normal voting-participant behavior. Spectator identities can open either realtime transport but vote commands are forbidden.
Errors
Handlers return JSON in this shape:
{ "error": "only the facilitator can do that" }Missing rooms return 404, forbidden operations return 403, and invalid commands/state return 400. Ending requires a revealed round; ended rooms reject joins and all further mutations.
Runtime configuration
| Variable | Default | Description |
|---|---|---|
ADDRESS | :8080 | Backend listen address |
DATABASE_DRIVER | sqlite | sqlite or postgres |
DATABASE_PATH | data/data.db | SQLite file path; use :memory: for ephemeral mode |
DATABASE_URL | Local PostgreSQL DSN | PostgreSQL connection string |
DATABASE_AUTO_MIGRATE | true | Apply pending migrations before the server starts; set to false when migrations run as a deployment step |
DATABASE_MAX_OPEN_CONNECTIONS | 25 | PostgreSQL maximum open connections |
DATABASE_MAX_IDLE_CONNECTIONS | 10 | PostgreSQL maximum idle connections |
DATABASE_CONNECTION_MAX_LIFETIME | 30m | PostgreSQL connection maximum lifetime |
DATABASE_CONNECTION_MAX_IDLE_TIME | 5m | PostgreSQL connection maximum idle time |
AUDIT_HMAC_KEY | Random per process | Shared key for stable redacted audit references; required when DEPLOYMENT_ENVIRONMENT=production |
ROOM_IDLE_TTL | 6h | Delete rooms after this duration without a persisted event |
ROOM_ENDED_TTL | 1h | Delete rooms after this duration since session end; the earlier retention deadline wins |
ROOM_PARTICIPANT_LIMIT | 50 | Maximum participants, including demo teammates, in a room |
COOKIE_SECURE | true | Restrict participant cookies to HTTPS; set to false only for local HTTP development |
STATIC_DIR | Unset | Compiled SvelteKit directory; the image uses /app/public |
The server handles SIGINT and SIGTERM and closes the HTTP server and repository during shutdown.