Case study
KATHA — engineering a reading & publishing platform end to end
A quiet, premium home for Filipino literature: a reading experience, a searchable catalogue, and an author studio — designed, built, and shipped as one product. This page explains how it is put together and why, using only what the repository itself can verify.
- Role
- Design & engineering, end to end
- Stack
- Next.js 16 App Router · TypeScript · Tailwind CSS 4
- Status
- v2.0.0 — live in production
- commits
- 64
- tagged releases
- 2
- routes
- 17
- components
- 40
- TypeScript modules
- 94
- runtime dependencies
- 3
01
Overview
KATHA is two products sharing one system: a reader (library, search, bookmarks, history, continue-reading) and an author studio (drafting, chapters, autosave, covers, publishing). Readers and authors are the same identity at different stages of a ladder — a modelling decision that shapes the whole architecture (§05).
Everything below is verifiable: the product is live, the repository is public, and the modules quoted here can be read in full.

02
Architecture
The dependency list is the first architectural decision: the production dependency tree is next, react, react-dom — nothing else. Search, persistence, the identity ladder, and the design system are pure TypeScript modules in the repo rather than imported packages. What it proves: features here are implemented, not assembled.
The system is layered so that coupling only flows in one direction — UI knows selectors, selectors know domain modules, domain modules know nothing about React:
Routes
App Router — 17 pages in two route groups: (reader) and studio
Components
40 shared components; interactivity isolated to "use client" islands
Selectors
content-aware helpers (lib/*-selectors.ts) — the only layer allowed to join stored state with book content
Domain modules
pure TypeScript — books, search, membership, bookmarks, history, continue-reading
Sources
static in-memory catalogue · localStorage (pre-auth, per device)
03
Information architecture
Two App Router route groups keep the reading product and the writing product cleanly separated — different navigation, different concerns, one codebase. The decision this proves: route groups as product boundaries, not folder decoration.
(reader) — 11 routes
- /
- /library
- /library/[slug]
- /library/[slug]/read/[chapter]
- /search
- /authors
- /authors/[slug]
- /bookmarks
- /history
- /continue-reading
- /join
studio — 6 routes
- /studio
- /studio/new
- /studio/works/[id]
- /studio/works/[id]/chapters/[chapterId]
- /studio/works/[id]/preview
- /studio/works/[id]/preview/[chapterSlug]
The studio preview routes render chapters through the same reader components — authors proof their work in exactly the typography readers will get.
04
The data layer
lib/books.ts is the single source of truth for book and chapter data — framework-agnostic by contract, shaped so a database can replace it without touching a single UI surface. The module documents this intent itself:
“Static sample data for now, shaped for a real source later … swapping AUTHORED_BOOKS for JSON.parse(…) or a query result is a one-line change … Every helper is a function over the record; call sites need no change when the bodies become await prisma.book.*.”
What this proves: the migration path to a real database was an explicit design constraint, not an afterthought. Derived fields (chapter numbers, reading time) are stamped by one transform — the same transform a loader would run on raw rows — so authored data can never drift from rendered data.
05
Identity & roles
One identity, three states: guest → reader → author. The model separates the person (account) from the pen (public writing identity) — becoming an author links a profile to the same user rather than creating a second account, which is how pen names work in real publishing.
Real authentication is deliberately deferred — but the seam for it is not. All role gating flows through one function, and the module names exactly where the future backend plugs in:
“getViewer()remains THE authentication seam: with Supabase it derives the same Viewer from users + authors rows, and everything downstream is already correct.”
What this proves: knowing which decision to defer is itself an engineering decision. The whole onboarding ladder — join, become an author, complete the author profile, enter the studio — is walkable today, and swapping in sessions changes one derivation, not the product.
06
The reading-state system
Bookmarks, reading history, and continue-reading are three features with one shared shape: a ReadingLocation — book → chapter → paragraph, plus the fields needed to render it. Each feature keeps its own independent storage; none of them is allowed to touch book content directly.
pure persistence
pure persistence
pure persistence
the only layer that joins stored locations with book content
one card renders all three features’ entries
“Kept separate on purpose: lib/bookmarks.tsstays a pure persistence layer (safe to swap for an API / cloud sync), while all book-content coupling lives in the selector layer. UI consumes selectors, never lib/books.ts directly.”

07
The search engine
Search is a pure function from (query, books) to ranked, grouped results — no React, no DOM, no storage, and no import of the catalogue itself, which makes the whole engine testable in isolation. Matching is deliberately forgiving; every query token must hit (AND semantics), and each token scores on a descending ladder:
- 1exactthe token equals the text
- 2prefixthe text starts with the token
- 3word-startany word starts with the token
- 4substringthe token appears anywhere
- 5fuzzy wordbounded edit distance — typo-tolerant, including word prefixes
- 6subsequencein-order characters — the last resort
One more deliberate boundary: clients search against getSearchIndex() — a minimal structural type — so chapter prose never ships in the browser bundle just to make titles searchable.
08
Component & design system
Forty shared components, built server-first: 37 of the 94 TypeScript modules opt into "use client" — interactivity (the reader’s controls, bookmarking, the studio editor) is isolated to islands while everything else renders on the server. Reuse is visible in the git history itself: “reusable BookCard component”, “shared AuthorCard”, “loading skeletons for all dynamic routes”.
The design system is typographically deliberate (documented in docs/design/): Cormorant Garamond for the brand, Literata for headings and long-form reading, Inter for UI — all self-hosted via next/font. The stated color philosophy: “KATHA should feel like walking into a quiet bookstore.”

09
Accessibility & performance
Quality work here is verifiable in the history rather than claimed: a dedicated “motion-safe sweep” pass, “next/font migration + motion consistency”, and “loading skeletons for all dynamic routes” all appear as commits in the v1.0 polish series. The global stylesheet carries explicit prefers-reduced-motion and focus-visible handling.
On performance, the architecture does the work: a server-first component tree, self-hosted fonts with no render-blocking requests, a search index that keeps prose out of the bundle, and a dependency tree of exactly three packages. No third-party scripts, no client-side data fetching for the catalogue.
10
Build history
64 commits, two tagged releases, one merged pull request — built in disciplined, named phases. A condensed arc, with subjects taken verbatim from the log:
Foundation
chore: initialize KATHA project · docs: add KATHA design foundation
Homepage
reusable Button · responsive navbar · homepage hero · reusable BookCard · discovery sections
Library
Phases 1–3 — real catalogue · URL-driven genre filtering · home shelves consume the catalogue
Reading state
reading location architecture · bookmarks · reading-history persistence · Continue Reading
Search
Phases 1–3 — pure fuzzy query engine · search components · /search page (merged as PR #1)
Authors
Phases 1–3 — author domain · profile navigation · /authors index + shared AuthorCard
v1.0 polish → v1.1.0
launch blockers · search index · shared icons · next/font migration · motion-safe sweep · loading skeletons
Studio
Phases 1–4 — route groups + Work domain · the writer's desk · chapter editor with autosave · reader-quality preview
Membership
Phases 1–4 — viewer domain · /join · free-preview edge · the author ladder
User/Author split
the person and the pen · Complete Author Profile — where the pen name is born
Editorial & covers
editorial covers + cover/publishedAt model · Studio cover upload · the curated catalogue — 15 titles, 13 shelves, 14 voices
Production → v2.0.0
craftsmanship sprint · "Museum pass: KATHA is production-ready"
11
Trade-offs & the road to v2
Every architecture buys something by giving something up. These are the trades KATHA makes today — each one paired with the seam the codebase already documents for undoing it:
localStorage persistence
Costs: Reading data is per-device and clearable; there is no cross-device sync.
The documented seam: The persistence modules are pure and, per their own docs, “safe to swap for an API / cloud sync.”
Static in-memory catalogue
Costs: Content ships with deploys; adding a book is a commit, not a form.
The documented seam: books.ts is shaped so a database or JSON payload satisfies it directly — “a one-line change” at the loader.
Deferred authentication
Costs: Accounts are a local stub until sessions exist; there is no real sign-in.
The documented seam: getViewer() is the single authentication seam; Supabase derives the same Viewer and “everything downstream is already correct.”
Client-side search
Costs: The search index ships to the browser and scales with the catalogue.
The documented seam: getSearchIndex() already minimises the payload (no prose); the pure engine could run server-side unchanged.
v2, therefore, is not a rewrite — it is filling seams the system was built around: real sessions via the getViewer() derivation, a database behind the books loader, and cloud sync behind the persistence modules. The UI layer does not change.
Read the code, then let's talk
The repository is public and every claim on this page is checkable against it.