Security
This is our threat model, in plain words: what we hold, who might want it, what stands in their way, and where the limits are. It names mechanisms rather than reaching for “bank-level”, because a claim you can check is worth more than one you cannot. Where something is not finished, it says so.
The short version
Your partner sees exactly what you chose to share, and nothing you did not. Our servers enforce that, not the app, and a suite of tests tries to break it on every change we make.
Merchant names, account names, notes, comments and payment handles are encrypted with a key that belongs to your household. Amounts, dates and categories are not, so the app can add them up.
We can read what the app can read. This is server-side processing, not end-to-end encryption, and we would rather say so than imply otherwise.
What we hold, and how it is stored
Everything the service keeps about a household, and the form it is kept in. “Encrypted” means encrypted with that household's own key before it is written to the database; “plain” means stored as ordinary values.
| Data | Why we hold it | How it is stored |
|---|---|---|
| Your Apple sign-in identifier and display name | Identity; the name your partner sees on an expense | Plain |
| Transactions: merchant, descriptor, memo | Classification; the record itself | Encrypted |
| Amounts, dates, categories, account types, privacy tiers | Budgets and the ledger; the database has to add them up | Plain numbers |
| Account names and last four digits | The connection list and the privacy sheet | Encrypted |
| Balances | Safe-to-spend; the Balance-only tier | Plain numbers |
| Bank access tokens from Plaid | Pulling transactions | Encrypted separately, in our key service, never under a household key |
| The ledger: who paid, how it was split | Who is ahead | Plain numbers and identifiers; it holds no text at all |
| Budgets and which expense went in which bucket | Plans | Plain labels |
| Notes, comments, and Venmo or Cash App handles | What you typed for yourself or your partner | Encrypted |
| App Store transaction records | Whether the household has a subscription | Plain |
| Push tokens and App Attest key identifiers | Background sync; proving requests come from a genuine copy of the app | Plain identifiers |
| Settlement pages you chose to share | The page itself | Encrypted; the link's address is stored hashed |
| The change feed each device downloads | Keeping your devices in step | Encrypted per household |
| The audit log | Sign-ins, tier changes, invites, exports, deletions, links created and revoked | Identifiers and outcomes only |
Not held, anywhere: your bank credentials (Plaid handles them in its own interface; we receive an access token), card numbers (Apple), passwords (none exist), your email address (Sign in with Apple's relay address is not kept), your location, your contacts, or anything else from your phone.
How the privacy firewall works
The person with the most motive and the most context to look at your finances is your partner. That is the product's core threat, and the design starts there.
- One chokepoint, on the server. Every read passes through a single visibility policy. What your partner receives is built from the fields they are allowed to see. It is never “everything minus the private parts”, and it is never filtered on their device.
- Totals only ever add up shared rows. No figure your partner sees is computed from anything they cannot see, so a total can never leak what a list hides.
- Changing a tier is an event, not a setting. When you narrow an account, your partner's device receives instructions to delete everything it may no longer show. When you widen one, it receives the rows it may now see. Manual expenses follow the same rule.
- Preview is reality. “Preview as partner” renders exactly what your partner would receive, through the same code path. A test asserts the two are identical byte for byte.
- The ledger holds no text. For a shared expense on an account your partner cannot see, they get “a purchase by Josh, $32.00”. Never the merchant.
- Personal budgets never leave your lane. Your partner cannot learn that a personal bucket exists.
- A settlement page has no room for secrets. It contains only the fields you ticked; the page has no field for an account, a merchant, or a balance.
- Leaving is clean. When someone leaves a household, their contributions narrow to nothing for the person who stayed, the household's encryption key is replaced, and the leaver's sessions are cut at the gateway.
A red-team suite runs in our build pipeline on every change and will for as long as the product exists. It tries to fetch a hidden account, leak a private amount through a subtraction, reach across households, defeat the delete instructions after a tier change, find residue after someone leaves or deletes, and carry a private row out through a partner's export. If any of those succeed, the build fails and nothing ships.
Who we defend against, and how
Someone with your phone
Your sign-in tokens live in the iOS keychain, available only after the phone has been unlocked once since restart and only on that device. The app's local database is a rebuildable cache under iOS file protection; it is excluded from iCloud and Finder backups, so your financial rows never travel through Apple's backup pipeline, and a restored phone simply downloads them again. You can require Face ID to open the app. The app blanks itself in the app switcher and whenever the screen is recorded, mirrored or shared to another display. Widgets carry derived numbers only, and the lock-screen widget says “Looking good” unless you opt into amounts. Signing out on any device, or asking us to, revokes the session on the server, not just on the phone.
Someone on the network, or holding your credentials
- Sign in with Apple only, so there is no password to phish or reuse.
- Access tokens last fifteen minutes. Refresh tokens are opaque, stored hashed, and replaced on every use; if an old one is ever replayed, every token in that family is revoked at once.
- Revocation is immediate at the gateway. Leaving, deleting, or an incident response signs a device out now, not when a token happens to expire.
- On the sensitive routes (sign-in, true-up, tier changes, invites, leaving, deletion, exports, subscriptions, settlement links) the app must present an App Attest assertion: a single-use challenge from our server, signed by a key that Apple has certified lives inside a genuine copy of the app on a real device, with a counter that detects replay.
- Rate limits on sign-in, attestation, invites and settlement pages, budgeted separately so a shared network cannot lock its own neighbours out. Guessing fails with delay, not detail: an unknown, expired and revoked code all read the same.
- Invite codes are ten characters, single-use, stored hashed, and expire after 48 hours, with one outstanding per household. Settlement links are 256-bit tokens, stored hashed, valid for 30 days, and revocable instantly because the link lives in our database rather than being a signed address we could not take back.
- TLS 1.3 at the edge with HSTS. Release builds of the app pin our server's public key, with a backup key and a rehearsed rotation, so a fraudulent certificate does not get a connection.
A stolen database or backup
Every household has its own AES-256-GCM key. That key is itself wrapped by a per-household key in a separate key service, so a copy of the database contains ciphertext and no way to open it. Each encrypted value is bound to its own household, table, column and row: move a ciphertext to another row or another household and it fails to decrypt. The change feed is encrypted the same way. Backups are encrypted on our servers before they leave, the company storing them cannot read them, a restore is rehearsed every week, and copies age out within thirty days.
What an attacker holding the database alone would see: amounts, dates, categories, account types, and who paid; not merchants, account names, memos, comments, or who you are beyond an Apple identifier and a first name.
A compromised server
This is the honest limit. The running application decrypts what it needs to serve you, so a full compromise of the running box, its memory and its key-service credentials together, exposes the data of households active during the compromise. What contains that: the keys live in the key service and its unseal material is never on the box; every query the application makes is scoped to one member's household by construction, and a second wall in the database itself refuses rows from any other household even if a bug asks; an outbound firewall allows the server to talk only to Plaid, Apple and its own package mirrors, so a compromised process has nowhere to send anything; and the first lever in our incident runbook is a kill switch that stops the service within seconds while we assess and rotate.
A modified copy of the app
The server trusts nothing computed on a device. The ledger, the settlement page, the subscription check and the privacy policy all run server-side; the app's own arithmetic is held equal to the server's by shared test fixtures both run in our pipeline. Requests are bound to a strict list of accepted fields, every command carries an idempotency key so a replay does nothing twice, and the sensitive routes require App Attest as above.
Plaid and Apple
Plaid's interface handles your bank credentials; we hold one read-only access token per institution, encrypted in the key service. We remove that token, and with it our access, when you disconnect an institution, when a subscription has been lapsed for 30 days, when you leave a household, and when you delete your account. Messages from Plaid and Apple are verified against their signatures before we parse them, and a replayed message applies once.
Us
We can read what the application can read. We do not, except to help you at your request, and the audit log records every export, deletion, tier change and link operation. Our application logs pass through a filter that permits identifiers and outcomes only; merchant names, balances, tokens and payloads have no way into them. There are no third-party analytics or advertising SDKs, and no data brokers. Your export is exactly what your device receives. Deletion is real: a solo household is erased from every table, and a test asserts zero rows remain; when one member of a couple deletes, their name and every string they typed are erased while the shared amounts stay with the partner. Backups age out within thirty days.
What we can see, plainly
Running normally, our servers can decrypt your merchants and account names in order to send them to your devices. That is what server-side processing means. We chose it over a design in which the server could see nothing because the settlement ledger, budgets and preview-as-partner need a server that can compute over your rows, and because a very small company can run one well-hardened service honestly. We tell you the consequence instead of hiding it behind a phrase.
Trade-offs, stated
- Amounts, dates and categories are stored as plain numbers so the database can aggregate them.
- The cache on your phone is protected until the first unlock after a restart, not whenever the phone is locked, because background sync needs to read it. It is not separately encrypted beyond that; it is a disposable copy, and if that ever changes we will say so here.
- Server-side processing, not end-to-end encryption, as above.
- One operator and, during the beta, one server with its key service alongside it. Production at launch runs on managed infrastructure with a hardware-backed key service.
- Backups are nightly, so up to a day of changes could be lost in a disaster; your devices hold their own copy and would resend anything missing.
What is not finished
- App Attest enforcement is switched on for the beta after the first real device verifies. Until then the server checks assertions and records them but does not yet refuse a request without one.
- Automatic re-wrapping of household keys after a key rotation. Today a household is re-keyed when someone leaves or deletes; a sweep that re-wraps every household after a rotation is planned.
- Database credentials issued per deployment from the key service, instead of a long-lived password, are planned for the beta.
- Subscription overlap (“you are both paying for Spotify”) is planned and not built. It is the one computation in the product that would read both partners' private rows at once, so we describe the rules now: both partners opt in or it does not run, only the merchants on both lists are disclosed and never either list itself, opting out stops future runs but cannot unlearn a result already shown, and every run is logged with a count and never the names.
How we check ourselves
Every change runs the full test suite, including the red-team probes above, a dependency vulnerability scan, and a scan of the server image before it can deploy. An architecture test fails the build if any database access exists that is not scoped to a member. The same JSON fixtures are run by the app and the server, so their arithmetic cannot drift apart. A restore from backup is rehearsed every week. We do not hold a SOC 2 report or an external penetration test and do not claim to; this page and the tests behind it are the claim.
Reporting a security issue
Email hello@isomoira.com with “Security” in the subject. We acknowledge within 72 hours, keep you informed while we fix it, and credit you if you would like. We will not pursue good-faith research conducted against your own household's data. Please do not include anyone else's data in a report.
Contact
Questions about any of this: hello@isomoira.com. What we collect and your rights over it are at isomoira.com/privacy; the agreement that governs the service is at isomoira.com/terms; help with the app is at isomoira.com/support.