Audio-first AI study companion for Philippine law students.
Read when you can. Listen when you can't.
Find Case → Study → Listen → Recall → Recite.
apps/
mobile/ Expo SDK 57 + expo-router + NativeWind — the primary product
api/ NestJS 12 — HTTP API and background worker (one image, two entrypoints)
packages/
shared/ zod schemas + inferred types shared by mobile and api
config/ shared tsconfig bases
supabase/ explicit, checked-in SQL migrations
scripts/ deliberate CLI tooling (case seeding)
- Node >= 22.12 (developed on 26.4)
- Supabase CLI
- Xcode (iOS) / Android Studio (Android) for development builds
- Docker Desktop, for running Supabase locally
npm install
npm run build --workspace @casebuddy/shared # api and mobile import its dist output
cp apps/api/.env.example apps/api/.env # nothing is required to boot
npm run dev:api # http://localhost:3000/health
npm run dev:worker
npm run dev:mobileVerify everything:
npm run typecheck && npm run lint && npm testThe mobile app needs a development build, not Expo Go. Config plugins don't
run in Expo Go, so UIBackgroundModes and the Android media foreground service
don't exist there — background playback and lock-screen controls silently do
nothing. Use npm run ios / npm run android, or an EAS dev build.
Audio uses expo-audio, not react-native-track-player. RNTP v4 doesn't
compile on SDK 57 and v5 is commercially licensed. Two consequences to design
around: AudioPlaylist can't own the lock screen until SDK 58 (so sections are
sequenced in JS with a single AudioPlayer), and the OS skip interval is
hardcoded to 10s.
On Android, setActiveForLockScreen is mandatory, not optional — it starts
the media foreground service. Without it, audio stops after ~3 minutes in the
background.
Jobs run on pg-boss over Postgres, not BullMQ over Redis. BullMQ's idle
polling alone exceeds Upstash's free tier of 500k commands/month. DATABASE_URL
must point at Supavisor session mode (port 5432) — transaction mode (6543)
supports neither prepared statements nor LISTEN/NOTIFY.
Generated audio goes to Cloudflare R2, not Supabase Storage. Supabase free allows 1GB storage / 5GB egress; R2 gives 10GB with free egress.
The API is ESM. NestJS 12 is ESM-only, so relative imports need explicit
.js extensions.
Validation is zod everywhere, via ZodValidationPipe — not class-validator.
Request contracts come from the same @casebuddy/shared schemas the app uses.
Philippine Supreme Court decisions are works of the government (IP Code §176),
but LawPhil licenses its collection under CC BY-NC 4.0. Case sourcing is
therefore always an adapter (CaseSourceAdapter), every case records its
source_url/source_name/retrieved_at, and seeding runs as a deliberate,
rate-limited CLI script — never a background crawler. See the plan for detail.
Milestone 1 complete: monorepo, API + worker skeleton, shared domain schemas, mobile app shell. Case data, AI pipeline, audio and recitation are next.