Purpose-Bound Account Service — dual-pool accounts with purpose-based MCC validation, powered by TigerBeetle.
A Rust service that manages purpose-restricted financial accounts (e.g., health, education, food, transport). Each account has two internal pools:
- Self-contribution — funds deposited from the account holder's own bank account. Can be used for payments and withdrawals.
- Others-contribution — funds deposited from third parties (employer, family, etc.). Can only be used for purpose-restricted payments, never withdrawn.
Payments are validated against allowed Merchant Category Codes (MCCs) for the account's purpose, and funds are drawn from the others-pool first before falling back to the self-pool.
Axum HTTP handlers
|
Service layer (business logic, MCC validation, pool splitting)
|
Repository layer
|
PostgreSQL (account metadata) + TigerBeetle (ledger / balances)
| Method | Endpoint | Description |
|---|---|---|
GET |
/purpose-types |
List all purpose types with allowed MCCs |
GET |
/purpose-types/{code} |
Get a specific purpose type |
POST |
/accounts |
Create a purpose-bound account |
GET |
/accounts/{id} |
Get account details |
GET |
/accounts/{id}/balance |
Get pool balances |
PATCH |
/accounts/{id}/status |
Freeze/reactivate an account |
POST |
/accounts/{id}/deposits |
Deposit funds (auto-routed to self/others pool) |
POST |
/accounts/{id}/payments |
Pay a merchant (MCC-validated, others-first splitting) |
POST |
/accounts/{id}/withdrawals |
Withdraw from self-pool only |
POST |
/normal-accounts/{id}/transfers/{id}/reverse |
Reverse a posted transfer (admin) |
The API is defined using Smithy models under model/. A generated Rust client SDK lives at crates/pba_client/.
The easiest way to get all dependencies is via Nix:
nix developThis provides Rust, PostgreSQL 16, TigerBeetle, Zig 0.14, just, sqlx-cli, cargo-watch, and Smithy CLI.
Alternatively, install manually via Homebrew:
just install-depsStart everything (Postgres + TigerBeetle + app):
just run # starts all services via process-compose, Ctrl+C stops everythingThe service starts on http://localhost:3030. Open the Admin Dashboard at:
http://localhost:3030/admin
From there you can browse accounts, create new ones, make deposits/payments/withdrawals, and view purpose types with their allowed MCCs.
Start in background:
just run-bg # starts detached; use 'just logs' to attach, 'just stop' to stopUnit tests:
just testE2E tests (Cucumber BDD):
just api-e2e # API tests only (via Smithy SDK client)
just ui-e2e # Browser UI tests only (headless Chrome)
just e2e-all # Both API + browser testsTo run browser tests with visible Chrome (useful for debugging):
just e2e-start
just ui-e2e-watch # opens Chrome so you can watch the tests
just e2e-stopE2E tests use isolated infrastructure — a separate Postgres database (pba_service_test), TigerBeetle instance (port 3001), and app port (3031) — so they never touch dev data.
Full local CI (format + lint + build + test):
just local-cijust migrate # run pending migrations
just migrate-new add_field # create a new migrationjust smithy-validate # validate the model
just smithy-build # regenerate the Rust client SDKPBA uses Keycloak for authentication. just run starts Keycloak alongside other services.
Admin UI: Navigate to http://localhost:3030/admin — you'll be redirected to Keycloak to log in.
Default credentials: admin@pba.local / admin
API access: Use the Authorization header with a base64-encoded client_id:client_secret:
API_KEY=$(echo -n "pba-api:pba-api-secret" | base64)
curl -H "Authorization: ApiKey $API_KEY" http://localhost:3030/purpose-typesDisable auth (development): Set AUTH_ENABLED=false in your .env file.
Environment variables (loaded from .env):
| Variable | Default | Description |
|---|---|---|
DB_HOST |
localhost |
Postgres host (use a path like /tmp for Unix socket) |
DB_PORT |
5432 |
Postgres port |
DB_NAME |
pba_service |
Postgres database name |
DB_USER |
$USER (Unix socket) |
Postgres user (required for TCP connections) |
DB_PASSWORD |
(empty) | Postgres password (supports encrypted values via SECRETS_PROVIDER) |
TIGERBEETLE_ADDRESSES |
3000 |
TigerBeetle address(es) |
TIGERBEETLE_CLUSTER_ID |
0 |
TigerBeetle cluster ID |
HOST |
0.0.0.0 |
Bind address |
PORT |
3030 |
HTTP port |
RUST_LOG |
pba_service=debug,tower_http=info |
Log level (tower_http=info enables per-request access logs) |
OIDC_ISSUER_URL |
http://localhost:8180/realms/pba |
OIDC provider issuer URL (discovery via .well-known/openid-configuration) |
OIDC_CLIENT_ID |
pba-admin |
OIDC client ID for admin UI login flow |
COOKIE_SECRET |
(dev default) | 32+ byte secret for session cookie signing |
AUTH_ENABLED |
true |
Set to false to disable auth |
PATH_PREFIX |
(empty) | URL prefix for reverse proxy / ingress (e.g., /pba) |
Run just to see all targets:
just run # Start everything via process-compose (Ctrl+C stops all)
just run-bg # Start everything in the background (detached)
just logs # Attach to running process-compose instance
just stop # Stop all services
just build # Build the project
just test # Unit tests
just api-e2e # API E2E tests (isolated infra)
just ui-e2e # Browser UI E2E tests (headless Chrome)
just ui-e2e-watch # Browser UI tests with visible Chrome
just e2e-all # All E2E tests (API + browser)
just local-ci # Format + lint + build + test
just migrate # Run database migrations
just smithy-build # Regenerate Smithy SDK