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.
| Rule | Setting |
|---|---|
| Semicolons | none |
| Quotes | single |
| Trailing commas | yes |
| TypeScript | strict 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.
bun run check # prettier --write . && eslint --fixA 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).
feat/create-user
fix/token-refresh-queue
chore/add-update-endpointIf 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.
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.2The 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:
| Section | What goes in it |
|---|---|
| Summary | A line or two on what the PR does and why |
| Changes | A concise list of the specific changes |
| Migration / Breaking Changes | Setup, config, or workflow changes — or "None" |
| Environment Variables | Any 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: 100and 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-containerutility. Always use.page__empty-containerfor page-level empty states. - Breakpoint grids. A few grids still use
sm:grid-cols-*/lg:grid-cols-*instead of the container-awareauto-fill+minmax()grids. New grids should be container-aware so they reflow when a side panel opens. - The
activeTabbreadcrumb suffix is dead code. The breadcrumb builder carries anactiveTabsuffix 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.