TechRace is a geo-location-based scavenger hunt with two Flutter apps (Participant, Volunteer/Admin), a Node.js/Express backend in TypeScript, and Firebase for Auth + data. This README serves as an educational guide, operational runbook, and technical setup reference.
- Core Stack
- Frontend: Flutter (GetX, Flavors for Participant vs Volunteer)
- Backend: Node.js/Express + TypeScript
- Firebase: Auth, Firestore (Cold Data), Realtime Database (Hot Data)
- Docker Strategy
- Dev: docker-compose with volume mounts and local env loading
- Prod: Multi-stage Dockerfile (Builder → Runner) with Railway environment variables
- Hybrid Database
- Firestore: Clue text, hints, image URLs (cold, cost-optimized)
- RTDB: Coordinates, team scores/state (hot, low-latency)
- Meter Checks read only lat/long from RTDB for speed
flowchart LR
subgraph Mobile
P[Participant App]:::app
V[Volunteer/Admin App]:::app
end
P -->|JWT + HTTPS| B[Backend API<br/>Node/Express]:::svc
V -->|JWT + HTTPS| B
B -->|Auth| A[Firebase Auth]:::fb
B -->|Cold Data| F[(Firestore<br/>clues, hints, urls)]:::fb
B -->|Hot Data| R[(Realtime DB<br/>coords, team state/scores)]:::fb
classDef app fill:#77c,stroke:#333,color:#fff
classDef svc fill:#6b8,stroke:#333,color:#fff
classDef fb fill:#f9a,stroke:#333,color:#333
- Latency: Meter checks and leaderboard rely on rapid reads/writes; RTDB suits bursty, low-bandwidth polling.
- Cost: Content (text/hints/images) changes rarely; Firestore’s document model is cheaper to store and read infrequently.
- Simplicity: Coordinates in RTDB enable straight-line distance checks without fetching large documents.
References:
- Dev Compose: docker-compose.yml
- Dev Dockerfile: Dockerfile.dev
- Prod Dockerfile: Dockerfile
- Firebase Admin bootstrap: firebase.ts
- Hybrid clue write: gameController:addClue
- Meter read from RTDB: getMeterReading
graph TD
F[(Firestore)]:::fb -->|read once per clue| APP[Apps via Backend]:::svc
R[(RTDB)]:::fb -->|meter polling & leaderboard| APP
subgraph Data Split
COLD[Cold: clue text, hints, media URLs]
HOT[Hot: lat/long, team state, scores]
end
COLD --> F
HOT --> R
classDef fb fill:#f9a,stroke:#333,color:#333
classDef svc fill:#6b8,stroke:#333,color:#fff
-
Prerequisites
- Flutter SDK
- Node.js v20+
- Firebase CLI (
npm i -g firebase-tools)
-
Backend (Local Dev with Docker)
- Enter backend:
cd techrace_backend - Copy env: duplicate .env.example to
.envand fill keys - Compose up:
docker compose up --build- Volumes mount
./for hot reloading; runsnpm run dev - Reads
.envvia compose env_file
- Volumes mount
- Entrypoints and routes:
- App: app.ts
- Auth: authRoutes.ts
- Game: gameRoutes.ts
- Admin: adminRoutes.ts
- Enter backend:
-
Backend (Production)
- Multi-stage build produces slim image
- Secrets:
.envis ignored in build via .dockerignore; use Railway variables - Deploy: GitHub Actions → Railway (manual or on push)
- Workflow: deploy_backend.yml
-
Flutter App Setup
- Enter app:
cd techrace - Configure Firebase:
flutterfire configure(generateslib/firebase_options.dart) - Install deps:
flutter pub get - Run flavors:
- Participant:
flutter run --flavor participant -t lib/main_participant.dart(main_participant.dart) - Volunteer/Admin:
flutter run --flavor volunteer -t lib/main_volunteer.dart(main_volunteer.dart)
- Participant:
- Enter app:
-
Build APKs (Local)
- Participant:
flutter build apk --flavor participant -t lib/main_participant.dart - Volunteer:
flutter build apk --flavor volunteer -t lib/main_volunteer.dart
- Participant:
-
Pre-Event: Bulk Load via Python
- Location: python_scripts
- Requirements:
pip install pandas requests - Upload Teams:
python upload_teams.py(upload_teams.py)- Reads
teams.csv, logs in as Admin (/api/admin/login), posts to/api/user/add-user(userController)
- Reads
- Upload Clues:
python upload_clues.py(upload_clues.py)- Reads
clues.csv, posts to/api/game/add-clueto write cold (Firestore) + hot (RTDB) (addClue)
- Reads
- Configuration Source
- Default scripts read
BASE_URL+ admin creds inline; for robustness prefer env variables on the machine running uploads
- Default scripts read
-
During Event: Monitoring & Validation
- Firebase Console
- RTDB nodes to watch:
teams/*(state, balance, last_lat/last_lng),clues/*(lat/long),base_url
- RTDB nodes to watch:
- Admin/Volunteer App
- Login fetches
base_urlfrom RTDB and uses backend APIs (volunteer_auth_controller.dart) - Leaderboard pulls via backend and listens to
teams(desk_controller.dart)
- Login fetches
- Clue Validation
- Validators confirm team proximity to target using backend (validateTeam)
- Firebase Console
-
Emergency: Force Logout
- Current API:
POST /api/auth/logoutupdates Firestoreis_logged_in=falseand optional last lat/lng (authController) - Recommended: Admin-protected endpoint
/api/admin/force-logoutto avoid unauthenticated logout attempts
- Current API:
-
Meter Check Performance
- Reads only lat/long from RTDB for fast, lightweight distance computation (getMeterReading)
- Backend throttles writes unless movement > 10m to reduce noisy updates
-
Anti-Cheat Logic
- Login gate checks large instantaneous movement vs last known point and denies suspicious jumps (loginTeam)
- Soft Session Lock: JWT valid for 24h; admins can log out teams if devices fail
-
Firebase Rules (Policy)
- Participants should not read
clues/*in RTDB directly; only backend does - Participants may read limited team state under
teams/{tid}necessary for UX - Admin roles can read/write administrative nodes:
base_url,last_clue, validations - Firestore access for clue content should be backend-only
- Participants should not read
- Backend Deploy
- GitHub Actions: deploy_backend.yml pushes to Railway
- Railway token stored as GitHub Secret
RAILWAY_TOKEN
- Mobile Build & Release
- Local builds for Participant and Volunteer flavors with
flutter build apk --flavor ... - Optional CI: add a
workflow_dispatch-based job to build and upload signed artifacts per flavor
- Local builds for Participant and Volunteer flavors with
- Align Speed Logic
- Current: distance jump threshold at login
- Improve: compute speed = distance / time between last_seen and now; deny >20 m/s consistently
- Secure Logout
- Current:
/api/auth/logoutis unauthenticated - Improve: require JWT tied to the team, and add admin-only
/api/admin/force-logout
- Current:
- RTDB
base_urlHardening- Risk: if
base_urlis modified by an attacker, Volunteers could be redirected - Improve: restrict RTDB write to admins only; validate HTTPS + whitelist domains
- Risk: if
- Firebase Rules
- Enforce backend-only access to Firestore clues; block participant reads of RTDB
clues/* - Rate-limit writes to
teams/*to reduce spam updates
- Enforce backend-only access to Firestore clues; block participant reads of RTDB
- Admin Password Rotation
- Current: rotation on each login can break ongoing scripted uploads
- Improve: rotate on schedule or via explicit action; scripts read creds from env or service account
- Secrets Hygiene
- Ensure
google-services.jsonis not committed or that API keys have strict usage restrictions
- Ensure
- Server-Side Distance & Clue Access
- Move all distance computation and clue access to backend; clients only display results
- Observability
- Add structured logs and dashboards for meter rates, validation errors, bans, freezes
- Backend Dev Compose: docker-compose.yml
- Dockerfiles: Dockerfile.dev, Dockerfile
- Firebase Admin init: firebase.ts
- Key Controllers: authController.ts, gameController.ts, adminController.ts
- Flutter Flavor Entrypoints: main_participant.dart, main_volunteer.dart