Skip to content

PeteShepley/ledger-api

Repository files navigation

ledger-api

A small double-entry bookkeeping API — accounts, transactions, and the immutable entries that belong to them. Balances are always derived by summing entries, never stored and mutated directly. TypeScript on AWS Lambda, Aurora Serverless v2 (Postgres) via the RDS Data API, IAM-authenticated (SigV4) API Gateway.

Meant to be called by other services/applications, not end users directly — see the auth section below.

For why things are built this way, and how the deployment infra fits together, see the design doc and build journal in the sibling operations repo: operations/docs/projects/ledger-api/design.md and operations/docs/projects/ledger-api/journal.md. The original domain design is operations/ledger-api-plan.md.

Structure

src/
  app.ts              Hono app: mounts every router, error handling
  index.ts             Lambda entrypoint — handle(app)
  db.ts                 Drizzle client factory — aws-data-api/pg in Lambda, node-postgres in tests
  schema.ts              accounts / transactions / entries tables
  routes/                 accounts.ts, transactions.ts, balances.ts
  lib/
    validation.ts           zod schemas, zJson (422 on body validation failure), zIdParam (404 on malformed :id)
    errors.ts                ApiError, isUniqueViolation (idempotency-replay detection, cross-driver)
    ledger.ts                 shared balance/lookup queries
    case.ts / response.ts      snake_case wire format bridge
drizzle/                 SQL migrations (drizzle-kit generate), including the zero-sum constraint trigger
openapi/ledger-api.yaml  full API spec
tests/                  vitest, against a real local Postgres (see design.md's "Testing tradeoff")

Commands

Command Action
npm install Install dependencies
npm run lint Lint (oxlint)
npm run typecheck Typecheck
docker compose up -d Start a local Postgres for tests (matches CI: postgres:16, db ledger, user/pass postgres)
npm test Run tests — needs DATABASE_URL pointed at a local Postgres (defaults to the compose instance)
npm run build Bundle src/index.ts into dist/index.mjs (esbuild)
npm run db:generate Generate a migration from src/schema.ts
npm run db:migrate Apply migrations — targets Data API if RESOURCE_ARN/SECRET_ARN are set, else DATABASE_URL
npm run start:local Build, then serve the real Lambda handler behind a local API Gateway emulator (see below)

Local development

npm run start:local runs the app the way it actually runs in prod — the built dist/index.mjs Lambda handler, invoked in a containerized Lambda runtime behind a local HTTP API Gateway emulator (sam local start-api, via template.yaml) — rather than a plain Node dev server. That means requests go through the same APIGatewayProxyEventV2 translation/hono/aws-lambda code path production traffic does, so payload-shape bugs show up locally instead of only after deploy.

Requires AWS SAM CLI (brew install aws-sam-cli) and Docker. One-time setup:

docker compose up -d                                                  # start Postgres
DATABASE_URL=postgres://postgres:postgres@localhost:5432/ledger \
  npm run db:migrate                                                  # apply migrations

Then:

npm run start:local   # serves http://127.0.0.1:3000
curl -X POST http://127.0.0.1:3000/accounts \
  -H 'content-type: application/json' \
  -d '{"name": "cash", "type": "asset", "currency": "USD"}'

template.yaml points the function at postgres://postgres:postgres@postgres:5432/ledger — the sam local container joins the compose project's Docker network (--docker-network ledger-api_default, wired into the npm script) and reaches Postgres by its compose service name, not localhost.

Not covered: AWS_IAM auth. In prod that's enforced by API Gateway before the Lambda is ever invoked, and sam local doesn't emulate authorizers at all — there's no app-level auth behavior to exercise either way. See operations/docs/runbooks/ledger-api-deployment.md for testing SigV4-signed requests against a deployed stack.

Endpoints

See openapi/ledger-api.yaml for the full contract; summary:

  • POST/GET /accounts, GET /accounts/{id}
  • GET /accounts/{id}/balance — current, or ?as_of=YYYY-MM-DD
  • GET /accounts/{id}/entries — paginated
  • POST /transactions — requires idempotency_key; a duplicate returns the original transaction with 200
  • GET /transactions/{id}
  • GET /balances — all accounts, current or ?as_of=

Auth

Every route requires AWS_IAM (SigV4). Callers need an IAM policy granting execute-api:Invoke on this API's ARN — see operations/docs/runbooks/ledger-api-deployment.md for the exact policy shape and how to sign requests.

About

Double-entry bookkeeping API — accounts, transactions, entries. TypeScript on AWS Lambda, Aurora Serverless v2 (Postgres) via the RDS Data API, IAM-authenticated API Gateway.

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

No releases published

Packages

 
 
 

Contributors