Thank you for contributing! This guide covers everything you need to get started: branch conventions, the verified test commands, and PR expectations.
For a deeper look at how the codebase is structured, see docs/architecture.md.
For component conventions (naming, ordering, and boolean-props), see docs/PROP_CONVENTIONS.md.
For ambient types, module augmentation, and never-narrow patterns, see docs/TYPESCRIPT_CONVENTIONS.md.
For standard component state implementations (default, hover, focus, disabled, error, loading), see docs/COMPONENT_STATES.md.
For authenticated browser data fetching, also read docs/client-api.md before adding new fetch calls.
For the UI PR review checklist that pairs with the PR expectations below, see docs/CODE_REVIEW.md (reviewers can also reference it directly).
- Prerequisites
- Branch Naming
- Development Setup
- Test Commands
- PR Expectations
- Where to Add Things
- Adding an i18n Key
- Node.js 18+
- npm (or pnpm — a
pnpm-lock.yamlis present) - A
.envfile with at minimumDATABASE_URL="file:./dev.db"andSESSION_PASSWORD(≥32 chars)
npm install
npx prisma migrate dev
npm run devFollow the pattern already used in this repo:
| Type | Pattern | Example |
|---|---|---|
| UI/UX work | uiux/<short-description> |
uiux/dashboard-empty-states |
| Feature | feat/<short-description> |
feat/recurring-remittance |
| Bug fix | fix/<short-description> |
fix/session-expiry-redirect |
| Docs | docs/<short-description> |
docs/contributing-architecture |
| Refactor | refactor/<short-description> |
refactor/contract-cache-types |
git checkout -b <your-branch>
npm install
npx prisma migrate dev # only needed if schema changed
npm run devWhen you need a backend to develop against without running the full Next.js server, start the mock backend:
npm run mock:backendThe mock backend listens on http://localhost:4010 by default (set MOCK_BACKEND_PORT
to use a different port). It is idempotent and keeps all data in memory — stopping and
restarting it does not create or migrate state. It serves mock responses for:
GET /api/health,/api/dashboard,/api/remittance/quotePOST /api/auth/nonce,/api/auth/login,/api/auth/logoutGET /api/transactions,/api/split,/api/goals,/api/billsGET /api/insurance,/api/family
All money values are returned as integer minor units.
All commands below are verified against package.json. For contributor testing guidelines and expectations, see docs/TESTING_STANDARDS.md. For the full multi-runner reference and test recipes, see docs/testing.md.
just check # Run lint + typecheck + test (requires `just`)
npm run check # Run lint + typecheck + test (no extra deps)
npm run lint # ESLint across the whole project
npm run typecheck # TypeScript type-check (tsc --noEmit)
npm run build # Next.js production build (also runs tsc)
npm run test # Alias for test:unit
npm run test:unit # Runs BOTH node:test suites AND Vitest unit tests (see below)
npm run test:coverage # Vitest run with coverage report
npm run test:watch # Vitest in watch mode
npm run test:ui # Vitest UI
npm run test:property # Property-based tests via Vitest
npm run test:integration # node:test integration suites + Vitest integration tests
npm run test:e2e # Playwright end-to-end testsnpm run test:unit runs two sub-commands back-to-back:
test:unit:node → node --test tests/unit/webhooks-verify.test.cjs
tests/unit/middleware.test.cjs
tests/unit/contract-cache.test.cjs
test:unit:vitest → vitest run tests/unit/validation/savings-goals.test.ts
Why two runners? The .cjs suites use Node's built-in node:test runner because they test CommonJS-compatible modules (webhook verification, raw middleware logic, and the LRU contract-cache) that do not need a browser-like environment. The .ts Vitest suite handles TypeScript validation logic and benefits from Vitest's expect API and fast ESM transform. When adding new unit tests:
- Write
.cjswithnode:test/assertwhen testing a Node-native module (crypto, HTTP internals, or anything inlib/that has no JSX). - Write
.tswith Vitest when you need TypeScript types,vi.mock, orexpectmatchers.
npm run test:integrationThis runs:
node --test tests/integration/auth.test.cjs tests/integration/health.test.cjs tests/integration/validation.test.cjs— node:test suites that spin up real HTTP calls against a running (or in-process) server.vitest run tests/integration/api/goals-validation.test.ts— Vitest integration tests for the goals API.
Integration tests require DATABASE_URL to point to a real (SQLite) database. They do not mock the database layer.
npm run test:e2e # Playwright — requires the dev server to be running- Claim the issue first — comment on the GitHub issue before starting work.
- One concern per PR — keep scope tight; large PRs stall in review.
- Lint must pass — run
npm run lintlocally before pushing. CI will reject lint failures.- Note on React Hooks:
useEffectwith no dependency array is banned to prevent accidental infinite loops and performance issues. You must provide an explicit dependency array or an// eslint-disable-next-line no-restricted-syntax(or biome equivalent) with a reason if it is truly intended to run on every render. useStatewith an initial function call (e.g.useState(myExpensiveInit())) is banned to prevent it from running on every render. Use a lazy initializer (e.g.useState(() => myExpensiveInit())) instead.
- Note on React Hooks:
- Build must pass — run
npm run buildto catch TypeScript errors early. - Tests — add or update tests that cover your change. Prefer the existing runner for the file type (
.cjs→ node:test,.ts→ Vitest). - PR description — summarise what changed and why. Link the issue (
Closes #NNN). - No commented-out code — remove dead code rather than commenting it out.
- i18n — any user-visible string must use the i18n system (see Adding an i18n Key).
Example commit message style used in this repo:
feat: add recurring remittance schedule UI
Implements the schedule picker and wires it to POST /api/remittance/recurring.
Closes #301
| What you're adding | Where it lives |
|---|---|
| New page/route | app/<route-name>/page.tsx (Next.js App Router) |
| Shared UI component | components/ui/ |
| Feature-specific component | components/<feature>/ |
| React hook | lib/hooks/ |
| Server-side API route | app/api/<resource>/route.ts |
| Versioned API route | app/api/v1/<resource>/route.ts |
| Soroban contract read/write | lib/contracts/<feature>.ts |
| Cached contract reads | wrap in lib/cache/contract-cache.ts helpers |
| Auth/session helpers | lib/session.ts or lib/auth/ |
| Database queries | lib/db.ts or a dedicated file under lib/ |
| i18n strings | lib/i18n/locales/en.json and lib/i18n/locales/es.json |
| Type definitions | types/ |
| Utility functions | utils/ or a file under lib/ |
See docs/architecture.md for a full layer map.
-
Add the key + English value to
lib/i18n/locales/en.json. -
Add the same key with a Spanish translation to
lib/i18n/locales/es.json. Use a placeholder ("[ES] <English text>") if you are not a Spanish speaker — a translator will follow up. -
In server components or API routes, use the
getTranslatorhelper fromlib/i18n/index.ts:import { getTranslator } from '@/lib/i18n'; const t = getTranslator(request.headers.get('accept-language')); t('your.new.key');
-
In client components, use the
useClientLocalehook fromlib/i18n/client.ts:import { useClientLocale } from '@/lib/i18n/client'; const { t } = useClientLocale(); t('your.new.key');
Join the RemitWise Discord or open a discussion on GitHub.