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 youT | undefined, notT. This catches a real class of bugs at the type level (a lookup that silently returnsundefinedtreated as present).noImplicitOverride— overriding a base method requires theoverridekeyword.verbatimModuleSyntax— type-only imports must say so (import type { Foo }), which ESLint's@typescript-eslint/consistent-type-importsalso 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/**andapps/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 callcrypto.randomUUIDdirectly — it only exists over HTTPS orlocalhost, and Wicker Money is meant to also run over plain HTTP on a LAN. UsenewUuid()instead, which falls back togetRandomValues.
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 with401from the 1st to the 14th of every month for exactly this reason until 0.2.1. Restore the clock withvi.useRealTimers()inafterEach. - Dates in tests: date logic that depends on "today" takes it as an
argument (as
horizonEnd(today, horizon)and the@wickermoney/plugin-sdk/recurrencefunctions do) rather than readingnew 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.