The privacy-first backend for Totals — a personal finance app for markets with fragmented banking. Designed to hold as little user data as possible and to be trustworthy by architecture, not by promise.
Totals-Engine is the backend that powers the few features in the Totals app that genuinely require coordination between devices — primarily collaborative settle-up (a shared ledger between people who split shared expenses) and identity recovery (encrypted backup of a device's cryptographic identity).
Everything else in the app — SMS-based transaction parsing, budgeting, analytics, multi-account tracking, the unified ledger — runs entirely on the user's device and never touches a server.
It's not a conventional fintech backend. There are no user accounts in the usual sense. There is no email, password, or phone number collection. There is no admin panel to look up users because there is nothing to look up. If someone subpoenas Totals-Engine for a specific user's financial history, the honest answer is that we don't have it.
These shape every design decision:
- Zero-knowledge by default. The server must be unable to read user data, even if fully compromised. Group names, member display names, shared expenses, recovery vaults — all encrypted on-device before they reach the server.
- Ephemeral where possible. Data is deleted as soon as it's no longer needed for delivery.
- Device is source of truth. The server is infrastructure, not a database of users' lives.
- Stateless services. Anything that can be stateless, is.
- Open source and self-hostable. Users who don't trust the official instance can run their own.
The cryptography is real: Ed25519 device identity, X25519 ECDH for member
key exchange, AES-256-GCM for all user content, Argon2id for recovery PINs.
We use audited libraries (@noble/curves, @noble/ciphers), not hand-rolled
crypto.
Each device generates an Ed25519 keypair on first launch. The public key is the device's identity; the private key never leaves the device. Every authenticated request is signed against a short-lived server-issued challenge. Group content is encrypted client-side with a shared symmetric key that's exchanged between devices via ECDH; the server only ever sees opaque ciphertext. For recovery, an optional PIN-encrypted backup of the device's identity can be uploaded to the server in sealed form — the PIN never leaves the device.
Requirements: Node.js 22+, pnpm 10+, Postgres 16.
# 1. Install
pnpm install
# 2. Start Postgres (or point DATABASE_URL at your own)
docker compose up -d postgres
# 3. Copy and edit env
cp .env.example .env
# At minimum, set DEVICE_SESSION_SECRET (32+ random chars). Generate with:
# openssl rand -base64 48
# 4. Apply migrations
pnpm db:migrate
# 5. Start everything
pnpm devThe gateway listens on http://localhost:4000 by default. Verify:
curl http://localhost:4000/health # → {"status":"ok"}
curl http://localhost:4000/ready # → {"status":"ready"} once DB is upapps/
gateway/ Main HTTP API (Hono). All client-facing endpoints live here.
worker/ Background jobs: payload retention, expired-group cleanup.
dashboard/ Dev/operator tool. NOT meant for production deployment.
packages/
db/ Drizzle schema, migrations, database client.
crypto/ Ed25519, X25519, AES-GCM helpers (thin wrappers around noble).
types/ Shared TypeScript types for client/server contracts.
config/ Environment variable loading and Zod validation.
logger/ Pino logger instance shared across apps.
All configuration is via environment variables, documented in
.env.example. Required at startup:
DATABASE_URL— Postgres connection string.BETTER_AUTH_SECRET— 32+ chars, signs admin dashboard sessions.DEVICE_SESSION_SECRET— 32+ chars, signs short-lived device session tokens. Independent fromBETTER_AUTH_SECRETso they rotate separately.
Strongly recommended in production:
TRUSTED_PROXY_IPS— comma-separated reverse-proxy IPs allowed to setX-Forwarded-For. Without this, per-IP rate limits can be bypassed by header spoofing. The gateway logs a warning at boot if unset.
Optional behavior toggles:
ENABLE_AUTH_SIGN_UP— defaultfalse. Set totrueonly for first-time admin account creation, then flip back.ENABLE_DEBUG— defaultfalse. Exposes/debug/*data-inspection routes. Operators only.MAX_GROUPS_PER_DEVICE— default6. Cap on active group memberships per device public key.
The gateway and worker run as separate processes against a shared Postgres. A typical production deploy looks like:
- A reverse proxy (nginx, OpenResty, Caddy, Cloudflare Tunnel, etc.) terminates TLS and forwards to the gateway.
- The gateway runs under a process supervisor (systemd, PM2, Docker).
- The worker runs as a separate long-lived process from the same source tree, sharing the database.
TRUSTED_PROXY_IPSis set to whatever IP your reverse proxy connects from (commonly127.0.0.1,::1,::ffff:127.0.0.1for same-host).pnpm db:migrateruns on every deploy; it's idempotent.
The dashboard app (apps/dashboard) is not intended for production
deployment. It's an operator tool that hits debug-gated endpoints. Self-host
the gateway and worker; skip the dashboard.
The gateway exposes a small surface:
POST /auth/challenge— issue a fresh 32-byte challenge for the challenge-signature handshake.POST /groups,POST /groups/:id/join,DELETE /groups/:id/members/me,GET /groups/:id/members— group lifecycle.POST /groups/:id/payloadsandPOST /groups/:id/payloads/targeted— submit encrypted payloads.GET /groups/:id/pendingandGET /groups/:id/pending/stream— pull or subscribe to pending encrypted payloads addressed to your device.POST /payloads/:id/ack— acknowledge delivery; fully-acked payloads are deleted.PUT /identity/vault,POST /identity/vault/fetch,POST /identity/vault/report-failure— encrypted identity backup and recovery.GET /health,GET /ready— deploy probes.GET /stats— aggregate counts only (groups, members, payloads). No PII.
Totals-Engine is designed to be self-hosted. The official instance at
engine.totals.detached.space is one deployment; nothing in the protocol
ties the mobile app to it. To run your own:
- Clone this repository.
- Follow Quick start.
- Point your mobile app at your gateway URL.
- Tell whoever you share groups with to do the same — both ends of a shared group need to talk to the same backend.
If you want help, open a GitHub Discussion.
Bug reports, security disclosures, documentation improvements, and patches welcome. See:
- SECURITY.md for vulnerability reporting.
- The internal design documents under
docs/internal/for historical context on why things are shaped the way they are.
MIT — see LICENSE.