Torv

Security

Most financial apps say “bank-level encryption” and show a lock icon. We’d rather show you our actual database.

What our accounts table actually looks like

This is a representative sample of how your account data is stored. Amber = AES-256-GCM encrypted. Blue = HMAC-SHA256 pseudonymized. Black = plaintext.

user_idnameinstitutionbalancetype
a1b2c3d4e5f6...9f8e7d6cenc:3a7f1b:c9e2d4:8b1f3a7e...enc:7d4e2a:f1b3c5:2e9a4d7b...$47,832.19Checking
a1b2c3d4e5f6...9f8e7d6cenc:5c8d2e:a4f1b7:6d3e8c1a...enc:9b2f4a:d7e3c1:4a8f2b5e...$312,457.83401k
a1b2c3d4e5f6...9f8e7d6cenc:2f9a4d:b5c8e1:7a3f6d2b...enc:4e1b7c:f8a2d6:5c9b3e7a...$184,291.50Mortgage

What we encrypt

The fields that could tie your financial data back to you are encrypted before they ever reach the database — with the key stored separately, outside it.

  • • Your email and any other identifying field (encrypted with AAD)
  • • Which bank any account belongs to (encrypted)
  • • Account names and owner names (encrypted)
  • • Bank login credentials (handled by Finicity or MX, never by us)
  • • MFA secrets and recovery codes (encrypted; codes also one-time)

Four layers of protection

1

Decoupled identity

Every user is identified internally by a randomly-generated UUID, not by your email or any value derived from your real identity. The financial tables store that UUID; the email column on the users table is encrypted (see layer 2). Linking a UUID back to a real person requires both database access AND the encryption key — which is stored separately, outside the database.

Internal user ID: a7f3b2c1-4d5e-6f78-9abc-def012345678
Email column: enc:v2:3a7f1b9c2e4d:c9e2d4f1b3a7:8b1f3a7e2c4d9b5f...

For deterministic email lookup at sign-in we also store an HMAC-SHA256 hash of your email; the HMAC secret lives outside the database, so a database breach cannot turn the hash back into your address.

2

PII encryption

Every personally identifiable field — account names, institution names, owner names, and bank connection tokens — is encrypted with AES-256-GCM before it touches the database. Each value gets a unique random initialization vector and a tamper-proof authentication tag. Each ciphertext is also bound to its row identity (table, row id, column) via AES-GCM associated data, so an attacker with database write access cannot move an encrypted value from one row or column onto another.

Account name: Chase Sapphire Checking
Stored as: enc:3a7f1b9c2e4d:c9e2d4f1b3a7:8b1f3a7e2c4d9b5f...

AES-256-GCM is the same standard used by governments and financial institutions worldwide. The “GCM” part means any tampering with the encrypted data is detected automatically.

3

Credential isolation

Your bank login never enters our system. Connecting an account is handled by Finicity (a Mastercard company) or MX — the bank-data networks that securely link your accounts — never by a form on Torv. Torv receives only an encrypted access token and read-only data. We never see your username or password, and we cannot log into your bank or move money.

4

Database-level tenant isolation

Even if application code forgot to filter a query by user, the database itself would reject it. The application connects to Postgres as a restricted role (torv_app) with row-level security enforced on every table that holds user data. Each request sets the active user’s pseudonymized ID as session context; the database returns only rows tagged with that ID.

A second role (torv_admin) is reserved for trusted background jobs (sync, cron) where there is no user request to scope to, and is never bound to a request that originates from a browser.

Technical details

Identity hashingHMAC-SHA256 with 256-bit secret
PII encryptionAES-256-GCM with 96-bit random IV per value; ciphertext bound to (table, row id, column) via AES-GCM associated data
AuthenticationCustom auth with bcrypt-hashed passwords + JWT sessions; TOTP MFA and WebAuthn passkeys both supported
Data in transitTLS 1.3 (HTTPS everywhere)
Data at restDisk encryption at rest + application-level AES-256-GCM
Key storageEncryption keys stored in Cloudflare Workers secrets, separate from database
Bank credentialsHandled by Finicity or MX — never stored by Torv
API authorizationPostgres row-level security on every PII table (torv_app role); per-request session context binds queries to the active user’s pseudonymized ID
Key rotationEncryption keys are versioned and rotatable; the previous key is retained during transition so existing ciphertexts remain decryptable while new writes use the current key
BackupsContinuous Postgres WAL archive to encrypted off-site storage (Cloudflare R2); point-in-time recovery to any second within the retention window
Content Security PolicyStrict CSP with violation reporting; no inline scripts, no third-party origins beyond what aggregation requires
TrackingNo cookies, no third-party analytics, no ad trackers

Reporting a vulnerability

If you’ve found a security issue, please report it through our contact form. We respond to every report.