Architecture overview
Tabletop Tracker — Hosting & Architecture Plan
Status: Accepted (pre-implementation) Date: 30 July 2026 Last verified: July 2026 — pricing and free-tier terms move constantly, re-check before committing spend.
This is the narrative overview. Individual decisions live in the decision catalogue; the rules agents are held to live in standards; the backlog is in work.
1. Context
A Letterboxd/Goodreads-style site and mobile app for tabletop games. Solo developer. Free to run at friends-and-family scale, with a credible path to monetisation if it proves out.
Priority features: curated lists & discovery, collection management, social feed & followers. Session logging and stats are present but deliberately understated in the UI.
Fixed constraints:
- GitHub for source control — non-negotiable
- £0 running cost at launch
- Web + mobile from day one
2. The stack
| Layer | Choice | Free tier |
|---|---|---|
| Source & CI | GitHub + GitHub Actions | Yes |
| Monorepo | pnpm workspaces + Turborepo | n/a |
| API | Hono on Cloudflare Workers | 100K req/day |
| Web | Astro SSR + islands on Workers | 500 builds/month |
| Mobile | Expo + Expo Router, EAS Build | Yes (build quotas apply) |
| Database | Neon Postgres via Cloudflare Hyperdrive | Yes |
| Query layer | Drizzle + drizzle-kit, drizzle-zod → shared schemas | n/a |
| Object storage | Cloudflare R2 | 10 GB, zero egress |
| Auth | Clerk | 50,000 MRU |
| Search | Postgres FTS + pg_trgm | Included |
| Catalogue | Wikidata (CC0) + Wikipedia (CC BY-SA), AI-normalised offline | Free sources; enrichment is paid |
| Docs subsite | Astro Starlight on Cloudflare Pages | Yes |
| Errors | Sentry | Yes |
Shape: one Hono API on Workers serving two clients. Shared TypeScript types and Zod schemas live in a workspace package consumed by both web and mobile.
3. Decisions and rationale
Every decision now lives in the decision catalogue as a numbered ADR. This section is an index; the ADRs are the source of truth.
ADR-0001 to ADR-0007 originated here as ADR-1..7 and were extracted unchanged in substance.
| ADR | Decision | In one line |
|---|---|---|
| 0001 | Cloudflare over Vercel | Vercel Hobby forbids commercial use; the cliff is $5/mo, not $20/seat |
| 0002 | No Next.js | OpenNext friction, and Expo needs a standalone API anyway |
| 0003 | Neon Postgres over D1 | Discovery needs FTS, pg_trgm, JSONB, and array columns |
| 0004 | Hyperdrive, not the Neon driver | Don’t stack two poolers — the trap most likely to bite |
| 0005 | Clerk is the doorman, not the filing cabinet | Own the users table; never FK to a Clerk ID |
| 0006 | Event-first data model | Ratings live on the session; players are not users |
| 0007 | Progressive disclosure for logging | Schema depth ≠ form depth. One tap to log |
| 0008 | Drizzle as query layer | One schema source of truth, feeding drizzle-zod |
| 0009 | Astro SSR + islands | Organic search is the only free acquisition channel |
| 0010 | CatalogueSource adapter | Unblocks seven of nine build-order steps from BGG |
| 0011 | Docker local, Neon branch in CI | Fast offline inner loop; real parity at the gate |
| 0012 | Issues for state, repo for specs | Split by what each medium is good at |
| 0013 | Astro Starlight on Pages | Reuses the stack; docs single-sourced from docs/ |
| 0014 | WSL2, repo off /mnt/c | Taking §4’s own advice before it costs agent iterations |
| 0015 | PR-gated agent autonomy | Full autonomy on a branch, human at the merge |
| 0016 | Open-data seeding with provenance | Wikidata is CC0 — the answer to the BGG risk |
| 0017 | AI normalisation, confidence-gated | Fills what open data lacks; never silently authoritative |
4. Gotchas
Cloudflare
- 10ms CPU per invocation on free is the real ceiling, not the 100K daily request count. Average Workers use ~2.2ms, but auth validation + a database query + serialisation in one request gets close. Watch the feed queries.
- No spend protection by default. Getting hammered blows the daily cap and forces a mid-month upgrade. Put rate limiting in the first commit.
- CDN terms prohibit video and large-file distribution, with a 512 MB per-file cache limit. Serve box art from R2, don’t treat the CDN as an asset store.
nodejs_compatflag and a recentcompatibility_dateare required or database drivers simply won’t work.- Environment variables are runtime-only on Workers, and
process.envstays empty unlesscompatibility_dateis 2025-04-01 or later.
Clerk
- A great deal of published material (and many models’ training data) still quotes the stale 10,000-user free tier. The current figure is 50,000 MRU. Check source dates.
- User data is stored in the US with no EU data residency option. Irrelevant at friends scale; revisit if UK/EU usage grows.
- MFA and passkeys are Pro features. Social login and the prebuilt UI are free.
Expo & app stores
- OAuth, native components and passkeys all require a development build — they do not run in Expo Go. Basic email/password does. Adding Google sign-in changes the local dev loop.
- Social buttons appear even without native credential setup, then fail on tap. Silent until pressed.
- Google needs three OAuth client IDs (iOS, Android, Web) plus Team ID, Bundle ID, and Android SHA-256 fingerprints.
- App Store Guideline 4.8: any app offering third-party social sign-in must also offer Sign in with Apple on iOS.
useSignInWithGoogleneedsexpo-crypto;useSignInWithAppleneedsexpo-apple-authentication+expo-cryptoand is iOS-only.
Catalogue data — was the one that could sink the project, now de-risked
The BoardGameGeek XML API is the obvious catalogue source, but its terms are permissive for non-commercial use and require permission for commercial use. Since this project intends to earn eventually, email BGG and get an answer in writing before building sync code (WI-H01).
This is no longer a project-level risk. Two decisions defused it:
- ADR-0010 puts the catalogue behind a
CatalogueSourceinterface, so the licensing answer blocks one swappable implementation rather than seven build-order steps. - ADR-0016 sources from Wikidata, which is CC0 — public domain, no permission to request, no terms that can be withdrawn. Wikipedia supplements it under CC BY-SA, which does carry an attribution obligation.
The original assessment that alternatives are “all worse” held only for hand-building a catalogue or depending on a third-party API. Harvesting open structured data was the option not considered, and it is strictly better than both.
Environment
- OpenNext/Wrangler tooling is more reliable on Linux. On Windows, plan for WSL rather than debugging Windows-specific runtime issues.
5. Cost model
| Item | Cost |
|---|---|
| Cloudflare Workers, R2, Pages | £0 |
| Neon Postgres | £0 |
| Clerk (to 50K MRU) | £0 |
| GitHub | £0 |
| Catalogue sources (Wikidata, Wikipedia) | £0 |
| Apple Developer Program | $99/year — unavoidable |
| Google Play Developer | $25 one-off |
| LLM catalogue enrichment | One-off, human-approved per run (WI-H05) |
Apple’s fee is the only compulsory recurring spend, and Guideline 4.8 makes it compulsory the moment Google sign-in ships on iOS.
The enrichment cost is new and worth being explicit about. ADR-0017 uses Claude to derive mechanics, weight, and player count from prose — the fields discovery runs on and that no free structured source provides. It is:
- Not a running cost. It scales with catalogue size, not with traffic, and is paid once per game rather than per request.
- Reduced substantially by the Message Batches API (50% off) and prompt caching of the shared vocabulary prefix.
- Never triggered by an agent. A human approves the volume and tier under WI-H05.
The £0 constraint in §1 covers the deployed system, and it still holds. It never covered a one-off build-time data pipeline.
Upgrade path when it grows: Workers Paid $5/mo → Clerk Pro $25/mo (above 50K MRU) → Neon paid tier. Nothing here has a cliff edge.
6. Build order
- Schema and migrations — Game, User, Player, Shelf, Session, List. Get the Player/User split right now.
- Catalogue ingest —
CatalogueSourceadapter, then the open-data seeder (tools/catalogue-forge: Wikidata → Wikipedia → AI normalisation). No longer pending the BGG answer — see ADR-0016. The BGG sync worker remains a later, optional source. - Auth — email OTP only. Enough to pass App Store review and get friends using it.
- Collection management — the feature that’s useful with a single user.
- Lists — hand-seed twenty good ones yourself. They bootstrap discovery before any behavioural data exists.
- Session logging — tier-one only (“Played”), expand later.
- Social feed — fan-out-on-read. A join across follows and recent activity serves Postgres happily into the tens of thousands of users.
- Discovery — content-based first (designer, mechanic, weight, player count). Works on day one with zero users.
- Social sign-in — once people actually use the thing.
7. Open questions
Resolved
| Question | Resolution |
|---|---|
| Astro vs Vite SPA for web | Astro SSR + islands — ADR-0009. SEO is the only free acquisition channel. |
| Drizzle vs Kysely for the query layer | Drizzle — ADR-0008. One schema source of truth feeding drizzle-zod. |
| BGG commercial licensing — “blocking for anything past step 2” | No longer blocking. ADR-0010 puts the catalogue behind an adapter, and ADR-0016 sources from Wikidata (CC0). The email (WI-H01) now blocks only the BGG sync worker. If BGG says no, the project is unaffected. |
Still open
- Meta/Facebook sign-in — whether it earns its place at all. Google + Apple likely covers everyone, and each extra provider adds a credential surface. Defer until WI-050.
- Enrichment volume and model tier — how many games to enrich, at what model tier and effort. Needs real cost numbers from a pilot run rather than a guess. Tracked as WI-H05; see ADR-0017.