Skip to content

Backend ​

The backend is a Go module under backend/.

Package map ​

PathResponsibility
cmd/serverComposition root, environment configuration, HTTP server lifecycle
cmd/migrateForward migration, schema status, and version operations
cmd/rebuildOffline rebuild of current and reporting projections from room events
internal/domainEvent-sourced room aggregate, reducer, participant/round types, and planning-poker rules
internal/portsEvent-store repository and room-notification interfaces
internal/applicationCommand orchestration, optimistic retries, and snapshot publication
internal/adapters/eventstoreDialect-aware transactional event store, current/round projections, and rebuild tooling
internal/adapters/httpapiHTTP/JSON, SSE, and WebSocket adapters
internal/adapters/webappStatic assets, cache policy, and SvelteKit dynamic-route fallback
internal/adapters/postgresPostgreSQL cross-replica notification listener
internal/adapters/memoryProcess-local publish/subscribe event hub
internal/databaseConnection 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.Snapshot produces the transport-safe representation, omitting credentials, history, and unrevealed values while preserving hasVote.
  • 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 ​

MethodPathIdentityDescription
GET/api/healthNoneLiveness response
POST/api/roomsNoneCreate a room and voting or spectator facilitator
POST/api/rooms/{code}/joinNoneJoin with a display name and optional spectator role
GET/api/rooms/{code}Participant cookieGet the room snapshot
GET/api/rooms/{code}/sessionParticipant cookieRestore the verified participant ID and room snapshot
POST/api/rooms/{code}/voteParticipant cookieSubmit or replace a vote
POST/api/rooms/{code}/revealParticipant cookieReveal the current round
POST/api/rooms/{code}/endParticipant cookieEnd and summarize the session
POST/api/rooms/{code}/resetParticipant cookieStart another round
POST/api/rooms/{code}/topicParticipant cookieChange the topic and reset
POST/api/rooms/{code}/demo-teamParticipant cookieAdd demonstration teammates
GET/events/rooms/{code}Participant cookieOpen the SSE stream
GET/ws/rooms/{code}Participant cookieOpen a WebSocket session

Create and join return:

json
{
  "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:

json
{ "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 ​

VariableDefaultDescription
ADDRESS:8080Backend listen address
DATABASE_DRIVERsqlitesqlite or postgres
DATABASE_PATHdata/data.dbSQLite file path; use :memory: for ephemeral mode
DATABASE_URLLocal PostgreSQL DSNPostgreSQL connection string
DATABASE_AUTO_MIGRATEtrueApply pending migrations before the server starts; set to false when migrations run as a deployment step
DATABASE_MAX_OPEN_CONNECTIONS25PostgreSQL maximum open connections
DATABASE_MAX_IDLE_CONNECTIONS10PostgreSQL maximum idle connections
DATABASE_CONNECTION_MAX_LIFETIME30mPostgreSQL connection maximum lifetime
DATABASE_CONNECTION_MAX_IDLE_TIME5mPostgreSQL connection maximum idle time
AUDIT_HMAC_KEYRandom per processShared key for stable redacted audit references; required when DEPLOYMENT_ENVIRONMENT=production
ROOM_IDLE_TTL6hDelete rooms after this duration without a persisted event
ROOM_ENDED_TTL1hDelete rooms after this duration since session end; the earlier retention deadline wins
ROOM_PARTICIPANT_LIMIT50Maximum participants, including demo teammates, in a room
COOKIE_SECUREtrueRestrict participant cookies to HTTPS; set to false only for local HTTP development
STATIC_DIRUnsetCompiled SvelteKit directory; the image uses /app/public

The server handles SIGINT and SIGTERM and closes the HTTP server and repository during shutdown.

Backlog Hold'em project documentation