Naalya Handbook

Conventions

Code style, branch and commit naming, the PR workflow, and the known debt you should not copy.

Every codebase has a grain to it — write with the grain and your change disappears into the surroundings; write against it and a reviewer spends their time on formatting instead of your logic. This page is the grain of the Education Hub. It's the short version of the house rules in CLAUDE.md, the file the AI agents and the humans both read before touching anything.

None of this is exotic. It's the same handful of conventions repeated everywhere, plus one short list of known debt you'll see in the code but should not propagate. Read that last section especially — copying an existing mistake is the easiest way to add a new one.

Code style

The formatting rules are not up for debate per file — Prettier and ESLint own them, and the config is the TanStack shared config. You don't hand-format; you let the tools do it.

RuleSetting
Semicolonsnone
Quotessingle
Trailing commasyes
TypeScriptstrict mode
Path alias@/* resolves to src/*

The one command to remember is bun run check — it runs prettier --write then eslint --fix across the repo. Run it before you push, every time.

Before you push
bun run check   # prettier --write . && eslint --fix

A pre-commit hook already runs this

Husky and lint-staged are wired up in package.json, so staged files get Prettier and ESLint on commit. That's a safety net, not a substitute — run bun run check yourself so you see (and fix) lint errors before they block the commit.

Always reach for the @/* alias instead of long relative chains. import { cn } from '@/lib/utils' reads the same from any depth; ../../../lib/utils doesn't.

Branches

Branch names are type/kebab-phrase — a type prefix, a slash, then a short kebab-case phrase describing the intent, not the implementation.

  • Type is one of feat, fix, chore, refactor, docs.
  • Keep the whole name short — under about 30 characters.
  • Describe the outcome (fix/token-refresh-queue), not the mechanics (fix/move-axios-interceptor-into-a-promise).
Good branch names
feat/create-user
fix/token-refresh-queue
chore/add-update-endpoint

If you realize partway through that the name no longer fits the work, rename it with git branch -m <new-name>unless the branch has already had merges, in which case leave it alone.

Commits

Commit messages follow type(resource): description — a type, the affected resource in parentheses, then a clear description in present tense.

Commit message format
feat(api): add build:watch to dev script
fix(inquiries): clear stale filter state on tab change
chore(build): bump chowbea-axios to 2.1.2

The type matches the branch prefixes. The resource is whatever area you touched — api, build, inquiries, auth. Present tense ("add", not "added") keeps the log reading as a list of changes the merge makes, not a diary of what you did.

Pull requests

The default target branch is staging, not main — open your PR against staging unless someone tells you otherwise.

The repo ships a PR template, and you fill it. Every section earns its place:

SectionWhat goes in it
SummaryA line or two on what the PR does and why
ChangesA concise list of the specific changes
Migration / Breaking ChangesSetup, config, or workflow changes — or "None"
Environment VariablesAny added, changed, or removed vars (and update .env.example)
Closes Issue(s)Any open issue this PR closes

Two things the template does not want, and that you should never add:

No footer, no test plan

Do not add a "Generated by Claude Code" footer, and do not add a test-plan section. Neither belongs in this repo's PRs — the template above is the whole body. Describe all the changes on the branch, not just the last session's.

Known debt

This is the section that saves you. The codebase carries a handful of deliberately documented shortcuts — they work, but they're not the pattern. You'll run into them while reading existing features. Recognize them, don't copy them.

Don't propagate these patterns

The items below are listed as known debt in CLAUDE.md. When you build something new, do the right thing instead — and if you're already editing a file that has one, fixing it is welcome.

  • Sorting UI is unwired. Tables set up getSortedRowModel, but the column headers don't actually trigger sorts. Don't assume a sortable-looking header works — wire it through if a feature needs sorting.
  • Staff tables over-fetch and client-paginate. Most staff tables request limit: 100 and paginate in the browser. That's fine for small sets, but for anything that can grow large, prefer the server cursor pagination (useCursorParams + CursorPaginationFooter).
  • Inline empty states. Some empty states use one-off inline classes (h-62 bg-muted flex ...) instead of the .page__empty-container utility. Always use .page__empty-container for page-level empty states.
  • Breakpoint grids. A few grids still use sm:grid-cols-* / lg:grid-cols-* instead of the container-aware auto-fill + minmax() grids. New grids should be container-aware so they reflow when a side panel opens.
  • The activeTab breadcrumb suffix is dead code. The breadcrumb builder carries an activeTab suffix that nothing renders. Ignore it; don't build on it.

The first three live in the table and component patterns — if any of these are new to you, the pages below show how the intended version looks.

Where to go next

On this page