Architecture¶
Pollux is a single-file Bun server with a modular route structure. This page explains how the pieces fit together.
Overview¶
Everything runs in a single Bun.serve process. HTTP handlers and the WebSocket
handler share two stores: a persistent SQLite database and a set of
ephemeral in-memory maps (see State & data model).
flowchart LR
A["HTTP Request"] --> B["Bun.serve"]
A2["WS Upgrade"] --> B
B --> C["Route handlers<br/>(src/routes/*.ts)"]
B --> D["WebSocket handler<br/>(ws.ts)"]
subgraph state["Shared state"]
E[("SQLite · db.ts<br/><i>votes, quizz data</i>")]
M["In-memory Maps · helpers.ts<br/><i>dynamic / quizz / raffle config</i>"]
end
C <--> E
C <--> M
D <--> E
D <--> M
C -->|"srv.publish()"| CH(("Topic channel"))
CH --> F["Connected clients"]
D <--> F
Project structure¶
src/
index.ts — Server entry point, assembles routes
db.ts — SQLite queries (poll, quizz tables)
routes/
helpers.ts — Shared types, maps, response helpers
pages.ts — Static HTML page routes
assets.ts — CSS/JS asset routes
uuid.ts — UUID generation endpoint
vote.ts — Vote submission and results
dynamic.ts — Dynamic poll step management
quizz.ts — Quiz creation, voting, scoring
raffle.ts — Raffle registration, spin, status
ws.ts — WebSocket handler + upgrade logic
scoring.ts — Pure quiz-scoring logic (speed bonus)
raffle-logic.ts — Pure pseudo generation + winner draw
layout/ — HTML page templates
scripts/ — Client-side JavaScript
styles/ — CSS stylesheets
Domain logic that would otherwise be buried in route handlers lives in its own
pure, dependency-free modules (scoring.ts, raffle-logic.ts) so it can be
unit-tested in isolation.
Routing¶
Pollux uses Bun's built-in route matching (Bun.serve with routes option). Routes are defined as an object where keys are URL patterns and values are handler functions or method-specific handler objects.
Route handlers are extracted into separate files under src/routes/ for maintainability. Each file exports an object that is spread into the main routes object in index.ts.
Request lifecycle¶
A mutating request (a vote) never replies with the new results directly — it persists, then lets the WebSocket fan-out deliver the update to everyone, including the caller.
sequenceDiagram
actor Voter
participant H as Route handler
participant DB as SQLite
participant CH as Topic channel
participant Clients as All subscribers
Voter->>H: POST /api/vote
H->>H: guardUUID (422 on bad id)
H->>DB: prepared INSERT
H-->>Voter: 201 Created
H->>CH: srv.publish(channel, results)
CH-->>Clients: { result: [...] }
Note over Clients: every open page re-renders
Every endpoint validates the poll id with guardUUID / isUUIDv7 (UUIDv7-only)
and returns 422 on mismatch. Handlers build responses through the shared
helpers in helpers.ts (json, invalid, notFound, html, js) so CORS
headers stay consistent.
Database¶
Pollux uses SQLite via bun:sqlite with WAL mode for better concurrent access. The database file is stored at data/db.sqlite.
Tables¶
erDiagram
poll {
string uuid PK
int choice
datetime date
}
quizz_answers {
string uuid PK
int step PK
string correct
string question
int timer
string choices
string media
}
quizz_submissions {
string uuid PK
string user_id PK
int step PK
string choices
int score
int total
int response_time_ms
}
quizz_players {
string uuid PK
string user_id PK
string pseudo
}
Only votes and quizz data are persisted here. Raffle state (players and winner) lives only in memory and is lost on restart — see State & data model for the full picture and for the choice-encoding scheme used by multi-round polls.
Data older than 4 hours is cleaned up automatically via a cron job.