Skip to content

Tech Stack

Four components, each with the smallest stack that does the job — no framework is shared just for the sake of sharing it, but the JS/TS side sits in one pnpm workspace so tooling (lint, format, typecheck, CI) is consistent across it. See Architecture for how the pieces fit together, and Engineering Decisions for the reasoning behind the biggest choices.

Bell web app (apps/bell-web)

ConcernChoiceRationale
Language / UIReact 19 + TypeScriptRequired by the product brief; a small, well-known stack for a one-screen app.
Bell renderingPixiJS 8Canvas-based animation (idle sway, tap ring/shake) that plain CSS/DOM can't do smoothly.
Build toolVite 8Fast dev server, first-class React/TS support, static output that GitHub Pages can serve directly.
Table data@game-master-bell/shared (tables.json + Zod schema), imported at build timeSame source of truth the API validates against — see Repository Map.
RoutingStatic per-table pages generated at build time from tables.json (scripts/generate-table-pages.mjs)GitHub Pages only serves static files; per-table t/{code}/index.html avoids SPA-router 404 hacks.
GeolocationBrowser Geolocation API directly, no libraryThe advisory geofence needs one getCurrentPosition call and a haversine formula — not worth a dependency.
HostingGitHub Pages, unified artifact with this docs siteFree; see Architecture → Deployment.

Call API (apps/api)

ConcernChoiceRationale
RuntimeNode.js 22 + TypeScriptSame language and workspace as the rest of the monorepo.
HTTP frameworkFastify 5Lightweight, fast, schema-friendly; the API is two routes (GET /healthz, POST /call).
ValidationZod 4Parses/validates the POST /call body and the shared tables.json schema with the same library.
Pushfirebase-admin 14 (Messaging only)Official server SDK for FCM; one dependency, one send() call to the game-masters topic. Hand-rolling FCM's HTTP v1 auth was considered and rejected — see Engineering Decisions.
CORS@fastify/corsRestricts POST /call to the bell app's origin.
LoggingpinoStructured logs to stdout; enough observability at this scale with no database to inspect.
TestingVitest 4Matches the Vite-based toolchain used elsewhere in the workspace.
DeployPlain Node process on the cafe's VPS over SSH (no Docker, no PaaS)One small always-on service; see Getting Started for the deploy workflow.

Receiver Android app (apps/receiver-android)

ConcernChoiceRationale
Language / UIKotlin + Jetpack ComposeModern Android default; the app is a status screen plus a notification handler, not a large UI surface.
PushFirebase Cloud Messaging (topic subscription, com.google.firebase:firebase-messaging-ktx)Reliable delivery in foreground, background, and killed states, with no server-side device registry — see Architecture.
Local storageRoom (androidx.room)Backs the recent-calls list on the status screen; a real (if tiny) local schema beats hand-rolled SQLite or a flat file.
BuildAndroid Gradle Plugin 8.7 + Kotlin 2.0, versions pinned centrally in gradle/libs.versions.tomlStandard modern Android setup; the version catalog keeps every dependency version in one file.
Google servicesgoogle-services Gradle pluginWires google-services.json (safe to commit — identifiers, not secrets) into the FCM client.
Min SDKAPI 26 (Android 8.0)Notification channels — which the custom bell sound depends on — require API 26+.
DistributionSigned release APK, built in CI, published as a GitHub Release on tag; sideloaded onto staff phonesA handful of devices doesn't justify Play Store review overhead.

Docs site (apps/docs, this site)

ConcernChoiceRationale
FrameworkVitePress 1Markdown-first — the source content already lives in the PRDs as markdown; built-in sidebar, dark mode, and local search need almost no config.
Diagramsvitepress-plugin-mermaid + mermaidRenders the PRDs' Mermaid diagrams (like the call-path sequence diagram) natively, no image export step.
SearchVitePress's built-in local search providerClient-side only, no external search service to run or pay for (FR-D11).
HostingGitHub Pages, same artifact as bell-web under /docs/A GitHub repo has exactly one Pages deployment; see Architecture → Deployment for how the two builds are assembled together.

Shared package (packages/shared)

ConcernChoiceRationale
Contenttables.json (table code → floor/number/active flag) + generated TypeScript typesOne file, imported directly by both bell-web and api at build time — no sync service, no runtime coupling between them.
ValidationZod schema, checked by scripts/validate-tables.mjs at build timeA malformed tables.json fails the build instead of shipping a broken table list.

Workspace-wide tooling

ConcernChoiceRationale
Package managerpnpm 10, single workspace (pnpm-workspace.yaml)One pnpm install, one lockfile, pnpm --filter per app — apps stay independently buildable while sharing tooling.
LanguageTypeScript 5.7 everywhere except the Android appType safety shared across the bell app, API, and shared package; one tsc --build project graph.
Linting / formattingESLint 9 (flat config, typescript-eslint) + Prettier 3Consistent style enforced in CI across every TS package.
ScriptingtsxRuns TypeScript scripts directly (QR generation, FCM test sends) without a separate build step.
CIGitHub Actions, path-filtered per appA docs-only edit doesn't rebuild the API; an API-only edit doesn't touch Pages — see Architecture → Deployment.

Docs for Game Master Bell, built with VitePress.