What we store, for how long, and who else sees it.
This page names real database tables and real retention windows rather than describing them in the abstract, because a privacy policy you cannot check against the system is not worth reading. PullPulse holds review metadata: who opened what, who is waiting on it, and what it touched. It does not hold your source code.
Last updated 30 August 2026.
What we store
| category | what it is | where |
|---|---|---|
| Slack workspace and user ids | Workspace id and name, enterprise grid id, the bot user id, the id of whoever installed it, and the Slack user ids of members who linked a GitHub login — plus their timezone and any snooze they set. | slack_installs, identity_links |
| Sealed credentials | The Slack bot token and, if you configure one, your model provider API key. Both are stored only as AES-256-GCM ciphertext. MCP tokens are stored as a SHA-256 hash and never as the token itself. | slack_installs, workspace_settings, mcp_tokens |
| GitHub installation and repository metadata | Installation id, the account it belongs to, the API root, and per repository: full name, default branch, whether it is private, your .pullpulse.yml and the CODEOWNERS file we read to work out who knows the code. | github_installs, repos |
| Pull request titles and metadata | Title, description, URL, author login and whether that author is an agent, state, reviewers, check results, head commit sha, line and file counts, the computed risk score, and the model-written brief where one was generated. | pull_requests |
| Slack message bookkeeping | Channel ids and Slack message timestamps for the messages and threads PullPulse owns, so it can update them instead of reposting. Message text is not stored; it is rendered from the metadata above. | pr_messages, pr_threads |
| Notification history | A ledger of who was mentioned when, which is what the per-person noise budget is computed from, plus the reminders already sent so escalation never repeats itself. | notifications, reminders |
| Webhook payload archive | The raw GitHub webhook body, exactly as delivered, so a failed delivery can be replayed instead of lost. These bodies contain private repository content. | R2 object storage, plus an index row in deliveries |
| Billing records | Your plan and status, the Stripe customer, subscription and price identifiers, period end dates, and a monthly count of model calls. Card details never reach PullPulse. | subscriptions, usage_counters |
| Feedback and search index | Thumbs up or down on AI output with the Slack user who gave it, and vector embeddings of pull request text used to find similar past changes. | ai_feedback, Cloudflare Vectorize |
| Short-lived cache | GitHub installation tokens, Slack user lookups and single-use OAuth nonces, each with its own expiry measured in minutes. | Cloudflare KV |
Diff content is never persisted and never logged. It exists in memory while a pull request is being enriched, and in the prompt sent to the model provider for the features that need one. Nothing writes it to a database or to a log line.
Card details never reach us. Checkout and the billing portal are hosted by Stripe; we store only the identifiers Stripe gives us for your customer and subscription.
How long we keep it
- Notification ledger: 7 days. Rows older than that cannot influence any noise budget window, so they are deleted by an hourly sweep.
- Webhook payloads and their delivery rows: 30 days. The archived R2 object and the
deliveriesrow expire together as one record, because the row holds the only handle anyone has on the object. The retention window is also the replay window: replaying a month-old webhook would post month-old facts into Slack, and a webhook body is private repository content that must not be kept past it. - Archive sweep pacing. Each hourly tick drains at most 14 days of archive partitions and a bounded number of records, so a large backlog clears over days rather than in one unbounded pass. The window above is the policy; this is only how fast the deletion walks.
- Everything else: the life of the installation. Pull request metadata, routes, identities and billing records are kept while the workspace exists, because they are what the product reads to work.
Who else processes it
- Cloudflare — the whole service runs there: Workers for compute, D1 for the database, R2 for the webhook archive, KV for short-lived caches, Durable Objects for per-pull-request state, Vectorize for embeddings, and Workers AI as the default model provider.
- Slack — the surface PullPulse posts to, and the identity provider for everyone using it.
- GitHub — the source of every event and the API we read repository metadata from.
- Stripe — payments, invoices and the billing portal. Stripe is the only sub-processor that sees payment details, and it sees them directly rather than through us.
- Your chosen model provider — Workers AI by default, which keeps model calls inside Cloudflare. A workspace may instead configure its own OpenAI or Anthropic key in App Home, in which case prompts go to that provider under your account and your agreement with them.
How it is protected
- Every stored credential is sealed with AES-256-GCM under a key derived per workspace from a single
MASTER_SECRETwe hold. Per-workspace derivation means one workspace's ciphertext cannot be replayed into another's row, and rotating the secret invalidates every stored credential at once. - Inbound GitHub webhooks are verified by HMAC signature and Slack requests by Slack's own signing scheme, both before the body is parsed. An unverified request is rejected, not logged and retried.
- The status page you reach after installing is not addressable by workspace id. It requires a signed, expiring capability minted from a verified Slack action, so a URL is not a way to read a workspace you are not in.
- MCP tokens are stored as hashes, scoped to one workspace, and revocable from Slack.
- Our logs carry only fields the code explicitly names, truncated, with token-shaped values redacted before truncation. Repository content and credentials are not among the fields any log line names.
- Every tenant-scoped query is keyed on the Slack workspace id, and that isolation is covered by tests that populate two workspaces with identical repositories and pull request numbers and assert each reader returns only its own rows.
Deletion
Uninstalling the app from Slack revokes the stored credentials immediately and stops all processing for that workspace: queued events for a revoked workspace are dropped rather than delivered. The rows survive the uninstall so that reinstalling restores your routing, identities and history rather than starting from nothing — and so that uninstalling and reinstalling cannot mint a fresh trial.
Ask us to delete the workspace and we delete it: every table is keyed on the workspace id with cascading deletes behind it, so removal is complete rather than best-effort, and the archived webhook payloads age out on the 30-day window above.
Contact
For access, correction or deletion requests, message PullPulse in Slack from an installed workspace and a human will answer.