Skip to content

Realtime turn notifications via Firestore, auto-resolve on last submit#169

Open
tsg21 wants to merge 20 commits into
mainfrom
firebase-notifications
Open

Realtime turn notifications via Firestore, auto-resolve on last submit#169
tsg21 wants to merge 20 commits into
mainfrom
firebase-notifications

Conversation

@tsg21

@tsg21 tsg21 commented May 25, 2026

Copy link
Copy Markdown
Owner

Summary

  • Auto-resolve: POST /commands now resolves the turn synchronously when the last player submits; the separate POST /resolve endpoint is removed
  • Firestore realtime: frontend subscribes to games/{gameId} via onSnapshot — no more 10s polling; both turn advancement and per-player submission status update in real time
  • Firebase Auth: new POST /api/v1/auth/firebase-token endpoint mints custom tokens with games claims; useFirebaseAuth hook manages token lifecycle and proactive refresh
  • Infrastructure: Terraform adds Firestore, Firebase project, and IAM grants; docker-compose adds firebase-emulators for local dev
  • Metadata consolidation: meta.json.gz replaced entirely by Firestore; GameDirectory abstraction owns game-summary data

Test plan

  • cd backend && uv run pytest — 611 unit tests pass
  • cd backend && uv run ruff check . && uv run ruff format --check . — clean
  • ./backend/int_tests/run.sh — 36 integration tests pass (int_tests docker-compose extended with firebase-emulators)
  • cd frontend && npm run typecheck && npm run lint && npm test — 355 tests pass, no type or lint errors
  • Manual smoke skipped (per AGENTS.md preference — not explicitly requested)

Notes

  • Deploy order: (1) terraform apply → Firestore exists; (2) ship backend + frontend together; (3) optionally delete orphaned meta.json.gz blobs in GCS
  • Pre-existing prod games will not appear in the lobby after cutover (no migration; documented in PRD 06 and task blast-radius notes)

🤖 Generated with Claude Code

tsg21 and others added 14 commits May 21, 2026 22:11
…ions

- PRD 03: document auto-resolution on last submission, turn-0 race selection path
- PRD 50: remove POST /resolve; update POST /commands response (turn_resolved, new_turn);
  add POST /auth/firebase-token endpoint; update WebSockets design decision; update
  GET /games and GET /games/{id} Firestore sourcing note
- PRD 06: add Firestore/GameDirectory section with doc model, security rules, write paths;
  remove meta.json.gz from bucket layout; update Storage Adapter Contract; add Firebase
  custom token auth section; update Local Development with emulator env vars; update
  architecture diagram; remove WebSockets from out-of-scope

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
… deps

- New package backend/openstars/game_directory/ with:
  - base.py: GameDirectory ABC, GameSummary Pydantic model (incl. all_turns_submitted property)
  - memory.py: InMemoryGameDirectory for unit tests / memory dev mode
  - firestore.py: FirestoreGameDirectory wrapping firebase_admin Firestore client
  - factory.py: build_game_directory() reading GAME_DIRECTORY_BACKEND env var
- backend/openstars/server/deps.py: add get_game_directory() FastAPI dependency
- backend/pyproject.toml: add firebase-admin dependency
- tests/server/test_game_directory.py: 19 unit tests covering factory, memory impl,
  GameSummary.all_turns_submitted, and Firestore mock assertions

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…to game lifecycle

POST /commands now resolves the turn synchronously when the last player submits
(no separate POST /resolve endpoint). The GameDirectory abstraction is wired into
all game-lifecycle routes (create, list, get, commands, designs, race) so that
game-summary metadata flows through the directory rather than meta.json.gz.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Mints a Firebase custom token with a `games` claim listing the caller's
game IDs, so the frontend can subscribe to Firestore game documents.
Warns when the games list hits the 200-entry cap to flag potential truncation.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…layer

