Privacy & data
A technical map of every byte. What's stored where, what gets sent to whom, and which paths are opt-in. No marketing voice.
This page is the long version of the marketing privacy page — the same facts, but written for someone who wants the field-level detail before installing. If you've read the source, none of this should surprise you; if you haven't, this is what you'd find there.
Two principles drive every choice below:
- Data lives on the device. The default storage for anything butter knows about your dashboard is your browser. Every sync layer that moves it elsewhere is either Chrome's own (which we don't operate) or opt-in (Pro cloud sync).
- Some things reach off-device by design. We used to say the AI Inbox was the only one; that's no longer true, so here's the full list. The AI Inbox and AI Meeting Prep each send a small slice of your PR / issue / event metadata to OpenAI via our Edge Functions — we don't retain the triage payload itself, though see below for what we do retain around it. Clicking, completing, or dismissing an AI Inbox item records an interaction event on our side. Signed-in users send first-party analytics events to our own backend. And sharing a setup explicitly uploads it. Every one of these paths is enumerated field-by-field on this page.
Where data lives
| What | Where | Notes |
|---|---|---|
| Layout, mode, theme choices On your device + Chrome sync | chrome.storage.sync | Rides Chrome's built-in profile sync. Travels between Chromes signed into the same Google account. We don't host or read it. |
| Todo content On your device — plus Supabase if "Sync widget content" is on | chrome.storage.local | Per-instance keyed ( |
| Scratchpad text On your device — plus Supabase if "Sync widget content" is on | chrome.storage.local | Per-instance keyed ( |
| Pomodoro running state On your device only | chrome.storage.local | Per-instance keyed ( |
| GitHub / Linear / Google Calendar OAuth tokens On your device only | chrome.storage.local |
|
| Finnhub API key (Ticker widget) On your device only | chrome.storage.local |
|
| Network response caches On your device only | chrome.storage.local |
|
| AI Inbox cached triage On your device only | chrome.storage.local | Namespaced by your butter user ID. TTL is the widget's "Cache for (minutes)" setting (default 60). |
| AI Meeting Prep cached brief On your device only | chrome.storage.local |
|
| butter account session On your device + Supabase auth | chrome.storage.local | Supabase JWT. Used to authenticate server-side calls (AI Inbox quota check, AI Meeting Prep, cloud sync, analytics, interaction tracking). Sign out from Settings → Account to clear. |
| Pro cloud-sync payload (opt-in) On your device + Supabase | Supabase row, scoped to your user | Layout + mode + theme choices. Todo lists and scratchpad text are added only if you also turn on "Sync widget content" (a second per-device toggle, off by default). Cloud sync itself is disabled by default on every device; turn on per-device. |
| AI Inbox interaction events On our side, scoped to your user | Supabase (<code>ai_inbox_interactions</code>) | When you click, complete, or dismiss an AI Inbox item, we record the event with the item's title, author, repo / team key, priority, and source. The last 30 days feed back into future triage prompts so the ranking learns your patterns. Signed-in only. |
| AI Inbox ranking weights On your device only | chrome.storage.local |
|
| Analytics events On our side | Supabase, scoped to your user | First-party product events: an event name (e.g. |
Every outbound request
A full enumeration of every domain butter calls from your browser. All
of these are direct browser → service requests; we don't proxy the
third-party ones. The *.supabase.co rows are our own
backend — those are the requests that actually reach us.
| Endpoint | When | What's sent |
|---|---|---|
| api.github.com | GitHub widget fetches PRs | Your OAuth token + search query. No payload back to us. |
| api.linear.app | Linear widget GraphQL queries | Your OAuth token + GraphQL query. No payload back to us. |
| www.googleapis.com/calendar/v3 | Calendar widget fetches your agenda window (today, 3 days, or the week, by widget size) | Your OAuth token + a date-window query. Read-only scope (<code>calendar.readonly</code>). No payload back to us. |
| api.coingecko.com | Ticker widget refreshes crypto quotes, or you search a coin in its settings | Coin ids / your search query. Anonymous, no key, no account. |
| finnhub.io | Ticker widget refreshes stock quotes (only if you added a key) | Ticker symbols + your own Finnhub API key. Direct browser → Finnhub; we never see the key. |
| hn.algolia.com | Hacker News widget refreshes | A public read against HN's Algolia API. No auth. |
| www.reddit.com | Reddit widget refreshes | Anonymous JSON read. No auth. |
| api.open-meteo.com | Weather widget refreshes | Coordinates + units. No account. |
| geocoding-api.open-meteo.com | You search a city in Weather settings | Your query string. |
| google.com/s2/favicons | Quick Links tile favicon load | The hostname of each link. (Legacy links with an emoji set skip the request.) |
| icons.duckduckgo.com | Fallback when Google has no favicon | Same — hostname only. |
| butter-auth.try-butter.workers.dev | You connect GitHub, Linear, or Google Calendar via the proxy flow; or any of those tokens is refreshed | Brokers OAuth handshake and refresh. No persistent storage. |
| api.are.na | Inspiration widget refreshes | Channel slug. Anonymous public read; no auth. |
| *.supabase.co/auth/v1/* | You sign in to butter, or your session token refreshes | The Supabase OAuth handshake (GitHub or Google) and token refresh. |
| *.supabase.co/rest/v1/profiles | The extension checks your Pro entitlement | Your JWT; returns your email, display name, avatar, and Pro status. |
| *.supabase.co/functions/v1/ai-inbox | AI Inbox widget runs a triage (signed in, provider connected). Background notification checks do NOT call this — they read the cached triage or fall back to raw counts | Titles, URLs, repo / team / state, timestamps, priority, draft flag. Payload below. |
| *.supabase.co/functions/v1/ai-meeting-prep | Calendar widget generates the AI brief for an upcoming meeting (Pro) | Event title, description, attendee names + emails, time window; plus the titles and identifiers of your recent GitHub PRs and Linear issues. Payload below. |
| *.supabase.co/rest/v1/ai_inbox_interactions | You click, complete, or dismiss an AI Inbox item | Item ID + type, the action, and metadata: title, author, repo, team key, priority, source. Retained — see the AI Inbox section below. |
| *.supabase.co/functions/v1/analytics-track | Product events fire while signed in — including on every new-tab open | An event name plus small properties (widget ID, item count, setup ID, plan). Scoped to your user ID server-side. Nothing sends while signed out. |
| *.supabase.co/rest/v1/dashboards + rpc/push_dashboard | Pro cloud sync pulls on boot or pushes on change (opt-in) | The cloud-sync payload: layout + mode + theme choices, plus todo / scratchpad content only if "Sync widget content" is on. |
| *.supabase.co/functions/v1/create-checkout-session, create-portal-session | You start a Pro checkout or open the Stripe billing portal | Your JWT; returns a Stripe URL. Card details go to Stripe, never to us. |
OAuth: what the auth-worker actually sees
GitHub, Linear, and Google Calendar integrations use OAuth.
butter operates a small Cloudflare Worker
(butter-auth.try-butter.workers.dev) that holds each
OAuth app's client_secret so you don't have to register
your own apps. The worker is briefly a middleman in the handshake:
- You click Connect. The worker redirects you to the provider's authorise page with the appropriate scopes.
- You approve. The provider redirects back to the worker with an authorisation code.
- The worker exchanges the code for a token. Uses
its
client_secretagainst the provider's token endpoint. - The worker hands the token to your browser via a
chromiumapp.orgredirect (URL fragment). It keeps no persistent state — the source is open and the README is explicit about this.
From that point on, your browser holds the token in
chrome.storage.local and talks directly to
api.github.com, api.linear.app, or
googleapis.com. The worker
is not in the data path for any subsequent fetch.
The AI Inbox: exact payload
This request fires when the widget runs a triage. The background notification check (the single Enable notifications toggle in Settings → Notifications) deliberately does not call it — notifications read your most recent cached triage, or fall back to raw item counts fetched directly from GitHub / Linear, so background checks never send anything to OpenAI and never spend your free daily run. When the widget does triage, the Edge Function receives a JSON body containing, for each PR:
titleurlrepoauthorupdatedAtdraft
And for each Linear issue:
identifiertitleurlteamKeystateNamepriorityupdatedAt
Plus a Bearer header with your Supabase JWT (used for
Pro authentication, never forwarded to OpenAI). The Edge Function
passes the structured payload to OpenAI, returns the model's response,
and discards the input. The triage payload itself isn't persisted on
our side, and per OpenAI's API terms, no model training on it.
Three things around the triage are persisted on our side, so we're saying so plainly:
- Interaction events. Clicking, completing, or
dismissing an item writes a row to
ai_inbox_interactionswith the item's title, author, repo, team key, priority, and source. - Those events feed future triages. The Edge Function reads your last 30 days of interactions into the prompt so the model learns what you tend to act on versus dismiss.
- Free-tier run timestamps. Each successful free-tier
triage records a row in
ai_inbox_runsto enforce the one-a-day cap.
Things that are not sent: PR diffs, review comments, commit history, Linear issue descriptions, issue comments, attachments, your OAuth tokens, or your IP address beyond the standard request metadata the Edge Function platform sees.
AI Meeting Prep: exact payload
Only relevant if you have the Calendar widget on your dashboard and a Pro subscription. The Edge Function receives a JSON body containing, for the next upcoming event:
idtitledescription(truncated at 1,000 chars)startMs,endMsattendees:email,displayName,isSelflocationjoinUrl
Plus up to thirty of your most recent GitHub PRs (same fields as the AI Inbox above) and up to thirty Linear issues, so the model can spot which work items relate to the meeting and surface them as references in the brief.
Things that are not sent: any other calendar's events, events outside today's window, PR or issue bodies, your OAuth tokens for any provider, attachments, or IPs beyond what the Edge Function platform sees. The brief is cached on your device for two hours so re-renders of the new tab don't re-bill the model.
Stripe (Pro billing)
Pro is billed through Stripe. We never see or store card details — Stripe handles checkout and stores the card on its own infrastructure. On our side we keep only the minimum needed to wire your subscription to your butter account: your email, your Stripe customer ID, your subscription ID, and whether your plan is active.
What we don't do
- No third-party analytics SDK — but there is first-party
analytics. No Segment, no Mixpanel, no Heap, no Google
Analytics in the extension. The extension does send product events
to our own Supabase Edge Function
(
analytics-track): an event name plus small properties — things likenew_tab_open(which fires on every new-tab open),widget_addedwith the widget ID, and AI Inbox view / action events. Events only send while you're signed in, and are scoped to your user ID server-side. Signed out, nothing is sent. No page content, no URLs you visit, no keystrokes. - No ads, no tracking pixels. butter doesn't have an ad business model and won't grow one. The free tier is paid for by the Pro tier.
- No selling data. The data path is: your browser → your devices → our Supabase backend for the signed-in paths listed above (analytics, interactions, cloud sync) → OpenAI only for the AI Inbox and AI Meeting Prep triage payloads. There is no other consumer of it.
- No required account. Free butter doesn't need an account; the AI Inbox and cloud sync are the only features that require sign-in.
Deleting your data
- Layout / mode / theme choices — uninstall the
extension, or clear
chrome.storage.syncvia chrome://extensions → butter → Storage → Clear. If Chrome sync is on, other devices will reflect the clear on next open. - OAuth tokens — disconnect from Settings → Connections, or revoke the integration from the provider's settings.
- Cloud sync payload and account data — turn cloud sync off in Settings → Account. There's no in-product account-deletion button yet; to have your account and its server-side rows (dashboard payload, interaction events, analytics) deleted, email support@trybutter.xyz and we'll do it. Pro subscription cancellation goes through Stripe's customer portal.
- Cached network data — also lives in
chrome.storage.local; the Storage panel above clears it too. Caches re-fetch on demand.