Security
Boring on purpose: few secrets, each one sealed or hashed, every access scoped and logged, and nothing that can move money.
The short version
- Truenote can't move money. It uses only Plaid's read products (transactions, balances, investments, liabilities, recurring). No transfer, payment or "auth for payments" endpoint exists in the code, and no agent tool writes to a bank.
- Your bank logins never reach Truenote. You sign in to your bank inside Plaid Link; Truenote gets a Plaid access token for reading, sealed with AES-256-GCM before it touches the database.
- Agents get only what you allow, per connection: which kinds of data (scopes), and which accounts, businesses or manual assets they must never see. Change or revoke that in one click; it applies to the agent's very next call.
- Every agent call leaves a receipt in your Activity log: which agent, what it asked, how many rows, whether it was refused.
- You can take everything out (Settings → Your data) or delete everything (Settings → Delete account), which also tells Plaid to forget every bank.
What's stored
| Data | Protection |
|---|---|
| Your email, name, sessions, passkeys | Sessions are signed cookies (httpOnly, SameSite, Secure). Passkeys are public keys; the private key never leaves your device. |
| Sign-in codes | Six digits, 10 minutes, 3 tries, stored hashed: a database read can't sign anyone in. |
| Plaid access tokens | AES-256-GCM with a random IV, the row's identity bound in (a ciphertext copied to another row won't open). Keys live in the server's environment, never the database. Opened only in memory, right before a Plaid call. Never returned by any endpoint, tool, export or log. |
| Documents (paystubs, W-2s, returns) | Sealed like Plaid tokens, the file and its text each bound to the document. Agents need a documents scope and get the text with Social Security and long account numbers masked to the last four. |
| Bank data and your context | Rows in Postgres (encrypted at rest by the host), scoped to you by the API and by row-level security. Rows an agent wrote say which agent. |
| Agent access | Every bearer secret (access and refresh tokens, personal access tokens, codes) is a random 256-bit string stored only as its SHA-256 hash. Shown once, never again. |
| The audit log | Append-only: the database refuses updates and deletes. Arguments are summarized (filters and ids), never secrets. |
Not stored: bank passwords, card numbers (Plaid gives masks like ··4821), any token in the clear.
Who can reach it
You
Sign-in is an emailed code or a passkey; there are no passwords. Codes are rate-limited per address and per IP. A session lasts 14 days from its last use. Managing agents, exports and deleting the account need your browser session: an agent's token can never do them.
Agents
An agent connects with OAuth 2.1 (PKCE, exact redirect URIs, tokens bound to the MCP or API endpoint) or a personal access token you make. Either way it's one grant with scopes and exclusions, read fresh on every call.
- Scopes: accounts, transactions, investments, liabilities, context (read and write), documents (read and write). New connections start read-only; writing asks again.
- Exclusions (accounts, businesses, manual accounts) are filtered in SQL in every read, totals included, and checked on every write. A rule an agent creates can't touch a transaction it can't see.
- What an agent can write: the spending plan, buckets (Truenote's ledger only; nothing moves at the bank), rules and Review decisions, your profile and entities, goals, income sources, notes, plans, categories, transaction categories and entities. Never: a bank, money, your sign-in, other agents' access, the audit log, exports, or your account.
- Limits: 240 calls a minute per grant, so a runaway loop stops there.
| Token | Lifetime |
|---|---|
| OAuth authorization code | 5 minutes, once (a replay revokes everything issued from it) |
| OAuth access token | 1 hour |
| OAuth refresh token | 30 days, rotated on every use; a used one coming back revokes the whole family |
| Personal access token | 7, 30, 90 or 365 days, or until revoked; flagged after 90 days unused |
| Web session | 14 days from last use |
Defense in depth
- The API is the only door. The web app never touches the database or Plaid.
- Every query is scoped in code to the signed-in user; someone else's id is the same 404 as one that doesn't exist.
- Row-level security in Postgres. Each signed-in request runs as a restricted role with the user set; every table that belongs to someone has one policy. A query that forgot its filter still gets only your rows.
- Same-user foreign keys: a row can't point at another user's row even if code tried.
- Headers and CORS: a strict Content Security Policy, no framing, HSTS. The agent endpoints answer browsers only from known origins and ignore cookies entirely.
- Logs never carry secrets. Every log line passes a scrubber that removes token shapes, secret-named fields and bank text. Error reports get no cookies, bodies or database values.
Threats, in plain words
| If… | Then… |
|---|---|
| Someone steals a database backup | They see bank data and context. They can't open Plaid tokens or documents without the keys (kept outside the database), can't use agent tokens (hashes only), can't sign in. |
| An agent misbehaves (a bad prompt, injected instructions in a transaction description) | It can read only what its grant allows and write only Truenote's own records, never money. Every call is in Activity; revoke it in one click; rows say which agent wrote them. |
| An agent token leaks | Revoke the grant. Access tokens die within an hour anyway; a stolen refresh token used after the real client is caught, and both lose access. |
| Someone guesses at sign-in | Codes are six digits with 3 tries each and tight per-address and per-IP limits. Passkeys can't be guessed. |
| A bug forgets to filter by user | Row-level security returns only your rows, and nothing at all when no user is set. |
Known gaps
- Bank text isn't filtered by exclusions: a transaction on an account an agent can see may name a hidden one ("TRANSFER TO SAV ...7730"). Free text you write isn't filtered either. The consent screen says so.
- Background jobs run outside row-level security; they're written and tested to act per user, but the database won't stop a bug there.
- Backups keep deleted data until they age out (6 to 89 days).
- One person per account. A partner signing in shares the account; household sharing with separate grants is planned.
- No independent security review has been done yet. One is planned before public sign-up grows.
Deleting your account
Settings → Delete account: every bank is removed at Plaid (so Plaid stops sharing), every agent's access is revoked, the audit log is purged, and everything you own is deleted, sessions and passkeys included. Backups keep their copies until they expire.
Found something? Write to security@usetruenote.com.