Data Policy

Last updated: October 12, 2026

Zedi is a personal-finance tool. You hand it bank statements and receipts, and it gives you back a real picture of where your money went. This page explains what we keep, where it sits, who can read it, and how to delete it. In plain English, grounded in the code.

Questions or requests: zedi@sneeks.ai.

1. What we store

  • Transactions, categories, accounts, and rules in our Postgres database. This is the data the dashboard runs on: dates, amounts, merchant names, descriptions, the categories you assign, and the auto-categorization rules you build by correcting us.
  • Receipt photos and PDF statements, transiently, in Cloudflare R2. A receipt image is deleted from R2 the moment OCR succeeds: the parsed line items become the record, not the photo. If OCR fails, the image is kept so you can hit Reprocess or delete the receipt yourself. PDF statements follow the same pattern: the file lives in R2 only until you save the import, discard it or delete the statement, and is deleted then. A saved import keeps the transactions read from the statement, not the PDF.
  • Your profile picture, if you upload one, in R2 under {user_id}/avatars/, plus the avatar style, emoji and colour you pick.
  • Feedback and support messages you send from the app: the message, a star rating for reviews, the page you were on, your browser's user agent, and an optional screenshot, stored in R2 under {user_id}/feedback/. A copy goes to the operator's private Discord channel (section 5).
  • People you add for splits and repayments: name, nickname, colour, emoji, payment handles (a Venmo username or the name on a Venmo or Zelle payment), relationship and notes, and the balances, obligations and settlements tied to them. They are entries in your own ledger: they get no Zedi account and Zedi never contacts them itself.
  • Where you heard about Zedi: the answer you pick at signup (and any detail you type, up to 200 characters), and a referral tag: the ?ref= tag of the link you arrived on, if it had one; otherwise, if you visited one of our /for pages, the first such page's own tag (for example for-itemized-grocery-tracking). The tag waits in your browser for up to 7 days before you sign up, and is copied to Stripe if you upgrade to Pro.
  • Your plan, if you upgrade: plan and status, the date you first went Pro, and the Stripe customer and subscription ids that link your account to Stripe. Card details go to Stripe and never reach Zedi.
  • Your Anthropic API key, encrypted, if you choose to bring one in Settings. Stored as a ciphertext blob in user_settings.anthropic_api_key_encrypted. When unset, Zedi's shared key is used and the monthly OCR allowance applies.
  • Your SimpleFIN connection, encrypted, if you connect a bank. SimpleFIN issues Zedi a read-only access URL when you paste a setup token; it is not your bank password, which Zedi never receives. It is stored as a ciphertext blob in bank_connections.access_url_encrypted, encrypted with AES-256-GCM by the same cipher as the BYOK key, next to the last sync time, its status, any errors the bridge reported and a count of today's reads.
  • The bank account list SimpleFIN reports: institution, account name, currency, balances, balance date, and holdings where your institution provides them, plus the Zedi account you mapped each one to. Refreshed on every sync. Synced transactions are stored like imported ones, and each also keeps the bank's transaction id, payee, memo and merchant category code (MCC) in import_original.bank and external_txn_id.
  • AI categorization runs, if you turn AI categorization on: when each run happened, how many rows it covered, and the category and confidence Claude suggested for each merchant, so the uncategorized queue can show them. Plus a monthly count of rows used against the free allowance.
  • A transaction audit log for the Memory page. Every edit, import, auto-categorizer touch, and delete on a transaction writes a row noting what changed, when, and which actor (you, an importer, the auto-categorizer) made the change. This is yours: it shows you your own history.
  • Your email preference: whether you get product update emails and, if you turned them off, when and how.
  • The operator's CRM records, seen only in the admin console (section 4): a contact row the operator may keep for you (name, email, company, a pipeline stage, the next step, tags and notes) and dated notes the operator logs on it or on your account, such as a support conversation.
  • Better Auth session data. Your email, name, optional handle, and the session rows that keep you logged in. If you signed up with email and password, we also store a one-way hash of that password (we never see the plaintext). If you signed up with Google or Apple, no password is stored; your provider-issued user ID lives in the linked-account row instead.