Game listing and metadata are now owned entirely by GameDirectory (Firestore /
InMemory). The meta.json.gz blob format and the storage methods that read/write
it are gone. Existing GCS meta blobs become harmless orphans.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- provider.tf: add google-beta provider (required for Firestore/Firebase resources)
- firebase.tf: enable Firestore/Firebase/IdentityToolkit APIs, create native-mode
  Firestore DB in eur3, deploy security rules restricting client reads to token-
  scoped game IDs
- firestore.rules: clients can only read games listed in their custom-token claim
- iam.tf: grant backend SA roles/datastore.user and self-referencing
  roles/iam.serviceAccountTokenCreator (needed for signing Firebase custom tokens)
- cloud_run.tf: add GAME_DIRECTORY_BACKEND=firestore and FIREBASE_PROJECT_ID
  env vars to the backend Cloud Run service

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- firebase.json: emulator config for auth (9099), firestore (8085), UI (4001)
- .firebaserc: pin demo project id openstars-local for emulator CLI
- docker-compose.yaml: add firebase-emulators service; wire backend to emulators
  with GAME_DIRECTORY_BACKEND=firestore and emulator host env vars; add Firebase
  env vars to frontend for emulator mode
- AGENTS.md: document emulator ports, full-stack compose command, and env vars

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Initialises Firebase app, Auth, and Firestore with env-var config and
optional emulator connections for local dev.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Hook fetches a custom token, signs into Firebase Auth, reads game claims,
and schedules proactive token refresh before expiry.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Subscribes to games/{gameId} via onSnapshot; fires onTurnAdvanced when
current_turn increases and onSubmissionsChanged on every snapshot update.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Replaces 10s setTimeout polling and manual resolve button with Firestore
realtime listener; submit eagerly reloads when turn resolves immediately.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Removes calls to the deleted POST /resolve endpoint from all integration
tests; extend int_tests docker-compose with firebase-emulators service;
all 36 integration tests pass against the emulator stack.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@tsg21

tsg21 commented May 25, 2026

Copy link
Copy Markdown
Owner Author

@codex review

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: dc4c3234eb

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread backend/openstars/server/routes/auth.py
);

return unsubscribe;
}, [gameId, auth]);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Stabilise listener dependencies to avoid resubscribing each render

The Firestore subscription effect depends on auth as an object, and useFirebaseAuth returns a fresh object every render; this causes onSnapshot to be torn down and recreated on unrelated UI state changes (for example command edits), which can increase read churn and create gaps where updates are missed. Depend on stable primitives/callbacks (or memoise the auth object) so the listener only reattaches when auth state actually changes.

Useful? React with 👍 / 👎.

tsg21 and others added 6 commits May 25, 2026 09:45
firebase_admin.initialize_app() with no credential falls through to
Application Default Credentials (ADC), which don't exist in CI. The
Firestore emulator bypasses auth entirely, so ADC are never actually
used for requests — but firebase_admin still calls get_credential()
during client construction, causing a 500 on every create_game call.

When FIRESTORE_EMULATOR_HOST is set, supply a minimal _EmulatorCredential
stub that returns AnonymousCredentials instead of trying to load ADC.
Production paths (no emulator host) continue to use ADC as before.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…r deps

P1 auth.py: count-based truncation at 200 game IDs does not prevent the
Firebase custom-claims 1 000-byte limit from being exceeded (e.g. 200 UUIDs
exceed it many times over). Replace with _fit_game_ids(), which greedily
builds the longest prefix whose JSON-encoded {"games":[...]} payload fits
within 960 bytes. Also rename _GAMES_LIMIT to _GAMES_FETCH_LIMIT to
clarify its role, and add unit tests for the helper.

P2 useGameNotifications.ts: the useEffect dep on the whole auth object
caused onSnapshot to be torn down and recreated on every unrelated render
because useFirebaseAuth returns a fresh object reference each time.
Destructure stable primitives (claims from React state, refresh from
useCallback) and depend on those instead.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant