Your block's recitation index cards, digitized. Beadles log who gets called; every classmate sees which cards are still in each subject's deck and gets a cautious, explained study-priority hint.
Students don't need accounts. Each block has a read-only class link (/c/<token>) the beadle posts in the group chat; classmates tap their own card once (remembered on their phone). Only beadles and class reps sign in, to log calls and manage subjects.
Mobile-first PWA, entirely on Cloudflare: Workers (Hono API + static assets) · D1 · Durable Objects (live updates) · Better Auth.
Production: https://recitradar.buildwithanthony.com (Worker custom domain in wrangler.jsonc; the workers.dev URL is disabled so class links and sessions live on one host).
npm install
cp .dev.vars.example .dev.vars # then set BETTER_AUTH_SECRET (openssl rand -hex 32)
npm run db:migrate:local
npm run dev # http://localhost:5173 — Vite + the Worker in workerd| Command | What it does |
|---|---|
npm run dev |
Frontend + Worker + local D1 + Durable Objects, hot reload |
npm test |
Engine unit tests + API integration tests inside workerd (real D1 migrations) |
npm run typecheck |
App, Worker, config, and test projects |
npm run db:generate |
New SQL migration from worker/db/schema.ts (Drizzle Kit) |
npm run deploy |
Build, apply remote migrations, deploy |
One-time setup:
npx wrangler d1 create recitradar # paste the database_id into wrangler.jsonc
openssl rand -hex 32 | npx wrangler secret put BETTER_AUTH_SECRET
npm run deployOptional Google sign-in: create an OAuth client with redirect URI https://<your-host>/api/auth/callback/google, then wrangler secret put GOOGLE_CLIENT_ID and GOOGLE_CLIENT_SECRET. The button appears automatically.
CI (.github/workflows/ci.yml) typechecks, tests, and builds on every push/PR. Deploys run on manual dispatch with CLOUDFLARE_API_TOKEN and CLOUDFLARE_ACCOUNT_ID repo secrets.
School shared tenant root (seeded list of PH law schools; reps can add one)
└─ Block a section, e.g. 1-D — keeps its roster, members, and class link across years
└─ Term AY + semester + year level; exactly one active, the rest read-only history
└─ Subject → deck → rounds → calls
- Other sections: any rep creates a block under an existing school. Blocks are isolated from each other — members of one get
404for another, and a class link only reaches its own block. - Next semester / next year: Block → Subjects → Next term archives the current term (its subjects become read-only, still browsable) and starts a new one. Roster, members, and class link carry over; rename the block if its label changes (1-D → 2-D) and mark students who left as inactive so they get no new cards.
| Who | How | Can |
|---|---|---|
| Classmates | Class link, no account | View this term's decks; pick "this is me" for a personal card + outlook. Never sees notes, recorders, student numbers, or emails. "Counts only" subjects show counts, not cards. |
| Beadles / reps | Account + join code, then made officer (whole block) or subject beadle | Log/void calls, reset rounds, manage roster, subjects, rules, terms, links |
| Block admin | Created the block | All of the above + roles, archive subjects |
React PWA ──HTTP──▶ Worker (Hono) ──▶ D1 ← single source of truth
▲ │
└──WebSocket── SubjectRoom DO ◀─ notify(version) after each committed write
- Cards belong to roster entries, not accounts. A beadle pastes the class list once; every student gets a card in every subject immediately, whether or not they've signed up. Students later claim their card. (
students.user_idis nullable.) - Every write is one D1 batch (one transaction). Logging a call resolves the active round, today's meeting (auto-created, Manila date), and an active enrollment inside a single
INSERT … SELECT, so a racing reset or drop can't attach a call to the wrong round. A uniqueidempotency_keymakes client retries safe. - Round resets are conditional on the round the beadle was looking at (
expectedRoundId), with a partial unique index guaranteeing one active round per subject. Two beadles resetting at once → one wins, the other gets409 ROUND_CHANGED. - Corrections void, never delete. Voided events stay visible (struck through) and in
audit_logs. - Durable Objects only carry "something changed" hints. Clients refetch from D1 on every hint, on reconnect, and on tab focus, so a dropped socket can't leave a screen stale. Sockets hibernate, so idle classrooms cost nothing.
- The outlook engine is pure (
shared/engine/outlook.ts) and unit-tested. It never outputs percentages; "Lower priority" requires an observed no-repeat (or sequential) rule.
shared/engine/ card states, outlook rules, Manila time, roster parsing (pure TS)
shared/ API view models + zod validators shared by Worker and UI
worker/ Hono API, Better Auth, access control, D1 queries, SubjectRoom DO
src/components/ index card system (IndexCard, MiniCard, stamps, tallies, deck stack)
src/pages/ screens
migrations/ SQL applied by wrangler (generated by Drizzle Kit)
tests/ vitest: engine.test.ts, api.test.ts (runs in workerd)
- Notes on calls: visible to beadles and the card's owner only.
- Per subject, beadles choose Whole class (everyone sees who has been called, like the physical deck) or Counts only (students see counts and their own card).
- Student numbers and member emails: officers only. Non-members get
404for any block resource.