docker run -p 8080:80 --rm ttrpg-initiative-tracker
```
Full-stack docker-compose arrives in M5.
## Backend architecture
Server-authoritative. Kills client-side last-write-wins races (root cause of skip bug).
```
Client (browser) Server
| |
|-- POST /api/.../nextTurn ----->| action
| | store.nextTurn():
| | shared.nextTurn(enc) -> patch
| | db tx: apply patch
| | addLog
| | broadcast(change)
|<---- WS {change} --------------| push to all subscribers
| |
Display / tablet |
|<---- WS {change} --------------| same push
```
- **SQLite** owns truth. Single writer (server). WAL mode.
- **shared/turn.js** = pure logic, ported verbatim from `App.js`. Bugs preserved for M3 characterization, fixed in M4.
- **WS** = real-time push (replaces Firebase `onSnapshot`). Client subscribes to a key (`'campaigns'`, `'encounter:id'`, `'activeDisplay'`...), server pushes on change.
- **Actions not results.** Client sends "do X", server computes X, persists, broadcasts. No client-side state mutation.
## Storage backend choice
Browser sandbox cannot touch filesystem. Cross-device (DM + tablet + player view) requires a real backend owning the DB file. SQLite = single file, docker volume, trivial backup. Postgres deferred until public multiuser exposure.