2. Where it lives

SaaS at zedi.sneeks.ai. Postgres is a Railway managed instance in a US region. Receipt and PDF blobs live in a Cloudflare R2 bucket named zedi-saas, with object keys prefixed by your user ID ({user_id}/receipts/..., {user_id}/statements/..., {user_id}/avatars/..., {user_id}/feedback/...).

Personal deployment. The same code also runs as a single-tenant box (Nicolo's personal instance on a Proxmox VM). That deployment holds exactly one user's data. If you signed up at zedi.sneeks.ai, your data is on the SaaS side, not on the personal box.

3. How long we keep it

  • Receipt images: deleted from R2 the moment OCR succeeds, in the same database write that records the parsed line items. We do not retain user receipt images longer than the OCR call itself.
  • Failed-OCR receipt images: held in R2 until you act on them, for at most 30 days. Either retry the OCR, in which case a successful parse triggers the same delete, or delete the receipt entirely, in which case the image is removed too. An image still sitting on a failed receipt after 30 days is deleted automatically.
  • PDF statements: held in R2 only while the import waits for you, from upload until you save or discard it or delete the statement, then deleted. A preview nobody saves is deleted after 7 days, and a failed read keeps its file for 14 days so Retry works without a new upload. A saved statement keeps its transactions, never the file.
  • Profile picture: until you replace it (the old one is deleted), remove it, or close your account.
  • Feedback and screenshots: kept with your account until you close it. The copy posted to Discord stays in the operator's channel until the operator deletes it; email us and we will.
  • Anthropic-side retention: Anthropic deletes API inputs within 30 days and contractually does not train on them (Anthropic Commercial Terms, Section B). That covers receipt photos and PDF statements during the OCR call, and the merchant lines an AI categorization run sends.
  • Bank connections: the encrypted access URL and the account list persist until you disconnect or close your account. Disconnecting deletes Zedi's copy of the credential and the account list, but it does not revoke access at SimpleFIN Bridge: remove the Zedi app in Bridge too. Transactions already synced stay in your ledger, like imported ones, until you delete them.
  • After you close your account: one deletion record stays in Zedi's database: your user ID, your plan at the time, what happened to a Pro subscription, and how many rows were removed from each table. It holds no email and no name, and exists so a billing question after the fact can be answered. Until your files are gone from R2, a purge-queue entry (your user ID, the storage prefix and its retry status) stays too, and is removed once the purge succeeds. Copies of feedback already posted to Discord follow the feedback line above.
  • Everything else: kept until you delete it. Transactions, categories, accounts, people, audit log entries, your encrypted BYOK key, your account itself. No silent purges beyond the windows above. If you want it gone, see section 6.

4. Who can read it

You. Postgres Row-Level Security isolates every user from every other user. The requests you make run as a Postgres role (app_user) that cannot bypass RLS, even on a bug. Every read and every write in them is scoped to the policy user_id = current_setting('app.user_id'), set at the start of each request from your verified session. A handler that forgets to filter by user gets blocked at the database layer.

The paths that run without RLS. A few server paths act for no single signed-in user, or must reach tables a user cannot, so they use a separate database role that bypasses RLS. Each of their writes to one user's data is keyed on that user's ID or on an id only that user's rows hold (the Stripe customer id, unique per account; a feedback message's id, when the operator marks it resolved); the only writes that span users are the background jobs' bulk updates that mark stuck scans and interrupted AI categorization runs as failed and delete expired previews and failed statements, plus the daily Discord role sync recording which linked accounts it checked. The paths: the Stripe webhook that records plan changes; the background jobs that time out stuck scans, expire old previews and failed uploads, and retry storage deletes; the startup step that fails AI categorization runs a restart interrupted; the bank sync scheduler that finds connections due for a sync; the daily Discord role sync that finds linked accounts due for a role check; the account deletion, which also removes your sign-in records; two lookups on the sign-in table (your email, for the admin check, and your account's age, for signup attribution); and the admin console, open only to the operator's allowlisted email, described below.

