Development Standards
The conventions that keep Octogriffin consistent: how the code is checked, organised, styled and tested. Most of them are enforced by tooling, so if a check fails, this page explains why it exists.
Checks and tooling
The repository is a pnpm workspace: the website and dashboard live in apps/web (Next.js), the API in apps/api (Hono), and the database schema in packages/database. Run every command from the repository root unless it says otherwise.
| Command | What it checks |
|---|---|
pnpm lint | ESLint with Prettier, plus the design-token check (pnpm lint:tokens). |
pnpm --filter @octogriffin/web exec tsc --noEmit | TypeScript in strict mode. |
pnpm test | Jest unit and component tests. |
pnpm build | A production build of every app. |
pnpm knip | Unused files, exports and dependencies. |
CI runs lint, types, tests and the build on every pull request. Husky runs the same checks locally: ESLint and Prettier on staged files before each commit, commitlint on the message, and lint, tests and the build before each push.
Code style
- TypeScript, strict. No
anyto silence the compiler. Import types withimport type; the linter rewrites mixed imports for you. - Prettier decides formatting: 80 columns, single quotes, semicolons, trailing commas, and no parentheses around a single arrow-function parameter.
- Everything is sorted. The perfectionist rules sort imports, object keys, JSX props, union types and array literals alphabetically. When order carries meaning, for example Habitica's STR, INT, CON, PER, don't write it as an array literal; derive it from ranks, as
attributeOrderdoes. - A blank line before returns and after blocks, braces on every
ifand loop. - No console logging. Use the logger in
src/lib/logger.tson the server; unused imports and variables fail the build. - Comments explain why, not what. Name things after what users see (
describeTrigger,recipeToTriggerValues), not after the implementation.
Project structure
Inside apps/web/src, each folder has one job. Put new code where its neighbours live.
| Folder | What goes there |
|---|---|
app/ | Routes. Pages are server components unless they need state or browser APIs. |
components/ui/ | Shared building blocks: Button, ButtonIcon, Tooltip, Menu, Dialog parts, SegmentedControl, Chip and friends. |
components/…/ | Feature components, grouped by area (dashboard, docs, recipes, roadmap). |
actions/ | Server actions, one per file, called from client components. |
accessors/ | Prisma queries. The only place that talks to the database. |
schemas/ | Zod schemas, shared by forms and server actions. |
store/ | Zustand stores for client state that several components share. |
hooks/ | React hooks, such as useRepositories or useRecipeParam. |
utils/ | Pure functions with no React or database access, each with a test. |
config/ | Content as data: recipes, the roadmap, the docs navigation. |
rewards/ | Turning GitHub events into Habitica scores. |
seo/, json-ld/ | Page metadata and structured data. Every new page is registered in seo/pageCatalog.ts. |
Server actions and data
- Wrap every server action in
runUserAction. It checks the session, loads the GitHub user and shapes the result, so the client always gets{ success, data, error }. - Validate input with
parseInput(schema, data), never trust what the browser sends. - Throw
ActionErrorfor problems the user should read ("Trigger not found."). Anything else is logged and shown as an unexpected error. - Scope queries to the signed-in user in the accessor, so one user can never read or change another user's triggers.
- Never log, return or send secrets to the browser. Habitica API tokens are encrypted at rest and only decrypted on the server for a request.
- In client stores, track
isLoadingandhasLoadedseparately: an empty state is only true once the first load has finished, and a component should only fetch when nothing has loaded or is loading yet.
UI and the design system
The design language is described in the design spec. The short version:
- Use the semantic colour tokens (
bg-surface,text-text-2,border-border,bg-accent,text-hp…). They switch between light and dark mode on their own. Tailwind's built-in palettes (slate, violet, red and so on) anddark:colour overrides failpnpm lint:tokens. - Wrap pages in Shell so every page shares the header's edges. Content pages open with
PageHeader. - Reuse components/ui before building something new. Icon-only buttons use
ButtonIconwith alabel, which becomes both the accessible name and the tooltip. Don't usetitleattributes. - Grain is for magic. The
arcaneandmistgradients are for Habitica moments (hero, character sheet, roadmap, page headers), never for forms, tables or dialogs. - Icons come from Hugeicons, the free stroke set only. No bulk, solid or two-tone variants.
- Write copy in sentence case, from the user's point of view, with buttons that say what they do ("Add trigger", not "Submit").
- Keep it accessible: real buttons and links, visible focus, labels on every control, and no motion beyond what
prefers-reduced-motionallows.
Testing
- Tests live next to the code (
thing.test.tsbesidething.ts) and run with Jest and Testing Library. - Write the test first and watch it fail for the right reason. A test that passes before the change exists proves nothing.
- Put logic in pure functions in
utils/and test them directly; keep components thin. - When a component uses a store, mock the server action modules it imports (
jest.mock('@/actions/…')). Importing a real action pulls in Prisma, which doesn't run in jsdom. - There's no jest-dom: assert on DOM properties such as
classList,textContentor attributes. - Check what the first server render says with
renderToString, so loading states never claim a list is empty.
Some behaviour can't be tested in jsdom
Commits and changelog
Commits follow Conventional Commits and feed the public changelog, which Habitica players read. The details are in How to Contribute; the rules that matter most:
featandfixsubjects describe what changes for the user, in plain English.- Refactors are
refactorcommits, kept apart from fixes and features. - Use the app as the scope (
feat(web): …). Bigger changes land as one squash-merged pull request with one clear changelog entry.
Database changes
Change apps/web/prisma/schema.prisma, then create and commit a migration with your change:
pnpm --filter @octogriffin/web prisma:migrate:dev --name describe_your_changeMigrations run automatically on deploy, so make sure they work on a database that already has data in it.