Skip to content

detached-space/totals-engine

Repository files navigation

Totals-Engine

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.

What this is

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.

What this isn't

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.

Core principles

These shape every design decision:

  1. 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.
  2. Ephemeral where possible. Data is deleted as soon as it's no longer needed for delivery.
  3. Device is source of truth. The server is infrastructure, not a database of users' lives.
  4. Stateless services. Anything that can be stateless, is.
  5. 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.

How requests work, in one paragraph

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.


Quick start

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 dev

The 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 up

Repository layout

apps/
  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.

Configuration

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 from BETTER_AUTH_SECRET so they rotate separately.

Strongly recommended in production:

  • TRUSTED_PROXY_IPS — comma-separated reverse-proxy IPs allowed to set X-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 — default false. Set to true only for first-time admin account creation, then flip back.
  • ENABLE_DEBUG — default false. Exposes /debug/* data-inspection routes. Operators only.
  • MAX_GROUPS_PER_DEVICE — default 6. Cap on active group memberships per device public key.

Deploying

The gateway and worker run as separate processes against a shared Postgres. A typical production deploy looks like:

  1. A reverse proxy (nginx, OpenResty, Caddy, Cloudflare Tunnel, etc.) terminates TLS and forwards to the gateway.
  2. The gateway runs under a process supervisor (systemd, PM2, Docker).
  3. The worker runs as a separate long-lived process from the same source tree, sharing the database.
  4. TRUSTED_PROXY_IPS is set to whatever IP your reverse proxy connects from (commonly 127.0.0.1,::1,::ffff:127.0.0.1 for same-host).
  5. pnpm db:migrate runs 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.

Public API

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/payloads and POST /groups/:id/payloads/targeted — submit encrypted payloads.
  • GET /groups/:id/pending and GET /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.

Self-hosting

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:

  1. Clone this repository.
  2. Follow Quick start.
  3. Point your mobile app at your gateway URL.
  4. 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.

Contributing

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.

License

MIT — see LICENSE.

About

No description, website, or topics provided.

Resources

License

Security policy

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors

Languages