Skip to main content

Coding conventions

Most of these are enforced by CI (pnpm lint, pnpm typecheck, pnpm test), not just documented — if you can get a PR past those three, you've already satisfied most of this page.

TypeScript​

tsconfig.base.json runs strict plus a few settings stricter than the default:

  • noUncheckedIndexedAccess — indexing an array or record gives you T | undefined, not T. This catches a real class of bugs at the type level (a lookup that silently returns undefined treated as present).
  • noImplicitOverride — overriding a base method requires the override keyword.
  • verbatimModuleSyntax — type-only imports must say so (import type { Foo }), which ESLint's @typescript-eslint/consistent-type-imports also enforces.

Money is numeric(19,4) in PostgreSQL and string in TypeScript — never number. Floating-point and currency don't mix, and this is the one rule in this document that's about correctness rather than style. Do the arithmetic with @wickermoney/plugin-sdk/money rather than a local helper (see Writing a plugin): it refuses a fifth decimal place instead of truncating, and rounds the same way the API does.

Architecture guards (ESLint)​

Two rules exist specifically to keep the layering in Architecture real rather than aspirational:

  • Files under apps/api/src/**/routes/** and apps/api/src/**/service/** can't import Kysely or the connection helpers directly — a handler or service that needs new data access gets a repository method, not a raw query. (Sanctioned exceptions: the plugin host adapter and the DB connectivity healthcheck, which are infrastructure that hands out a connection or checks reachability, not business logic.)
  • Browser code (apps/web, plugins, packages/*) can't call crypto.randomUUID directly — it only exists over HTTPS or localhost, and Wicker Money is meant to also run over plain HTTP on a LAN. Use newUuid() instead, which falls back to getRandomValues.

Testing​

  • Unit tests (pnpm test) — no database. Services are tested against in-memory fakes of their repositories.
  • Integration tests (pnpm test:integration) — files named *.integration.test.ts, run against a real PostgreSQL with row-level security actually enabled. Anything touching tenant isolation belongs here, not behind a mock — a mock can't catch a policy that's missing or wrong.
  • Faking the clock: call vi.useFakeTimers({ toFake: ['Date'], now }) before registering users or issuing tokens, not after. A token issued against the real clock and checked against a faked one can look expired. Six integration tests failed with 401 from the 1st to the 14th of every month for exactly this reason until 0.2.1. Restore the clock with vi.useRealTimers() in afterEach.
  • Dates in tests: date logic that depends on "today" takes it as an argument (as horizonEnd(today, horizon) and the @wickermoney/plugin-sdk/recurrence functions do) rather than reading new Date() deep inside. The service resolves the user's today once and passes it down, so the pure functions can be tested on any day, including month ends and February 29.

Commits​

Conventional Commits for commit subjects — feat:, fix:, docs:, chore:, etc. Release notes are generated from them, so an inaccurate prefix produces an inaccurate changelog entry, not just a style nit.

Formatting​

ESLint runs on save/CI (eslint.config.mjs, flat config); there's no separate Prettier pass documented here yet — if your editor already formats on save against the ESLint config, you're set.