R2 keys are user-prefixed. Every receipt image and PDF blob is stored under {user_id}/... in the bucket. The storage layer also verifies the prefix matches the requester on read and delete, so a forged database row would still not reach another user's bytes.

BYOK keys are encrypted at rest. If you store your own Anthropic API key in Settings, it is encrypted with AES-256-GCM before it touches the database. A fresh random nonce is used per encryption. The 32-byte master key lives outside the database, in the backend's environment. The SimpleFIN access URL for a bank connection is encrypted the same way.

The operator. Nicolo, who runs the service, has administrative access to the production database and the R2 bucket. That access exists for operations: migrations, incident response, support requests you initiate. It is not used to browse user data.

The admin console. To run support and the product, the operator has an admin console, open only to allowlisted operator emails. Its user lists show account-level information only: your email and name, whether the email is verified, when you signed up and how you heard about Zedi (the answer you picked and any referral tag), your plan and when you went Pro, when you were last active, whether you connected a bank, whether you get product update emails, whether you saved your own Anthropic key (never the key itself), whether the account holds sample data, this month's receipt and PDF scan counts, and counts of your accounts, statements, imports, transactions and receipts. Each account also has a dated timeline: imports with how many rows they added, statements uploaded, receipts scanned per day, the feedback you send, and the operator's own notes about you. A sessions page lists, by email, when you made changes in Zedi: each session's start and end time, its length, how many changes it held and where they came from (an edit, an import, a rule), rebuilt from the transaction audit log. The user lists, the timeline and the sessions page never show transaction contents: no amounts, merchants or descriptions, no statement file names, no receipt contents. The console's change log page is the one exception: for every change to a transaction's amount, store, description, date, notes, category, account, or pending or hidden flag, made by you or by Zedi (an import, a rule, transfer pairing or AI categorization), it shows the value before and after, for debugging imports and categorization.

5. Third parties

A short list of services see specific slices of your data so Zedi can function. The full list:

Anthropic (Claude Vision and AI categorization)

Receipt photos and PDF statements are sent to Anthropic's Vision API for OCR. Per Anthropic's Commercial Terms, Section B, Anthropic may not train models on API inputs and deletes them within 30 days. CSV and QFX files are parsed locally in our Rust backend and are never sent to Anthropic as files; the only part of them that can reach Anthropic is what AI categorization sends, below, and only if you turn it on.

Model disclosure. Both calls use the Claude model claude-sonnet-5. A receipt call sends the receipt image and a fixed extraction prompt. A PDF call sends the statement document and a fixed extraction prompt. Nothing else goes with them: no account identifier, no email address, no other transactions, no history from earlier calls. If you bring your own key, the same request goes out under your key instead of ours.

AI categorization (opt-in). Off until you turn it on in Settings > Categories or the uncategorized queue. When you ask Claude to categorize, Zedi sends the model claude-haiku-5-5 your category names (and which are income or transfers), up to 40 merchants you already filed, each with the category it is filed under (never rows Claude filed), and one line per merchant group: the merchant name, one transaction description with every run of four or more digits masked, the amount rounded to whole dollars, whether money went in or out, how many rows the group has, and the account type. It never sends account names or numbers, balances, notes, your email, or any payment to or from a person: a transaction tagged with a person, one on a Venmo, Cash App or Zelle account, or one whose description names Venmo, Cash App or Zelle. With your own key the request goes out under your key; otherwise Free covers 50 rows a month.

Cloudflare

