Security

Most financial apps say “bank-level encryption” and show a lock icon. Torv would rather show you its actual database.

What Torv never has

The strongest protection isn’t encrypting data — it’s never receiving it. A breach can’t leak what was never here.

Full account or routing numbers

Torv requests only the last 4 digits from its data providers, so you can tell your accounts apart — and even those are stored encrypted. Full account and routing numbers never reach Torv’s servers.

The ability to move money

Torv is read-only by design. There is no code path that initiates a transfer, payment, or trade — the capability doesn’t exist, so it can’t be stolen or abused.

SSN, date of birth, or street address

Retirement planning uses your birth year — nothing more precise. Besides the state you file taxes in, the only location Torv keeps is the approximate city of each device you’re signed in on, which you can see under Account.

A verified identity

Sign-up asks for an email and a password — no name, and nothing checked against a document or a credit file. Torv never confirms who you are.

This isn’t a policy that could drift — the exact list of fields Torv requests from its data providers is frozen by an automated test in its build. Requesting a new field fails the build until it passes a deliberate review.

What Torv does ask, and why: a first name — initials are fine — for each person in a household, so your accounts can be told apart on screen instead of reading “Person A” and “Person B.” It’s optional, Torv never asks for a last name, and it’s encrypted at rest like every other identifying field. Nothing in the planning or tax math reads it — it’s a label, not an identity.

What Torv’s 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 Torv encrypts

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 Torv)
  • • MFA secrets and recovery codes (encrypted; codes also one-time)
  • • Every word your bank sends about a transaction, and the rules and patterns Torv builds from those words (encrypted)

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 Torv also stores 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 — and every transaction’s text is encrypted with AES-256-GCM before it touches the database. Each value gets its own initialization vector and a tamper-proof authentication tag. Each ciphertext is also bound to its identity via AES-GCM associated data — identity fields to (table, row id, column), transaction text to (table, column) — so an attacker with database write access cannot move an encrypted value onto another column or table, or an identity value onto another row.

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 Torv. 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. It never sees your username or password, and it 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.

Who looks at your data

One person can open the production database: the founder. Every such session opens through one script, under a database role of its own, and is recorded — who, when, and why — so the server’s own connection log can be checked against the record. Those sessions are correctness audits over pseudonymous rows, and they are counted below, not defined away. The admin dashboard shows counts only; it cannot show a person’s accounts or transactions.

Sessions, last 30 days
152
Last opened
Oct 5
Recorded since
Sep 20

Live from the session record; refreshed within a minute. Sessions before Sep 20 were not recorded and are not counted.

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
Transaction textAES-256-GCM, the same as the identity fields. The database never reads the words; search and sort happen in the app after decryption
AuthenticationCustom auth with bcrypt-hashed passwords and opaque server-side sessions: the cookie is a random secret, only its hash is stored, and signing out deletes the row. 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
BackupsTwice-daily encrypted database dumps to off-site immutable storage (Cloudflare R2 with bucket locks — backups can’t be deleted or altered, even with the database server’s own credentials). Encryption is asymmetric: the server holds only the public key and cannot read its own backups; the private key is held offline. Restores are tested against real dumps.
Content Security PolicyStrict, nonce-based CSP with violation reporting: a script runs only if it carries the per-request nonce. The only third-party script origins are the aggregation connector and Cloudflare’s bot check on the contact form
TrackingNo third-party analytics, no ad trackers, no tracking cookies — the only cookies are the ones that keep you signed in
Log retentionIP addresses and browser user-agents in session and security logs are purged after 30 days

Reporting a vulnerability

If you’ve found a security issue, please report it through the contact form. Every report gets a reply.