A real-time game-scoring app with multi-user simultaneous editing
For amateur baseball | an Expo + Next.js + Supabase monorepo | solo-built a zero-trust design pushing authorization into the DB (RLS on all 69 tables, 280 policies) and offline-resilient, idempotent concurrent input
Client
A real-time game-scoring platform for amateur baseball (iOS / Android mobile app + a Web admin console for operators) | Users: players, team managers, scorers, scouts, operators — multiple stakeholders | Setup: solo (full-stack), from domain design through mobile, admin console, DB/authorization, CI/CD, observability, and release operations
My role
Technical architect and full-stack developer. Solo from design through production operations — domain modeling, DB / RLS design, the mobile app (React Native / Expo), the operator Web admin console (Next.js), auth/authorization (MFA / RLS / audit logs), CI/CD, observability, and release operations (EAS / OTA / Supabase).
Challenge (Situation & Task)
"Collaborative scoring" — where multiple scorers record the same game simultaneously without breaking from duplication, conflict, or out-of-order writes — had to work even at ballparks with poor reception (frequent offline). At the same time, multi-layered authorization was required to strictly separate what each role — player, team manager, scout, operator — can see and do. Starting from the premise that the mobile app can be tampered with, placing the trust boundary on the server/DB side was the fundamental requirement.
This app concentrated the difficulties of collaborative editing, mobile, and multi-layered authorization.
-
Concurrent editing × offline: multiple people make high-frequency, pitch-by-pitch entries, and reception is unstable at ballparks, so offline is frequent. Data integrity had to hold across send retries, double sends, and device restarts.
-
Zero-trust multi-layered authorization: permissions differ across player, team manager, scorer, scout, and operator, and authorization was multi-layered — "team-level viewing," "time-limited scorer permission," "per-field disclosure to scouts (mindful of personal-information-protection law)." Because client-side gating can be bypassed, it had to be enforced on the DB side.
-
Domain correctness: baseball scoring has complex state transitions (inning, top/bottom, outs, runners, batting order, pitch sequence), and invalid states had to be structurally excluded with types and DB constraints.
-
Sustainability of solo operation: developing and operating alone made advanced automation that mechanically prevents regressions (types, tests, schema consistency, a11y, deploys) indispensable.
Why these technologies (Rationale)
A pnpm + Turborepo monorepo: because mobile, admin, and DB needed to share the same domain vocabulary, Zod schemas are consolidated in
packages/domainas the single source of truth for the whole stack. pnpm catalog centrally pins major dependencies (Zod / React, etc.) to eliminate silent drift.Supabase (PostgreSQL + RLS): to enforce authorization logic in the DB layer with PostgreSQL Row-Level Security rather than scattering it across the app. A zero-trust design that doesn't trust the client, realized at low cost without an extra backend server.
Idempotency-key-driven offline-first sync: rather than broadcasting high-frequency input over WebSocket, write safely with deterministic idempotency keys and reflect to each client via TanStack Query cache invalidation + short-interval refetch. Prioritizing "never breaking even with poor reception" (device-local is Zustand + a persistent queue).
The operator admin console on Next.js 16 (App Router / RSC): the admin console uses server components +
@supabase/ssrfor RLS-effective server fetching, avoiding excessive client state.TypeScript strict + CI-enforced type coverage: enabling even
noUncheckedIndexedAccessandexactOptionalPropertyTypes, with per-package type-coverage thresholds (96.7%–100%) enforced in CI.
What I did (Action)
[Offline-resilient, idempotent concurrent input] Clients generate deterministic idempotency keys — at-bat
game:{id}:inn:{n}:{top/bottom}:seq:{n}, pitchab:{id}:pitch:{n}. The DB's(recording_team_id, idempotency_key)unique constraint +upsert(ignoreDuplicates)+ a permission-scoped RPCresolve_own_at_bat_idon conflict make retries, double sends, and concurrent users always converge to the same logical row.[A fault-tolerant write queue] A pitch tap is first optimistically reflected to Zustand and added to an AsyncStorage persistent queue. A single "drain worker" alone writes to the
pitchestable, with single-flight control and exponential backoff, so it re-sends safely across offline, app restarts, and out-of-sync operation positions.[Consolidating zero-trust authorization in the DB] RLS is enabled on all 69 public tables, with 280 policies expressing per-role access. Disclosure to scouts is controlled by per-field grants on the intersection of "player approval ∩ team-manager approval" + an append-only audit log (mindful of personal-information-protection law). Sensitive operations are gated by email-OTP MFA;
custom_access_token_hookinjectsmfa_verifiedinto the JWT, andrequire_mfa()enforces it at the RPC entry.[Structural correctness of the baseball domain] At-bat results, pitch types, on-base reasons, etc. are expressed with Zod enums, modeling fielder's choice correctly by making the on-base reason orthogonal to the at-bat result. Outs 0–3 and innings 1–99 are doubly guaranteed by TypeScript and DB CHECK constraints, and rules like put-out conditions are consolidated into pure functions shared by TS / SQL (DRY).
[Automation that supports solo operation] 11 GitHub Actions automate types, lint (Biome), type coverage, dead-code detection (knip), i18n consistency, RN a11y-policy checks, migration-safety checks (squawk), RLS coverage, pgTAP, schema-drift detection, and EAS / OTA delivery.
[Observability and privacy] Sentry (mobile / admin) runs through a 7-layer PII scrubber, masking tokens, email, phone, IP, and user IDs before sending. The structured Transport in
packages/observabilityautomatically scrubs log context too.
This product's design principle was consistently "don't trust the client (place the trust boundary in the DB)."
Concurrent-editing integrity — optimistic but unbreakable:
Broadcasting high-frequency input over WebSocket brings delivery cost, fragility under failure, and ordering problems. The solution was deterministic idempotency keys. A unique key is computed from an at-bat/pitch "slot," and the DB absorbs duplicates with the (recording_team_id, idempotency_key) unique constraint. Writes use upsert(ignoreDuplicates), resolving one's own team's row via a permission-scoped RPC only on conflict. The device is offline-first: a pitch is first reflected optimistically locally + queued persistently, and a single drain worker sends safely with exponential backoff. Even if the app restarts or the scorer advances the operation position, re-sends never collide thanks to the keys and converge. Reflection between clients uses mutation-driven cache invalidation and short-interval refetch (near-real-time), with a game_states.version column enabling optimistic locking too.
Authorization — enforced in PostgreSQL, not the app:
Authorization doesn't rely on app-side gating but is enforced via Row-Level Security in the DB. RLS is enabled on all 69 public tables, with 280 policies expressing role, team, time-limited permissions, and per-field disclosure. RPCs are written with the canonical SECURITY DEFINER + fixed search_path, verifying auth.uid(), role, and require_mfa() at the function entry. Disclosure of player info to scouts is three-layered — "request → grant → audit" — disclosing only the intersection of player and team-manager approval, with append-only auditing (mindful of personal-information-protection law). RLS changes must be accompanied by pgTAP tests, switching role via set local request.jwt.claims to verify both allow and deny and prevent regressions.
Automation to run in production alone: 11 GitHub Actions handle types, lint, type coverage, pgTAP, RLS coverage, migration safety (squawk), a11y policy, schema-drift detection, and OTA delivery. Schema-drift monitoring in particular is a safety net added after an actual incident of "merged but not deployed," checking the sync of migrations and Edge Functions daily.
Key technical decisions
Supabase RLS: authorization enforced at the DB row level on all 69 tables (zero-trust)
Deterministic idempotency keys + unique constraint + resolution RPC: converging to the same logical row even offline / concurrent
Zod domain types as the single source of truth: shared across mobile, admin, DB types, and Edge Functions
pgTAP + type coverage + schema-drift detection: mechanically blocking regressions in CI
Responsibilities
- Domain design / DB / RLS design
- Mobile app development (React Native / Expo)
- Operator Web admin console (Next.js 16 / RSC)
- Auth/authorization design (MFA / RLS / audit logs)
- CI/CD, observability, release operations (EAS / OTA, Sentry)
Technologies
Results in numbers
- RLS policies
- 280+RLS enabled on all 69 public tables (100% coverage, zero-trust).
- Total tests
- 1,200files+App-layer (Jest / Vitest) + DB-layer (243 pgTAP) test files.
- Type coverage
- 96.7%+CI-enforced across all packages (up to 100%, TypeScript strict).
- CHECK constraints
- 349+Structurally excludes invalid states such as outs 0–3.
- CI/CD workflows
- 11workflowsAutomates types, lint, schema-drift detection, a11y, and OTA delivery.
- DB migrations
- 238migrationsEvolved while statically checking destructive changes with squawk.
Results
- Realized collaborative scoring where multiple scorers record the same game simultaneously, without breaking even at poor-reception ballparks (frequent offline).
- Consolidated authorization into PostgreSQL RLS, enabling RLS on all 69 public tables (100% coverage). Built a zero-trust foundation that strictly separates per-role access with 280 policies.
- Controlled disclosure of player info to scouts via per-field grants on "player approval ∩ team-manager approval" + an append-only audit log — a design mindful of personal-information-protection law.
- Constantly verifies RLS, integrity, and state transitions with 243 pgTAP DB tests. With the app layer, ensured quality across ~1,200 test files, with type coverage CI-enforced across all packages (96.7%–100%).
- Automated with 11 CI workflows — schema-drift detection ("merged but not deployed"), migration-safety checks (squawk), RN a11y-policy checks — for safe continuous delivery even solo.
- Established observability that detects production anomalies while masking tokens, email, phone, IP, and user IDs via Sentry + a 7-layer PII scrubber.
- Designed, developed, and operated the mobile app (iOS / Android) and operator Web admin console solo, under Zod domain types as the single source of truth.
同様の課題、抱えていませんか?
あなたのビジネス課題も、最新の技術で解決できます。 まずは30分の無料技術相談から、状況をお聞かせください。
自社の課題もSaaS化できるか相談するプロジェクト単位(請負)・技術顧問、どちらにも対応可能です