Sits in front of zedi.sneeks.ai as its proxy and CDN: every request, signed-in app traffic included, reaches Cloudflare's network first, where the HTTPS connection ends, and Cloudflare relays it to Railway. So your transactions, categories and uploads pass through Cloudflare in transit. It also serves DNS for the sneeks.ai domain and hosts the R2 bucket that holds receipt photos, PDF statements, profile pictures and feedback screenshots, for the windows in section 3. Cloudflare Web Analytics is limited to the public marketing pages, never signed-in pages, and sets no cookies: it records page views and load-speed metrics (page path, referrer, browser, device type, country). See the Cloudflare Privacy Policy.

Railway

Hosts the SaaS application (backend and frontend) and the Postgres database for zedi.sneeks.ai. Your transactions, categories, accounts, rules, audit log, and account record live in a Railway-managed Postgres instance in a US region.

Stripe (Pro payments)

Only if you upgrade to Pro. Checkout and the billing portal are Stripe pages. Zedi sends Stripe your email address, your Zedi user ID and your signup referral tag, if any; you enter your card on Stripe's page, and Zedi never receives the card number. Stripe sends back the customer and subscription ids and the subscription's status, which is how Zedi knows you are on Pro. When you delete your account, a live subscription is set to cancel at the end of the paid period; Stripe keeps its own customer and payment records under its policy. See the Stripe Privacy Policy.

Resend (email)

Sends Zedi's account email: the email-verification and password-reset messages. For those, Resend receives your email address, your name (for the greeting) and the one-hour link in the message. Product update emails may come later, also sent through Resend; they are switched off until Zedi has a business postal address to print in them. If they start, Resend holds a contact for you (your email address and name), every one of those emails carries an unsubscribe link, and Zedi still sends Resend no transaction data. Closing your account asks Resend to delete that contact (section 6). See the Resend Privacy Policy.

Discord (feedback and support inbox)

When you send feedback, a bug report or a support request from inside Zedi, a copy is posted to the operator's private Discord channel so it gets read: your name and email, the message, a star rating for reviews, the page you were on, your browser's user agent for bug reports, and the screenshot if you attached one. Nothing else from your account goes to Discord through this inbox. Joining the Zedi Discord community is optional and happens on Discord, under Discord's privacy policy, and members should never post account numbers, statements or balances there. If you link your Discord account to get the Pro role, Zedi stores your Discord user id and username to add or remove that role, and you can unlink it anytime in Settings. See the Discord Privacy Policy.

SimpleFIN Bridge (optional bank sync)

Only if you connect a bank. You subscribe to SimpleFIN Bridge yourself and SimpleFIN bills you directly, $1.50 a month; Zedi takes no cut and sees no payment details. You link your bank inside Bridge and paste a setup token into Zedi, which exchanges it once for a read-only access URL. On each sync Zedi sends SimpleFIN that access URL's credentials and the date range it wants, nothing else: no email, no name, no other data from your Zedi account. What comes back is your account list (institution, account name, currency, balances, holdings where available) and up to 90 days of transactions (date, amount, description, payee, memo, MCC). Zedi reads at most 24 times a day per connection. See SimpleFIN's privacy policy and security page.

