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.

CommandWhat it checks
pnpm lintESLint with Prettier, plus the design-token check (pnpm lint:tokens).
pnpm --filter @octogriffin/web exec tsc --noEmitTypeScript in strict mode.
pnpm testJest unit and component tests.
pnpm buildA production build of every app.
pnpm knipUnused 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 any to silence the compiler. Import types with import 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 attributeOrder does.
  • A blank line before returns and after blocks, braces on every if and loop.
  • No console logging. Use the logger in src/lib/logger.ts on 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.

FolderWhat 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 ActionError for 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 isLoading and hasLoaded separately: 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) and dark: colour overrides fail pnpm 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 ButtonIcon with a label, which becomes both the accessible name and the tooltip. Don't use title attributes.
  • Grain is for magic. The arcane and mist gradients 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-motion allows.

Testing

  • Tests live next to the code (thing.test.ts beside thing.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, textContent or 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
Native arrow-key handling in radio groups and focus returning after an animation need a real browser. Check those by hand and mention it in your pull request.

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:

  • feat and fix subjects describe what changes for the user, in plain English.
  • Refactors are refactor commits, 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_change

Migrations run automatically on deploy, so make sure they work on a database that already has data in it.

Something went wrong?
Open an Issue on GitHub →