What nobill stores, sentence by sentence
This page is written from the code, and each sentence names the file it comes from. If the code and this page ever disagree, the code is wrong or this page is, and either way it gets fixed. Last checked against the code on 2026-09-18.
The permission we ask for
nobill asks Google for exactly one Gmail permission,
gmail.readonly, plus
openid and
email so it can tell your account
apart from another one.
worker/src/google.ts, SCOPES
It cannot send, delete, label or modify mail with that permission, and the code contains no call that would. worker/src/google.ts, the only Gmail calls are users.messages.list and users.messages.get
What it reads
An import asks Gmail for messages matching a receipt search (words like receipt, invoice, trial, renewal) from the last 180 days, in pages of 100. worker/src/google.ts, IMPORT_QUERY and listMessageIds
For each message it fetches the From, Subject and Date headers and the short preview snippet Gmail returns in its metadata format. It never requests the full message, so no body, no HTML and no attachment ever reach the Worker. worker/src/google.ts, getHeaders, format=metadata with three metadataHeaders
The snippet is cut at 200 characters before anything else happens. worker/src/google.ts, getHeaders
What it keeps
The rules run on those three headers and the snippet in memory and produce one row per merchant with these fields: merchant key and name, sender domain, amount and currency, cadence, trial end date, billing day, last charge date, next charge date, charge count, card last four digits when a receipt shows them, the Gmail message ids as opaque handles, and the cancel guide key. worker/migrations/0001_init.sql, table subscriptions, every column carries its derivation
Your account is stored as a hash of your Google id and a hash of your email address. The email address itself is not stored. worker/migrations/0001_init.sql, table users; worker/src/index.ts callback handler
If you type an address for reminders on your list page, that address is stored so reminders can go there. It is the only email address nobill keeps in clear text. worker/migrations/0001_init.sql, table reminder_prefs
The refresh token Google issues is encrypted with a key held as a Worker secret before it is stored, and the key never appears in the code or the repository. worker/src/crypto.ts encrypt; worker/src/google.ts storeRefreshToken
What it never stores
No message body, no HTML, no attachment, no raw header text, no snippet. The database has no column for any of them, and a test asserts that every table lacks a column named body, html, raw, text, snippet, payload or message. scripts/e2e-oauth.sh, the schema proof step against GET /admin/schema
When a mail cannot be labelled by the rules and the model pass is switched on, the model sees the sender domain, the subject with every digit masked, and the 200 character snippet, and that redacted line plus the model's answer are kept for audit. The model pass is off by default. worker/src/classify.ts redactForModel; worker/wrangler.jsonc AI_PASS off
Deleting everything
One call, the Revoke button on your list page, deletes the stored token, tells Google to revoke the grant, and deletes every row that belongs to you: subscriptions, reminders, reminder address and audit lines. worker/src/index.ts, POST /revoke; worker/src/google.ts revoke; worker/src/db.ts deleteUserRows
You can also remove nobill from your Google account's third-party access page at any time; the next import would then fail and nothing new would be read.
Everything else on this site
The waitlist form stores the address you type and a confirmation token, and confirms only when you click the link in the confirmation email. worker/src/index.ts, POST /waitlist and GET /waitlist/confirm
The site counts visits with a beacon that records the path only, never a query string, and classifies each hit as human or bot by user agent and by whether the page saw a scroll or a click. Both numbers are kept separately so the human count is never inflated. worker/src/hits.ts; worker/src/index.ts, POST /hit and GET /stats
The landing page loads Google Tag Manager. That is the one third-party script on the site. landing-static/index.html, head
Checkout runs in Stripe test mode until launch; no live card is charged and no card number touches nobill. worker/src/index.ts, POST /checkout requires an sk_test_ key
Where it runs, and how long it is kept
Everything above runs on Cloudflare Workers with D1, KV, Queues and R2 in Cloudflare's network. Retention windows are on the data retention page.
Questions: howard@nobill.app, answered within two working days.