Skip to content

Repository files navigation

Pī, the Bayana mascot, a little magenta parrot

Bayana ばやな

A JLPT vocab + grammar trainer that doesn't get in your way.

License: MIT

Bayana is a mobile-first JLPT study PWA: one Next.js 16 service that schedules ~8,100 vocabulary words and 220 N3 grammar points with FSRS, the algorithm modern Anki runs on. Every word carries an example sentence written once by Claude Haiku through the Batch API and cached in Postgres, which cost about $2.55 for the whole deck. This README covers the study modes, the engineering highlights, the stack, local setup, and tests; ARCHITECTURE.md walks the design decisions behind them.

Live demo: bayana.chairulakmal.com. "Try the demo" needs no account and no email: one click mints a private ephemeral session and lands you in onboarding.

The motivation is the gap between the two obvious options. Anki is incredible but demands setup (decks, note types, add-ons, sync configs), and Duolingo is fun but ad-riddled with no real JLPT course. Bayana is the thing in between: open it and study, guided by Pī, with no deck wrangling, no ads, and no guilt-trips about streaks.

Study modes

Mode What it is
Flashcard Real spaced repetition (FSRS). Cards come back right as you're about to forget them.
Quiz Fast multiple-choice rounds with confusability-scored distractors. Good for a two-minute gap.
Exam A JLPT-style benchmark: kanji reading (問題1) then kanji writing (問題2), 10 questions each. Tells you where you actually stand.
Grammar A separate FSRS queue for JLPT grammar points, 220 patterns across 22 lessons (N3 v1), plus a browsable lesson-by-lesson reference.

There is also whole-deck browse and search with live filtering.

Highlights

  • The FSRS integration is one pure module. src/lib/fsrs.ts is the only file that speaks the ts-fsrs dialect, it touches no database, and a shared CardLike type lets the vocabulary and grammar queues schedule through the same adapter. Purity is what makes src/lib/fsrs.test.ts possible. It round-trips rows through persist-then-restore, exactly the cycle every card goes through between two study sessions, then checks the intervals themselves: Again ≤ Hard ≤ Good ≤ Easy, and asking for higher retention shortens them.
  • Review writes are serializable, with retry. Every rating reads a card's state, runs the FSRS math in JavaScript, and writes it back, so a double-tapped rating button would otherwise be a textbook lost update. serializableTxn (src/lib/db.ts) runs each one at SERIALIZABLE isolation and re-runs the loser of a conflict; undo replays the ts-fsrs rollback and deletes the log row inside the same guarantee (src/lib/review.ts).
  • AI example sentences are a one-time cost, not a running one. src/lib/generate.ts builds requests around a shared, cache-marked system prompt with strict JSON validation, so junk is never stored; scripts/seed-sentences.ts submits an Anthropic Batch job and scripts/collect-batch.ts polls and writes the results. The full five-level seed (~8,100 words) cost about $2.55, measured on the Anthropic console.
  • Quiz distractors are scored for confusability, not picked at random. src/lib/quiz.ts blends shared kanji (Jaccard overlap) with reading similarity (edit distance) to surface options you might actually confuse, while a meaning-overlap guard rejects any candidate close enough to be a second right answer. src/lib/exam.ts applies the same signals per question type.
  • A demo session is a signed cookie and nothing else. No account, no server-side session row: the HMAC covers both the userId and a server-enforced expiry (src/lib/current-user.ts). The mint endpoint is POST-only with an Origin check and per-IP plus global rate limits, and abandoned demo accounts are deleted within 14 days by a scheduled sweep (src/lib/demo-cleanup.ts), with the login path keeping the same sweep as a backstop.
  • Every claim on the privacy page is a property of the code. /privacy and /terms are plain-language static routes, and the two strongest things they say are checkable. There are no analytics, no tracking pixels and no third-party scripts of any kind: the CSP in next.config.ts permits no external origin, so the browser cannot load one by accident. And nothing a user does is ever sent to an AI model, because generation is a one-time seeding pipeline over deck words.
  • The route guard is deliberately cheap. proxy.ts, Next.js 16's rename of middleware.ts, checks cookie presence and rate-limits on the one X-Forwarded-For hop a client cannot spoof; real session verification happens server-side on every page and API route. Sign-in itself is a passwordless magic link (Auth.js + Resend) restricted to an email allowlist (src/auth.ts).
  • It installs like an app. A mobile-first PWA designed against the iPhone SE (375 × 667) baseline, with the manifest generated in code (src/app/manifest.ts), fullscreen on Android and standalone on iOS.

