Invoicing & expenses API for small businesses

A multi-tenant REST API covering organizations, roles, customers, invoices with a status machine, idempotent payments, expenses and reports. It is production-style: typed end to end, validated with zod, documented with an OpenAPI 3.1 spec generated from the code, and covered by 33 integration and unit tests.

Node 24TypeScript (strict)Fastify 5zodDrizzle ORMSQLite (node:sqlite) / Postgres-readyJWT + rotating refresh tokensRBACRFC 7807 errorsIdempotency keysRate limitingpinoVitest

What's inside

Multi-tenant by design

Every query is scoped to the organization in the JWT. Other tenants' records return 404, never 403, so their existence isn't leaked.

Auth that holds up

bcrypt hashing; 15-minute access JWTs; refresh tokens that rotate, are stored hashed, and revoke their whole family when reuse is detected; timing-safe login.

Correct money

Integer cents, per-line tax with half-up rounding, a server-side total, and a status machine (draft → sent → partially_paid → paid / void). Overpayments are rejected.

Safe retries

POST /payments requires an Idempotency-Key. Retries replay the stored response; reusing a key with a different body returns 422. Concurrent retries are serialized.

Operable

Request IDs, pino structured logs with secrets redacted, /health and /ready probes, rate limits, graceful shutdown, Docker and Compose files, CI.

Documented from code

The same zod schemas validate requests, serialize responses and generate this OpenAPI 3.1 document, so the docs can't drift from the code.

Try it MOCK

Pick a call and press Send. Nothing hits a server: the response was recorded from the real API during npm run docs. That run registers an organization, invites a member, creates customers and invoices, records idempotent payments and expenses, and triggers some errors on purpose.

Endpoints

MethodPathSummaryTag
GET/healthLiveness probeHealth
GET/readyReadiness probeHealth
POST/v1/auth/registerCreate an account and its organizationAuth
POST/v1/auth/loginExchange credentials for tokensAuth
POST/v1/auth/refreshRotate a refresh tokenAuth
POST/v1/auth/logoutRevoke a refresh token (and its family)Auth
GET/v1/auth/meCurrent user, organization and roleAuth
GET/v1/members/List organization membersMembers
POST/v1/members/Add a member (owner only)Members
PATCH/v1/members/{userId}Change a member's role (owner only)Members
DELETE/v1/members/{userId}Remove a member (owner only)Members
GET/v1/customers/List customersCustomers
POST/v1/customers/Create a customerCustomers
GET/v1/customers/{id}Get a customerCustomers
PATCH/v1/customers/{id}Update a customerCustomers
DELETE/v1/customers/{id}Delete a customer (owner only)Customers
GET/v1/invoices/List invoicesInvoices
POST/v1/invoices/Create a draft invoiceInvoices
GET/v1/invoices/{id}Get an invoice with lines and paymentsInvoices
PATCH/v1/invoices/{id}Update a draft invoiceInvoices
DELETE/v1/invoices/{id}Delete a draft invoice (owner only)Invoices
POST/v1/invoices/{id}/sendSend (finalize) a draft invoiceInvoices
POST/v1/invoices/{id}/voidVoid an invoiceInvoices
POST/v1/payments/Record a payment (idempotent)Payments
GET/v1/payments/List paymentsPayments
GET/v1/expenses/List expensesExpenses
POST/v1/expenses/Record an expenseExpenses
DELETE/v1/expenses/{id}Delete an expenseExpenses
GET/v1/reports/summaryFinancial summary for a periodReports

Tests & coverage

33/33 tests passing. Integration tests call the real HTTP layer through app.inject, and every test file gets a fresh, migrated SQLite database.

Statements95.1%
Branches83.4%
Functions89.0%
Lines95.1%
tests/auth.test.ts 11 tests
  • ✓ auth › registers an owner with a new organization and returns tokens
  • ✓ auth › rejects duplicate emails with 409 problem+json
  • ✓ auth › returns field-level validation errors for weak passwords and bad emails
  • ✓ auth › uses the same 401 for unknown email and wrong password (no user enumeration)
  • ✓ auth › rotates refresh tokens and revokes the whole family when an old token is replayed
  • ✓ auth › logout revokes the refresh token
  • ✓ auth › rejects missing, malformed and tampered access tokens
  • ✓ platform › exposes liveness and readiness probes
  • ✓ platform › propagates a valid X-Request-Id and generates one otherwise
  • ✓ platform › answers unknown routes and malformed JSON with problem+json
  • ✓ platform › rate-limits the auth endpoints
tests/invoices-payments.test.ts 10 tests
  • ✓ invoices › computes per-line tax with half-up rounding and server-side totals
  • ✓ invoices › validates the body (lines required, dates ordered, positive quantities)
  • ✓ invoices › enforces the status machine: edit only drafts, send once, void only unpaid
  • ✓ invoices › deletes drafts only and never reuses invoice numbers
  • ✓ invoices › paginates, filters and sorts the invoice list
  • ✓ payments › requires an Idempotency-Key header
  • ✓ payments › replays the original response for a retried request and never double-charges
  • ✓ payments › rejects reusing a key with a different body (422)
  • ✓ payments › moves invoices to partially_paid then paid, and blocks overpayment and draft payments
  • ✓ payments › summarizes invoiced, collected, expenses and aging in the report
tests/rbac-tenancy.test.ts 6 tests
  • ✓ RBAC › members can work with customers but only owners can delete them
  • ✓ RBAC › only owners manage members, and the last owner cannot be demoted or removed
  • ✓ RBAC › members may delete only their own expenses
  • ✓ tenant isolation › another organization's resources are invisible (404, never 403)
  • ✓ tenant isolation › cannot reference another tenant's customer or pay another tenant's invoice
  • ✓ tenant isolation › invoice numbers are sequential per organization
tests/unit-openapi.test.ts 6 tests
  • ✓ domain logic (unit) › rounds half-up robustly and totals invoices line by line
  • ✓ domain logic (unit) › encodes the invoice state machine
  • ✓ domain logic (unit) › buckets receivables by days past due
  • ✓ domain logic (unit) › fingerprints request bodies independent of key order
  • ✓ domain logic (unit) › refuses to boot in production with the development JWT secret
  • ✓ OpenAPI › generates an OpenAPI 3.1 document from the route schemas

API reference

The full OpenAPI 3.1 reference (schemas, parameters, responses) opens on its own page, rendered with Redoc bundled locally (no CDN). Keeping it separate keeps this page light and responsive.

Open the full API reference   Download openapi.json