bugwatch docs

Security & privacy

For agents: nothing here is configurable per call. The per-project knobs are storeIp and filters via update_project; channel secrets are write-only via create_channel; tokens are managed with list_tokens / revoke_token (org:admin). Use search_docs for 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 the raw/ 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_address is dropped unless the project sets storeIp. 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 of sha256("<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, never 403.

Secrets

  • API tokens: 24 random bytes, bwt_ prefix, stored as SHA-256, shown once, optional expiry, revocable, last_used_at tracked.
  • Sessions: 32 random bytes in an HttpOnly; Secure; SameSite=Lax cookie, 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=transactions for 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.