Security
Most financial apps say “bank-level encryption” and show a lock icon. We’d rather show you our actual database.
What we never have
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
We request only the last 4 digits from our data providers, so you can tell your accounts apart — and even those are stored encrypted. Full account and routing numbers never reach our 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.
Your name
Sign-up asks for an email and a password. There is no name field anywhere in Torv.
SSN, date of birth, or street address
Retirement planning uses your birth year — nothing more precise. The only location we store is which state you file taxes in.
This isn’t a policy that could drift — the exact list of fields we request from our data providers is frozen by an automated test in our build. Requesting a new field fails the build until it passes a deliberate review.
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_id | name | institution | balance | type |
|---|---|---|---|---|
| a1b2c3d4e5f6...9f8e7d6c | enc:3a7f1b:c9e2d4:8b1f3a7e... | enc:7d4e2a:f1b3c5:2e9a4d7b... | $47,832.19 | Checking |
| a1b2c3d4e5f6...9f8e7d6c | enc:5c8d2e:a4f1b7:6d3e8c1a... | enc:9b2f4a:d7e3c1:4a8f2b5e... | $312,457.83 | 401k |
| a1b2c3d4e5f6...9f8e7d6c | enc:2f9a4d:b5c8e1:7a3f6d2b... | enc:4e1b7c:f8a2d6:5c9b3e7a... | $184,291.50 | Mortgage |
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
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.
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.
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.
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.
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.
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 hashing | HMAC-SHA256 with 256-bit secret |
| PII encryption | AES-256-GCM with 96-bit random IV per value; ciphertext bound to (table, row id, column) via AES-GCM associated data |
| Authentication | Custom auth with bcrypt-hashed passwords + JWT sessions; TOTP MFA and WebAuthn passkeys both supported |
| Data in transit | TLS 1.3 (HTTPS everywhere) |
| Data at rest | Disk encryption at rest + application-level AES-256-GCM |
| Key storage | Encryption keys stored in Cloudflare Workers secrets, separate from database |
| Bank credentials | Handled by Finicity or MX — never stored by Torv |
| API authorization | Postgres row-level security on every PII table (torv_app role); per-request session context binds queries to the active user’s pseudonymized ID |
| Key rotation | Encryption keys are versioned and rotatable; the previous key is retained during transition so existing ciphertexts remain decryptable while new writes use the current key |
| Backups | Twice-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 Policy | Strict CSP with violation reporting; no inline scripts, no third-party origins beyond what aggregation requires |
| Tracking | No third-party analytics, no ad trackers, no tracking cookies — the only cookies are the ones that keep you signed in |
| Log retention | IP 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 our contact form. We respond to every report.