- TypeScript 99.5%
- Shell 0.3%
- Dockerfile 0.1%
| drizzle | ||
| http | ||
| public | ||
| scripts | ||
| src | ||
| .dockerignore | ||
| .env.example | ||
| .gitignore | ||
| .nvmrc | ||
| .prettierignore | ||
| .prettierrc | ||
| AGENTS.md | ||
| docker-compose.yml | ||
| Dockerfile | ||
| drizzle.config.ts | ||
| eslint.config.mjs | ||
| IMPLEMENTATION_PLAN.md | ||
| next-env.d.ts | ||
| next.config.ts | ||
| package-lock.json | ||
| package.json | ||
| PARKED_IDEAS.md | ||
| postcss.config.mjs | ||
| publish-qnap.sh | ||
| README.md | ||
| SPEC.md | ||
| tsconfig.json | ||
| vitest.config.ts | ||
| vitest.integration.config.ts | ||
Family Bet
Family World Cup prediction app. SPEC.md is the product source of truth and IMPLEMENTATION_PLAN.md tracks phased delivery.
Stack
- Next.js App Router with TypeScript
- Tailwind CSS, without a UI component library for MVP
- Drizzle ORM with SQLite
- Vitest
- Docker Compose deployment with SQLite at
/data/family-bet.sqlite
Local Setup
Install dependencies after Node/npm are available:
nvm use
npm install
Copy environment defaults:
cp .env.example .env
Local development defaults to ./data/family-bet.sqlite. Docker Compose overrides this to /data/family-bet.sqlite on a persistent volume.
Commands
npm run dev
npm run build
npm run lint
npm run format:check
npm run format
npm run typecheck
npm test
npm run test:integration
npm run db:generate
npm run db:migrate
npm run db:backup:prod
npm run db:copy-prod-to-local
npm run db:backup:prod copies /mnt/qnap-ssd/family-bet/data/family-bet.sqlite to ~/BackUps/family-bet-db/family-bet_yyyy_MM_dd_HH_mm_ss.sqlite.
npm run db:copy-prod-to-local copies the production database to the local development database at ./data/family-bet.sqlite. If a local database already exists, it is first backed up to ~/BackUps/family-bet-db/local-family-bet_yyyy_MM_dd_HH_mm_ss.sqlite.
Docker
Build the image locally:
docker build -t family-bet:latest .
Run with Compose:
docker compose up --build
The Compose deployment stores SQLite at /data/family-bet.sqlite on the family-bet-data volume and exposes the container on ${APP_PORT:-3000}.
Required deployment environment variables:
APP_URL: public application URL, for examplehttps://bets.example.combehind a TLS reverse proxy.AUTH_SECRET: long random secret for auth/session signing inputs.TZ: application timezone used for displaying match times, for exampleEurope/Zurich.APP_PORT: host port for Docker Compose, defaults to3000.DATABASE_PATH: SQLite database path. Compose sets this to/data/family-bet.sqlite.SECRETS_ENCRYPTION_KEY: long random key used to encrypt provider API keys stored by admins. If omitted, the app derives an encryption key fromAUTH_SECRET; this is convenient for existing deployments but couples session-secret and provider-key rotation.NEXT_TELEMETRY_DISABLED: set to1to disable Next.js telemetry. npm scripts and Compose set this by default.SYNC_ENABLED: set totrueto enable automatic provider fixture/result sync. Defaults to disabled.API_FOOTBALL_API_KEY: optional bootstrap/fallback API-Football/API-Sports token. Admin-managed DB credentials take precedence when configured.FOOTBALL_DATA_API_KEY: optional bootstrap/fallback football-data.org token. Admin-managed DB credentials take precedence when configured.FOOTBALL_DATA_COMPETITION_CODE: optional bootstrap/fallback provider competition code, defaults toWC.API_FOOTBALL_ENABLED: set totrueto enrich completed imported matches with final goal events from API-Football. Defaults to disabled.API_FOOTBALL_LEAGUE_ID: optional bootstrap/fallback API-Football league id, defaults to1for the World Cup.API_FOOTBALL_SEASON: optional bootstrap/fallback API-Football season, defaults to2026.API_FOOTBALL_DAILY_CALL_CAP: local safety cap for API-Football calls, defaults to7000.SYNC_FIXTURES_INTERVAL_HOURS: fixture import interval, defaults to24.SYNC_RESULTS_INTERVAL_MINUTES: result check interval, defaults to10.
Production should be served over HTTPS by the reverse proxy so secure session cookies work correctly.
Provider Sync
Automatic sync is opt-in. Set SYNC_ENABLED=true and configure provider credentials at /admin/sync to start the in-app scheduler inside the Docker process. Environment provider variables remain bootstrap/fallback values when DB credentials are absent. If API-Football is not configured, the scheduler falls back to football-data.org when that provider has a configured key.
Provider credential rotation:
- Update provider API keys and provider-specific config at
/admin/sync. - Stored API keys are encrypted in SQLite and are never displayed back to admins.
- Entering a new API key replaces the stored key; leaving the field blank keeps the current stored key.
- Clearing a stored key makes the app use the environment fallback if one exists.
- Prefer setting
SECRETS_ENCRYPTION_KEYbefore storing keys. If it is omitted, encrypted provider keys depend onAUTH_SECRET; rotatingAUTH_SECRETwould also require re-entering provider API keys.
Scheduler behavior:
- Fixtures sync once per day and imports or updates the next 10 scheduled World Cup matches from the active provider.
- Result sync runs every 10 minutes, but calls the active provider only when an imported match has no final score and kickoff was at least 2 hours ago.
- Live sync polls once per minute when the
livecapability is enabled, using only local matches that are currently live by app state. Live snapshots and events are provisional display data and never affect final scores, points, rankings, or achievements. - API-Football is the primary fixture/result provider; football-data.org remains available as a fallback implementation.
- Providers are queried with broad competition/league fixture-list endpoints, not one request per match.
- Manual admin final scores are never overwritten by sync.
- If sync is enabled without an API key, the app starts normally and logs a warning.
- Sync startup and job messages are written to Docker stdout/stderr with
[family-bet][sync]prefixes so they appear in Container Station logs. - Imported teams get country codes for emoji flags when the active provider supplies them or when a reliable country-name fallback exists.
- API-Football enrichment, when enabled, imports completed-match goal events that power score-progression timelines and achievements.
- API-Football fixture ids are matched to local matches by teams and kickoff time, then completed-match events are imported only when reconstructed event scores match the local final score.
- Cross-provider fixture matching uses API-Football team ids/codes when available and falls back to normalized national-team aliases such as
Turkey/Türkiye. - When both providers are configured, football-data enriches API-Football imported group-stage matches with group names, while API-Football remains the primary fixture/result source.
- Admins can trigger immediate historical API-Football timeline backfill with
POST /api/admin/sync/api-football/backfill. JSON fields: optionalcallCapandforce; default behavior skips matches that already have any goal events. - Dashboard and match detail pages show provisional API-Football live score/minute/events when available. Final scoring still waits for validated final results.
The football-data.org free tier is limited to 10 calls per minute. This app uses serialized provider calls and the normal schedule stays well below that limit. API-Football enrichment is capped locally and defaults to a 7000 calls/day safety limit.
Publish to the QNAP-hosted registry:
npm run docker:publish:qnap
The publish script first runs npm run db:backup:prod behavior, backing up the production database from /mnt/qnap-ssd/family-bet/data/family-bet.sqlite to ~/BackUps/family-bet-db/family-bet_yyyy_MM_dd_HH_mm_ss.sqlite. If the database cannot be read or copied, publishing stops before the image build.
After the backup succeeds, the script builds family-bet:latest, tags it as 192.168.1.10:32768/family-bet:latest, and pushes it. To run the same image flow manually:
docker build -t family-bet:latest .
docker tag family-bet:latest 192.168.1.10:32768/family-bet:latest
docker push 192.168.1.10:32768/family-bet:latest
Verify the pushed image can be pulled where needed:
docker pull 192.168.1.10:32768/family-bet:latest
The QNAP registry is HTTP-only, so Docker must allow 192.168.1.10:32768 as an insecure registry on machines that push or pull directly.
MVP Acceptance Smoke Checks
After deploying from a clean volume, verify:
- First admin setup creates an authenticated admin session.
- Invite activation works for a pre-created user.
- Login persists after redirect through the HTTPS reverse proxy.
- Prediction save works before the deadline and is rejected after the deadline.
- Result entry recalculates stored prediction points and rankings.
- Dashboard, match list, match detail, ranking, profile, and admin pages work on desktop and mobile.