Security & privacy
For agents: nothing here is configurable per call. The per-project knobs are
storeIpandfiltersviaupdate_project; channel secrets are write-only viacreate_channel; tokens are managed withlist_tokens/revoke_token(org:admin). Usesearch_docsfor anything a customer's security questionnaire asks.
Where data lives
Bugwatch runs entirely on the Cloudflare Developer Platform: Workers for ingest, processing, API, notifications, cron, and these docs; Queues between them; R2 for objects; D1 for the control plane; Durable Objects for the per-project and per-org coordination that must be exact; KV for read-mostly caches; Analytics Engine for sampled analytics. Other providers receive data only for the features that need them. The full list, with what each receives and when, is the subprocessors page. In short:
- Email (Resend): recipient addresses and message text, including sign-in links, alerts and status notices.
- Payments (Polar by default, or Stripe): the organization id, the buyer's email and what was bought. Card data never reaches Bugwatch.
- Paging: Twilio receives a voice channel's number and a spoken alert summary. Apple and Google push services receive a device token and the notification text.
- AI features: when a person runs the fix agent or mobile chat, the issue's context goes to the model provider they chose (Anthropic, OpenAI or Google). That context is the title, exception, stack frames with source lines, breadcrumbs and release, plus files from a connected repository. Nothing is sent to a model provider unless somebody runs one.
Alerts also go wherever you route them (Slack, Discord, Teams, PagerDuty, webhooks).
| Store | Holds | Never holds |
|---|---|---|
| R2 (record of truth) | Raw envelopes (short TTL), processed event and transaction JSON (gzip), artifact bundles | Anything queried by attribute |
| D1 | Orgs, users, projects, keys, tokens, releases, issues and their exact counters, rules, channels, usage, audit | Per-event rows |
| Durable Objects | Fingerprint → issue map, dedup set, minute counters, exact quota | Bulk data |
| Analytics Engine | One sampled point per event/session/outcome: ids, hashes, low-cardinality dimensions | Bodies, messages, PII |
| KV | DSN config, quota flags, artifact manifests, query cache | Any source of truth |
Every repository query is scoped by organization; the analytics query builder pins the project id into every statement; R2 keys are prefixed by project id and read only through the API.
Retention
| Data | Retention |
|---|---|
Raw envelopes (raw/), as received, before scrubbing |
7 days (replay buffer) |
Processed events, projects on the r30 tier |
30 days (Free) |
Processed events, projects on the r90 tier |
90 days (paid plans) |
Attachments sent with an event (attachments/) |
As long as the event: 30 or 90 days by tier |
Trace markers (traces/), one empty object per transaction |
90 days, the longest event tier |
Source-map upload chunks (chunks/), before they are assembled |
1 day; deleted at once on a successful upload |
| Analytics index | ~3 months; nightly rollups keep per-issue daily counts in D1 beyond that |
| Uptime results, incident timelines | 90 days (swept nightly) |
| Alert delivery records | 30 days (swept nightly) |
| Sessions; sign-in links | 30 days; 15 minutes (24 hours for an email confirmation link) |
| Issues, releases, uploaded source maps, configuration, audit | Until the organization is deleted |
Event retention is enforced by R2 lifecycle rules per key prefix, so it needs no sweeper to run. The rules live in one file, infra/r2-lifecycle.txt, which setup applies and reads back. Control-plane records with a lifetime (sessions, sign-in links, delivery logs, monitor and incident history) are deleted by the nightly job. Both are verified end to end on every change: the full system runs locally, a real SDK sends an error and a transaction, the run fails if anything was stored under a prefix with no rule, and it ages a row of each swept table, runs the real nightly job, and requires that exactly the aged rows are gone.
Personal data
- Scrubbing happens as the processor writes an event to the record you read: sensitive keys, card numbers (Luhn-validated) and private-key blocks become
[Filtered]. Details are in Filters & scrubbing. The request as received is held for 7 days first, in theraw/replay buffer, so a failure in processing loses nothing. Scrub in your SDK too for anything that must never be stored at all. - IP addresses:
user.ip_addressis dropped unless the project setsstoreIp. An address your SDK puts anywhere else (a forwarded-for header, a tag) is not recognised as one. - User identity in analytics is
user_hash= 16 hex ofsha256("<project_id>:<id>")— salted per project, so the same person cannot be correlated across projects and raw identifiers never enter the append-only index. - Cross-organization requests return
404, never403.
Secrets
- API tokens: 24 random bytes,
bwt_prefix, stored as SHA-256, shown once, optional expiry, revocable,last_used_attracked. - Sessions: 32 random bytes in an
HttpOnly; Secure; SameSite=Laxcookie, stored as SHA-256, 30-day lifetime. - Passwords: PBKDF2 with 100,000 iterations and a per-user salt; login uses constant-time comparison and burns a full verify on unknown emails so timing does not reveal account existence.
- Channel configs (Slack URLs, PagerDuty keys, webhook URLs): AES-GCM encrypted with a Worker secret (
enc:v1:envelope); the API returns only a masked target; audit rows keep only the kind. - Uptime check credentials (basic and bearer auth, secret headers, a password in the URL): the database row holds the masked check plus the full one sealed in the same
enc:v1:envelope. The API shows the masked part and never needs the key; only the uptime Worker opens the sealed part, to run the check. - Outgoing webhooks are signed:
x-bugwatch-signature: v1=HMAC-SHA256(secret, "<timestamp>.<body>"). - Payment webhooks (Polar and Stripe) are verified with the signing secret and a 5-minute tolerance; redelivery cannot double-apply credits.
Sign-in
Email + password, Google, GitHub, Microsoft (OAuth), and email magic links. Providers are enabled per deployment (GET /auth/providers).
An email address is never taken on trust. An account records when its address was proven: by a magic link, by a Google or GitHub email the provider marks verified, or by the confirmation link sent at signup. When the deployment can send email, a password account cannot sign in until its address is confirmed, so registering somebody else's address gets you nothing. When the real owner later proves the address, they claim that account: the password set by whoever registered it is cleared, and every session and token it held is revoked.
A Microsoft sign-in never counts as proof of an address, because in Entra ID a tenant administrator sets the email claim. It signs in to the account it created, and is never linked to another account by email. An unproven account somebody could be using (created through Microsoft, or on a deployment with no email provider) is never taken over by an email match. Sign-in with that address is refused instead, with the reason.
Agent actions
Every mutating MCP tool call is attributable to a token and, when the token was created by a person, to that person: agent_actions rows plus issue activity entries. Agents cannot mint tokens, start checkout, or upload binaries (Tokens & scopes).
Deletion and export
Members, tokens, devices, channels, monitors and status pages can be removed in the dashboard or over the API, and IP storage can be turned off per project.
Deleting an organization is in Settings → General → Danger zone, for owners, with the organization's slug typed back (DELETE /v1/orgs/{org} with {"confirm": "<slug>"}; human-only, never an API token or agent). Cancel a paid plan first. The organization is gone at once: every request for it answers 404, its DSNs stop accepting events, its open incidents are resolved and its monitors stop, and its public status pages are never refreshed again. Its data is then erased in the background, normally within a few hours, however large the organization: DSN configs, filters, source-map manifests and status-page snapshots in KV; each project's coordinator, the quota counter and every monitor's state (stopping its probes again, in case the first attempt failed), with any incident still open resolved; every stored event, transaction, trace marker, raw request and uploaded artifact in R2; and every database row. What remains is a record that the deletion happened (the organization's opaque id and timestamps). Sampled analytics points age out of Analytics Engine within three months, as they do for everyone. Separately, and for every organization, an incident's escalation state is erased a day after it is resolved, and an on-call schedule's coverage cache is erased within a day of the schedule being deleted; the incident record and its timeline stay in the database for their stated retention.
Deleting your account is in Settings → Your account, with your email typed back (DELETE /auth/me with {"confirm": "<email>"}; your session only). It is refused while you own an organization that still exists, and the answer names it: delete the organization first, or ask support to hand it over. Each membership is ended as an admin's "remove member" would end it (tokens, phones, mobile channel, rotations, direct page targets), then your user record, sign-in methods, sessions, tokens, devices and notification rules are erased, and assignments or fallbacks that would route work to you are cleared. Organizations keep their own history, such as who resolved an issue, as an opaque id that no longer maps to any name or address.
Exporting an organization's data is in Settings → General → Export, for owners and admins, and over the API with an org:admin token, so a scheduled backup can run unattended. Both answers are newline-delimited JSON streamed straight to disk:
GET /v1/orgs/{org}/export: a header line ({"export":"bugwatch","version":1,…}), then one{"table": "…", "row": {…}}line for every database row the organization owns, then its members ("table": "members", with email and role). The rows are exactly the ones deletion erases, read from the same plan, so the two cannot drift apart.GET /v1/orgs/{org}/projects/{project}/export/events: every stored error event body for the project, one event per line; add?kind=transactionsfor its transactions.
No credential leaves in an export, because a downloaded file outlives every rotation: token and sign-in-code hashes, encrypted channel settings, push endpoints and keys, monitor heartbeat tokens, service inbound keys, status-page subscriber links and repository tokens are left out, and a monitor's check is exported as the API shows it (redacted). A test fails if a new column that looks like a credential is not classified. Nothing of another organization is ever in the file.
A copy of your own account record (your profile, sign-in methods and devices, outside any organization) is not self-service: email privacy@bugwatch.io from the account's address and it is done on request. The privacy policy covers your rights in full.
Roadmap, stated plainly
Ownership transfer, EU data residency (region-pinned R2/D1/DO), SSO/SAML and SCIM, 2FA, and SOC 2 are planned and not shipped.