Stack

Layer What the code pins
App Next.js 16.2.7 (App Router), React 19.2.4, TypeScript 5.9
Data Prisma 7.8 via the pg driver adapter, PostgreSQL 16 (Docker locally, Railway managed in production)
Scheduling ts-fsrs 5.4
AI @anthropic-ai/sdk 0.100, model claude-haiku-4-5 (Messages + Batch API)
Auth Auth.js (next-auth 5 beta) with Resend magic links, plus signed demo cookies
Styling Tailwind CSS 4
Tests Vitest 4
Deploy Railway, Railpack builder (railway.json)

Running locally

Prerequisites: Node 24+, Docker.

# 1. Environment and database (Postgres on localhost:5887)
cp .env.example .env
docker compose up -d

# 2. Install dependencies, create tables, generate the Prisma client
npm install
npx prisma migrate dev

# 3. Seed the default local user, then the vocabulary decks
npx tsx scripts/seed-user.ts
npx tsx scripts/import-csv.ts

# 4. App on http://localhost:3887
npm run dev

Ports are themed 887 (ば・や・な): Postgres 5887, app 3887. To skip the magic-link round-trip locally, set DEV_AUTH=1 in .env and visit /api/dev/login; it mints a real session for the seeded user and 404s in production.

Two seeding steps are deliberately left out of the sequence above. Example sentences are generated separately (step 3 leaves them empty) because generation calls the Anthropic API and costs real money: run scripts/seed-sentences.ts and scripts/collect-batch.ts with an ANTHROPIC_API_KEY if you want them. And npx tsx scripts/seed-grammar.ts needs a decks/grammar-*.md file that is gitignored on purpose: the grammar content comes from a source not licensed for redistribution, so the repo ships the schema and the seed script but you supply your own deck in the documented markdown shape (SPEC.md §4.1).

Testing

158 tests across 8 files (npm test, vitest.config.ts), covering the domain layer in src/lib: the FSRS adapter and the intervals it produces, the confusability scoring behind both question modes, the review-and-undo cycle for the vocabulary and grammar queues, queue composition and ordering, the read aggregates behind /browse, /stats and the home hub, and the demo-cookie authentication path.

These are characterization tests: they pin what the code does today rather than asserting correctness in the abstract, because src/lib is the layer that will move to a new framework close to verbatim and code that moves without being re-read is code whose behaviour can drift silently. They assert at the function boundary ("four options, exactly one correct, distractors drawn from the same level"), never on database row shapes, so they survive a schema redesign and become its specification. Randomness is tested as invariants over many runs rather than with a seeded generator, since seeding would pin an implementation detail and let the tests pass for the wrong reason.

Database-touching functions take a dependency object as a defaulted last parameter, so tests inject an in-memory fake (src/lib/fixtures/fake-db.ts) and no call site changes. The fake implements only the query features the app actually uses and throws on anything else, which is the property that makes it worth trusting: a fake that quietly ignored an argument would turn a passing suite into evidence of nothing. What it deliberately does not cover is stated in SPEC.md §12.1: it runs transactions inline, so the lost-update race that SERIALIZABLE isolation exists to prevent is out of reach, and it tests composition rather than SQL.

Not covered: React components, route handlers, and Server Actions. There is no CI pipeline yet.

Design docs

ARCHITECTURE.md is the technical tour: six decisions, each stated as the choice, the reasoning, and the trade-off accepted, with file paths throughout. SPEC.md is the full design document and the project's source of truth, including the alternatives analysis; DECISIONS.md is the dated log of every decision that got it there. BRAND.md owns the look: the palette, the typography, and Pī himself.

Credits

Vocabulary from open-anki-jlpt-decks (MIT).

About

🦜 ばやな · Minimalist JLPT vocabulary trainer. Open data, FSRS spaced repetition, no account. Open and go.

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Contributors

Languages