MX (SimpleFIN's data aggregator)

SimpleFIN Bridge uses MX Technologies to connect to banks. When you link a bank in Bridge, the bank login goes to MX, not to SimpleFIN and never to Zedi; MX fetches your balances and transactions and hands them to SimpleFIN, which passes them to Zedi on a sync. Zedi has no account with MX and sends MX nothing directly. See the MX Privacy Statement.

Google Identity (optional sign-in)

If you choose “Continue with Google” on the login or signup page, Google handles the authentication handshake and returns your email, name, and a stable user ID to Zedi. Google does not see your transactions, receipts, categories, or anything else you do inside Zedi. You can skip this and use email + password instead, in which case nothing is sent to Google.

Apple Sign in with Apple (optional sign-in)

If you choose “Continue with Apple,” Apple handles the authentication handshake and returns either your email or a private-relay alias (your choice via Apple's “Hide my email”) plus a stable user ID. Apple does not see your transactions, receipts, categories, or anything else you do inside Zedi. You can skip this and use email + password instead, in which case nothing is sent to Apple.

Better Auth (library)

Open-source library that handles signup, login, and session management inside the Next.js frontend. Better Auth is not a third-party service: it runs as code inside our own backend, and its tables (users, sessions, accounts, verifications) live in our own Postgres database. No user data is sent to a Better Auth vendor, because there isn't one.

No data brokers. No advertisers. No analytics on signed-in pages, and none on the marketing pages beyond the Cloudflare Web Analytics above: no Google Analytics, no Mixpanel, no PostHog, no Amplitude, no session replay. If we ever add another sub-processor, this list updates first.

6. Exporting and deleting your data

Both are self-serve in Settings > Privacy & data, on every plan:

  • Export my data downloads a zip: one JSON file per kind of record (accounts, transactions, categories, rules, receipts, people, statements, change history, settings and the rest), a transactions.csv that opens in Sheets or Excel, and a manifest with the row count of each file. Uploaded files (statement PDFs, receipt photos whose scan failed, your profile picture, feedback screenshots) are not in the zip yet, only their storage keys; email us for the files themselves. Your saved Anthropic key, any bank access token and your Stripe ids are never exported. One export every 10 minutes.
  • Delete account asks you to type your account email, then hard-deletes, right away and with no undo: your user record, sign-in methods and sessions, every transaction, receipt, category, rule, person, statement, bank connection, audit log entry and setting, the CRM contact linked to your account and every note logged on it or on your account (a separate contact that only shares your email stays until we delete it; ask and we will), and every file under your prefix in R2 (receipts, statements, profile picture, feedback screenshots). If that storage purge fails, a background job retries it every few minutes, up to ten times (about an hour); a purge that still fails is flagged for the operator to finish by hand. Right after the deletion, Zedi also asks Resend, our email provider, to delete your contact there, if there is one. A Pro subscription is set to cancel at the end of the period you paid for; if Stripe cannot be reached, nothing is deleted and you can try again. Section 3 lists what stays afterwards.

Smaller deletions live where the data does: transactions on the Transactions page, statements (and the transactions they brought in) on the Import page, accounts on the Accounts page. Privacy & data links to each.

Bank sync you can stop yourself: disconnect it under Settings > Connections & keys, which deletes the stored access URL and account list right away, then remove the Zedi app in SimpleFIN Bridge so the access URL stops working there too. Synced transactions stay until you delete them or close your account.

Email still works: if an export is too large to download, you want the uploaded files themselves, or anything fails, write to zedi@sneeks.ai and we will send the export or close the account for you within a day.

7. Changes to this policy

If we change something material (adding a sub-processor, changing how a class of data is handled, broadening retention), this page updates first, with a dated changelog line below, before the change takes effect. Cosmetic edits (typos, clarifications) only move the Last-updated date.

  • October 12, 2026. Statement PDFs are no longer kept once you save an import (sections 1 and 3). Section 1 adds AI categorization runs, if you turn AI categorization on, your product update email preference and the operator's CRM records; section 4 describes the admin console and lists the paths that run without RLS; section 5 adds what AI categorization sends to Anthropic and Resend's role for product update emails, switched off for now. Deleting your account also removes the linked CRM contact and asks Resend to delete yours (section 6).
  • October 11, 2026. Added Stripe, Resend and Discord to section 5. Cloudflare now proxies all traffic, signed-in app included, not just DNS and file storage, and section 5 discloses its Web Analytics, limited to the public marketing pages. Export and account deletion are now self-serve (section 6). Section 1 now lists profile pictures, feedback, people, signup attribution and plan details; section 3 gives the real windows for statement PDFs, failed receipt images, profile pictures and feedback, and the deletion record; section 4 names the paths that run without RLS. Section 5 now covers the optional Discord account link for the Pro role. Added SimpleFIN Bridge and MX for optional bank sync on Pro.

Contact

Anything in this policy unclear, anything you want changed, anything you want a copy of, anything you want deleted: zedi@sneeks.ai. You'll hear back within a day.

See also: Terms of Service.