Skip to content
AtomicReps
Docs
Home

Data flows

What moves, per event

For a security reviewer: every Slack event the integration touches, the fields we process and store, and the fields we never receive. Nothing here introduces a claim that is not already in the Security page, Privacy Policy, or DPA.

Every request between Slack and Atomic Reps travels over TLS 1.3. Inbound Slack requests hit a single Convex HTTP endpoint, where we verify Slack's request signature before doing any work. Convex's infrastructure encrypts data at rest (AES-256). We store authentication tokens, including the Slack bot token, encrypted, and they never leave the server. The example payloads below use dummy identifiers.

1. OAuth install (inbound)

When an admin approves the install, Slack redirects to our Convex HTTP endpoint with a one-time code, guarded by an anti-CSRF state value we issued. We exchange the code for a bot token over TLS 1.3.

  • We store: the workspace (team) ID, the granted scopes, and the bot token (stored encrypted). We record the installer's Slack user ID and email to verify the email-match claim gate that links the workspace to an account.
  • We never receive: any workspace member's messages, channel contents, or a user token. The bot token authorizes posting and reading channel lists, nothing more.
Token exchange response (fields we keep)
{
  "ok": true,
  "app_id": "A0B1LP1EADA",
  "team": { "id": "T01ABCDEF", "name": "Acme Eng" },
  "scope": "commands,chat:write,chat:write.public,channels:read,groups:read,users:read,users:read.email,im:write",
  "access_token": "xoxb-****  (stored encrypted, never logged)",
  "authed_user": { "id": "U01INSTALLER" }
}

2. Daily rep post (outbound)

At the scheduled time we call Slack to post the question. The request carries the target channel ID and the question content as Block Kit blocks. No member data leaves Atomic Reps in this call.

chat.postMessage (outbound)
{
  "channel": "C01GHIJKL",
  "blocks": [
    { "type": "section", "text": { "type": "mrkdwn", "text": "*Rep of the Day - Redis*" } },
    { "type": "section", "text": { "type": "mrkdwn", "text": "A replica 3s behind is promoted. What happens to those writes?" } },
    { "type": "actions", "elements": [ /* one button per option */ ] }
  ]
}

3. Answer interaction (inbound)

When a member taps an option, Slack sends a block_actions interaction payload to our endpoint. We read the chosen option, the user ID, the team ID, the channel ID, and the one-time response_url for the ephemeral reply.

  • Trust boundary: we bind the write to the team ID attested by Slack's verified request signature. We encode an orgId in the button value and treat it only as a cross-tenant hint, never as authority.
  • We never receive: message history, channel content, or any text the user typed anywhere else. The payload carries the button they pressed and routing identifiers, nothing more.
block_actions payload (fields we read)
{
  "type": "block_actions",
  "user":    { "id": "U01MEMBER", "team_id": "T01ABCDEF" },
  "team":    { "id": "T01ABCDEF" },
  "channel": { "id": "C01GHIJKL" },
  "actions": [
    {
      "action_id": "qotd_answer_2",
      "value": "{\"questionId\":\"js7abc...\",\"optionIndex\":2,\"orgId\":\"org_hint\"}",
      "action_ts": "1721476800.001"
    }
  ],
  "response_url": "https://hooks.slack.com/actions/T01ABCDEF/....  (one-time)"
}

4. Ephemeral verdict (outbound)

We post the verdict and explanation back to the member's response_url as an ephemeral message. Slack shows it to the invoker only. No one else in the channel can see it, and Slack does not store it in the channel.

Response to response_url (ephemeral)
{
  "response_type": "ephemeral",
  "replace_original": false,
  "blocks": [
    { "type": "section", "text": { "type": "mrkdwn", "text": ":white_check_mark: Correct." } },
    { "type": "section", "text": { "type": "mrkdwn", "text": "Redis replication is asynchronous by default, so ..." } }
  ]
}

5. End-of-day close (outbound)

At the hour the workspace sets, we edit the original message to remove its answer buttons. We post no new message and add nothing to a thread, and the edit carries no number: the question and one closed line, both authored by us.

This used to be a summary thread carrying aggregate counts. It is gone. In a small channel, a count of who answered can identify them by elimination, so the channel now carries nothing derived from anybody's answers. That is a stronger promise than the k-anonymity floor it replaced.

chat.update on the original message (authored content only)
{
  "channel": "C01GHIJKL",
  "ts": "1721476800.000",
  "text": "Rep of the Day (closed)",
  "blocks": ["... the question as posted ...", ":lock: Answers are closed for today."]
}

6. Direct messages (outbound, im:write)

With the im:write scope we send direct messages: the daily question in DM-delivery workspaces and apprentice companion updates. These are the only things we DM. Any member can stop all of them with /reps mute and resume with /reps unmute; muting does not affect channel posts.

7. Uninstall (inbound)

When the app is uninstalled, Slack sends an app_uninstalled event. We read only the team ID from it. The event triggers deletion of that workspace's attributed Slack data.

All attributed Slack-specific data (user mappings, post history, sprint sessions, question feedback, insight delivery records, practice preferences, and in-Slack companion progress) is permanently deleted within 15 days: a 14-day retention window plus a daily deletion sweep, the same wording as the Privacy Policy. Anonymous aggregates that contain no identifiers continue under the 90-day window. See retention for the full schedule.

app_uninstalled event (fields we read)
{
  "type": "event_callback",
  "team_id": "T01ABCDEF",
  "event": { "type": "app_uninstalled" }
}

8. Email question of the day (answer links)

Subscribers to the email question of the day get one question with four answer buttons. Each button is a link carrying an opaque, HMAC-SHA256-signed token. The token references the subscription, the question, the send day, and the option index. The URL never contains a plaintext email address, user ID, or answer ID, and we trust nothing in the token until the signature verifies. Tokens expire after 14 days.

  • Opening a link never records an answer by itself. The landing page renders first and records the answer only after a real interaction signal (a tap, a keypress, or the page staying visibly rendered). Link prefetchers never trigger that, and we designed the gate to filter out sandboxing gateways that execute JavaScript. Without JavaScript, an explicit confirm button submits the answer.
  • Where the answer lives: a per-send row in our database, graded against the answer options pinned when the email was sent. The first answer wins; clicking again shows the recorded verdict. We delete these rows after 90 days (see retention).
  • The correct answer is never in the email. We grade on the server, and only when a subscriber taps a button.

9. Unsubscribe mechanics

  • One-click in your mail client (RFC 8058). Every email carries the List-Unsubscribe headers. When your mail client's unsubscribe button fires, the opt-out takes effect server-side immediately and permanently.
  • The footer link. Clicking unsubscribe in an email footer opens a page that records the opt-out and offers a 24-hour undo. The page does the writing, not the link itself, so a scanner prefetching the footer link can never silently unsubscribe you. The undo exists only for this path; it never reverses a mail-client one-click.
  • One flag stops everything. An unsubscribe suppresses all marketing streams (lifecycle emails, the newsletter, and the question of the day) at once. We keep the suppression record itself so we keep honoring the opt-out, and we remove it entirely on account deletion - the same rule the Privacy Policy states.

Three things that never happen

  • No message-history scopes. We do not request any message-history scope (channels:history, im:history, or similar) and never read message content from your channels or DMs.
  • No code ingestion. Atomic Reps does not ingest, read, or store source code from your repositories.
  • No AI training on your data. We never use your data to train, fine-tune, or improve AI or machine-learning models, and we do not send customer data to third-party AI providers.

For the same statements in their authoritative form, see the Security page, the Privacy Policy, and the DPA. Where anything here and a legal page differ, the legal page governs.