Privacy & data — butter docs Skip to content
Essentials

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).
  • One thing reaches off-device by design. The AI Inbox sends a small slice of your PR / issue metadata to OpenAI via our Edge Function. It's the only widget that does. It's a Pro feature; it's clearly labelled; and we don't retain or log the payload.

Where data lives

What Where Notes
Layout, theme, background
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 Pro cloud sync is on
chrome.storage.local

Per-instance keyed (todos:<id>). Free: device-local only. Pro cloud sync (opt-in): rides in the v2 payload.

Scratchpad text
On your device — plus Supabase if Pro cloud sync is on
chrome.storage.local

Per-instance keyed (scratchpad:<id>). Same rule as Todo content: device-local on Free, included in the Pro cloud-sync payload when enabled.

Pomodoro running state
On your device only
chrome.storage.local

Per-instance keyed (pomodoro:<id>). Never synced, on either tier — a running timer shared across machines would mean the alarm fires on a Chrome you're not at.

GitHub / Linear / Google Calendar / TickTick OAuth tokens
On your device only
chrome.storage.local

oauth:<provider>. Never synced, never exported. Used directly against the provider API on every fetch. Refresh routes through the auth-worker server-side so the client_secret stays out of the extension.

Network response caches
On your device only
chrome.storage.local

cache:<channel>:.... Two-minute TTL for Calendar; five for HN / Reddit / GitHub / Linear; thirty for Weather. Re-fetched on demand.

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

ai-prep:v1:<eventId>. Per-event cache so reopening the new tab doesn't re-bill the model. Two-hour TTL; the brief's refresh button forces a regenerate.

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). Sign out from Settings → Account to clear.

Pro cloud-sync payload (opt-in)
On your device + Supabase
Supabase row, scoped to your user

Same payload as the sync layer: layout + theme + background. Disabled by default on every device; turn on per-device.

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 them.

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 today's events Your OAuth token + a today-window query. Read-only scope (<code>calendar.readonly</code>). No payload back to us.
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. Hidden if you set an emoji.
icons.duckduckgo.com Fallback when Google has no favicon Same — hostname only.
auth.trybutter.xyz You connect GitHub, Linear, Google Calendar, or TickTick 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.
api.ticktick.com Todo widget reads or mutates a synced project Your TickTick OAuth token + task payload (title, due date, priority, tags, sort order). Goes direct to TickTick, never through our servers.
*.supabase.co/functions/v1/ai-inbox AI Inbox widget refreshes 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.

OAuth: what the auth-worker actually sees

GitHub, Linear, and Google Calendar integrations use OAuth. butter operates a small Cloudflare Worker (auth.trybutter.xyz) 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_secret against the provider's token endpoint.
  • The worker hands the token to your browser via a chromiumapp.org redirect (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 or api.linear.app. The worker is not in the data path for any subsequent fetch.

The AI Inbox: exact payload

Only relevant if you've added the AI Inbox widget. The Edge Function receives a JSON body containing, for each PR:

  • title
  • url
  • repo
  • author
  • updatedAt
  • draft

And for each Linear issue:

  • identifier
  • title
  • url
  • teamKey
  • stateName
  • priority
  • updatedAt

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. No persistence on our side, and per OpenAI's API terms, no model training on it.

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:

  • id
  • title
  • description (truncated at 1,000 chars)
  • startMs, endMs
  • attendees: email, displayName, isSelf
  • location
  • joinUrl

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 analytics SDK. No Segment, no Mixpanel, no Heap, no Google Analytics in the extension. The marketing site has a privacy-respecting analytics setup; the extension itself has none.
  • 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 → maybe Supabase if you opted into cloud sync, maybe OpenAI if you opted into the AI Inbox or AI Meeting Prep. 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 / theme / background — uninstall the extension, or clear chrome.storage.sync via 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 — turn cloud sync off in Settings → Account, then delete your butter account via the same panel. 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.