turniere-backend/doc/leader_follower.md

6.5 KiB

Leader/Follower Tournament Sync

Backend supports one-way tournament replication:

  • leader tournament stays writable
  • follower tournament stays read only
  • leader pushes full snapshots asynchronously
  • follower never pulls
  • follower never needs to reach leader

This fits live topology:

  • leader = local laptop / Raspberry Pi / LAN host without public IP
  • follower = internet VM with public HTTPS endpoint
  • only leader needs outbound access to follower

Live Setup

1. Create follower tournament on remote backend

Create empty tournament with:

  • read_only_mode: true
  • sync_auth_token: <shared secret>

Do not send teams payload for follower. Backend allows empty read only follower tournament creation. The token is write-only. Backend responses never return it; owner responses only expose sync_auth_configured.

2. Create normal leader tournament on local backend

Create tournament as usual. This remains normal writable tournament.

3. Configure leader to push to follower

Update leader tournament with:

  • sync_target_url: https://remote.example/tournaments/:id/sync_state
  • sync_auth_token: <same shared secret as follower>

Leader becomes push source. Follower accepts only authenticated snapshot pushes.

4. Run tournament normally on leader

These leader-side actions enqueue async snapshot push:

  • tournament updates
  • timer updates
  • team renames
  • match state changes
  • match score changes
  • stage finish / playoff generation

Queue retries with backoff until follower accepts snapshot.

Local Manual Testing

Backend now has local manual setup target:

inv docker-blackbox-follow-manual-up

This starts:

  • leader production image on http://127.0.0.1:3002
  • follower production image on http://127.0.0.1:3003
  • separate leader and follower Postgres containers
  • bootstrapped owner users on both backends

Teardown:

inv docker-blackbox-follow-down

Local topology

Compose setup now separates DB traffic:

  • source-postgres only on source-db internal network
  • follower-postgres only on follower-db internal network
  • both apps share only sync network

This gives:

  • separate production app containers
  • separate production DB containers
  • DB isolation between leader and follower

Important limit:

  • Docker Compose does not enforce true one-way HTTP reachability here
  • local setup still allows follower container to reach leader container over shared sync network
  • real production trust model still comes from backend design:
    • leader pushes
    • follower never pulls
    • follower has no code path that contacts leader

So local setup is good for frontend/manual testing of behavior, not strict network-policy simulation.

Manual frontend testing flow

  1. Run inv docker-blackbox-follow-manual-up
  2. Open leader backend at http://127.0.0.1:3002
  3. Open follower backend at http://127.0.0.1:3003
  4. Log in with:
    • email: e2e@example.com
    • password: password123
  5. On follower backend, create empty read-only follower tournament
  6. On leader backend, create normal tournament
  7. On leader tournament, set:
    • sync_target_url to follower PATCH /tournaments/:id/sync_state
    • sync_auth_token to same shared token used on follower
  8. Point local frontend leader instance to http://127.0.0.1:3002
  9. Point local frontend follower/remote instance to http://127.0.0.1:3003
  10. Progress tournament on leader, verify follower mirrors state and stays read only

Frontend team can use this setup to build:

  • owner sync configuration UI
  • follower read-only UX
  • status display for sync_last_pushed_at
  • status display for sync_last_push_error
  • takeover flow by disabling read_only_mode

Takeover

If remote must become writable after sync phase:

  1. disable read_only_mode on follower
  2. backend clears follower token when no longer needed
  3. follower becomes normal standalone tournament again

After takeover, leader should no longer push to old follower URL.

API Contract

Leader-side writable config

Tournament push mode active when:

  • sync_target_url present
  • sync_auth_token present
  • read_only_mode false

Follower-side push receiver

Tournament accepts push when:

  • read_only_mode true
  • sync_auth_token present

Push endpoint:

  • PATCH /tournaments/:id/sync_state

Auth:

  • Authorization: Bearer <token>

Payload:

  • { snapshot: ... }

Snapshot import replaces tournament graph using sync_source_id mapping, not local DB ids.

Frontend Integration Notes

Frontend does not need separate "follower UI mode" for normal viewers. Remote tournament should look same for non-owner users.

Owner-facing frontend can expose:

  • read_only_mode toggle
  • sync_target_url input
  • sync_auth_token input
  • sync health display:
    • sync_last_pushed_at
    • sync_last_push_error

Recommended owner UX:

  1. generate a shared token outside the backend
  2. create remote follower with that sync_auth_token
  3. paste same token and remote sync_state URL into leader
  4. save leader sync config
  5. show last push time / error state
  6. allow explicit follower takeover by disabling read_only_mode

Frontend should block owner edits on follower when read_only_mode true. Backend already enforces this with 423 Locked.

Future Feature Rule

Any new persisted tournament state must be handled in three places:

  1. leader write path
  2. snapshot export/import path
  3. follower/read-only behavior

Guardrail now exists:

  • TournamentSyncSchema lists every synced and ignored model column
  • spec fails when new DB column appears without explicit sync decision

This does not auto-implement sync for new features. It makes missing sync work fail loudly in tests instead of silently drifting.

For every new tournament feature, add:

  1. schema decision in TournamentSyncSchema
  2. snapshot builder/importer support if state must replicate
  3. one follower lifecycle test or roundtrip spec that proves behavior survives sync

Coverage Today

Current backend coverage includes:

  • async push queue and retry behavior
  • follower auth and read-only lock
  • full follower lifecycle HTTP E2E with unrelated tournaments and mismatched ids
  • team renames
  • multiple decider matches
  • playoff generation blocked before deciders finish
  • playoff generation after deciders finish
  • follower takeover
  • large roundtrip graph coverage for 32 groups, 4 teams each, 64-team playoffs

Still not magic:

  • if new feature adds new state and nobody updates sync contract, tests will fail
  • if new feature changes behavior but not schema, add behavior test too