Transparency

How anonymity works.

An anonymous feedback tool only works if people trust it. This page is the exact design: what we store, how the math works, and where the limits are.

Not an engineer? There's a plain-language version of this page written for the people using Sincere.

The two problems

Anonymity would be trivial if we stored nothing about you. But two rules have to be enforced, and both are about you specifically:

  1. You can only vote once per message.
  2. You can only post once every 24 hours.

The naive implementation stores your Slack user ID next to each vote and each post. Anyone with database access could then deanonymize everything. Sincere enforces both rules without ever writing your user ID to the database.

Keyed tokens instead of identities

The server holds a single secret. It lives only in the server's environment, never in the database and never in backups. Your user ID is run through HMAC-SHA256 with that secret before anything touches disk:

voter_token  = HMAC-SHA256(secret,
                 "vote:" + message_ts + ":" + user_id)

poster_token = HMAC-SHA256(secret,
                 "post:" + user_id)

One vote per message, enforced by the database

Voting twice on the same message produces the same token, and a database uniqueness constraint rejects the second vote. This is enforced by the database itself, not just application code, so two simultaneous clicks can't slip through.

Votes can't be linked to a person, or to each other

The message timestamp is part of the vote hash, so your token on message A and your token on message B share nothing. Even with a full database dump, nobody can tell that the same person voted on two different messages. And only the agree/disagree totals exist. Which way anyone voted is never stored.

Rate limiting without history

Posting claims a single expiring lock row per (hashed) user, overwritten each time, so no history of when you posted builds up. Expired locks are deleted every minute.

No timestamps that give you away

Knowing when a message was submitted is enough to deanonymize its author if anything else shares that timestamp. So:

  • Queued messages store no submission timestamp, only the future posting time, picked at random between 5 seconds and 24 hours after submission.
  • The rate-limit lock's expiry is rounded up to the next full hour, so it can't be matched against a message's posting window.
  • Messages are posted by a scheduler that scans every 60 seconds, by the bot, with no reference to the submitter.
  • Slack request payloads contain your user ID and message text together. We never log them.

AI content moderation

Every submission is checked by an AI moderator (Anthropic's Claude API) before it is queued, to block harassment, threats, hate speech, and doxxing while allowing blunt, honest feedback. The same rules apply as everywhere else:

  • Only the message text is sent, never your user ID, team ID, or any timestamp. The API cannot tell who wrote it.
  • Neither the text nor the verdict is stored. A rejected message never touches the database, and we log only the rejection category.
  • A first rejection costs you nothing. Your daily post is returned so you can rephrase. A second rejection in a row uses your daily post, tracked as a single expiring HMAC-token row, domain-separated so it can't be joined against the rate-limit table.
  • The trade-off: the (anonymous) message text is processed by Anthropic at submission time under their data policy. That is one more party that sees the text, with no identity attached.

What is actually in the database

Table Contents Identity?
messages posted text, Slack timestamp, workspace/channel ID, agree/disagree counters workspace-level only (the message is already public in that channel)
message_votes message timestamp + voter token HMAC token only
post_locks poster token + hour-rounded expiry HMAC token only
moderation_strikes strike token + hour-rounded expiry HMAC token only
scheduled_messages text, team ID, random posting time none
installations Slack OAuth data for your workspace workspace-level only, never linked to any message or vote
subscriptions billing state for your workspace (paid plans) workspace-level only: billing identifies the paying workspace, never posters or voters
reporting_settings where your workspace's reports go: a channel id, the digest's timezone, when the last digest was sent workspace-level only: a channel, never a person
workspace_first_installs your workspace's id and the date it first installed Sincere, kept after an uninstall so a reinstall does not restart the free trial workspace-level only: a date, never a person

Reports: built only from what is already public

Workspaces can have a weekly digest posted to a channel of their choosing; the same report is available in the app's Home tab and as a CSV export. Reports add no new data collection:

  • Everything in a report is already visible in the feedback channel: the message text, its agree/disagree totals, and a label derived from those totals. There is no "who" and no per-person anything. That data does not exist.
  • Time is shown by the week. The export carries the week a message was posted, never a timestamp, and the posting time is itself randomized by up to 24 hours.
  • We do not keep a list of who may see reports. Access is membership of the reports channel (plus the installer and workspace admins), checked with Slack at the moment someone opens the report. The ID of the person looking is used in memory for that check and is neither stored nor logged, so there is no record of who viewed what either.
  • Breakdowns are suppressed below 5 messages, where a count would start describing individuals rather than the team.

How long data is kept

  • Posted messages and their vote rows are deleted after 12 months. A sweep runs every minute and removes anything older. A message's stored date is its posting time. The submission time was never stored.
  • Uninstalling the app deletes everything your workspace produced: posted messages, vote rows, any still-queued submissions, the reporting settings, and the installation itself. Two workspace-level records stay: the billing record of a paid subscription, which runs independently of the Slack install until it is cancelled, and the date your workspace first installed Sincere, so that installing again does not start a second free trial.
  • Rate-limit locks and moderation strikes expire within 24 hours by design and are pruned every minute.
  • Deleting our copy does not remove messages from Slack. They were posted publicly in your channel and remain there under your workspace's own Slack retention settings. Our retention governs only what Sincere stores.

Where the limits are

What this design does not protect against:

Slack knows you ran /sincere

Slash-command usage is visible to Slack Inc. per their privacy policy. We can only change what we store, not what Slack sees.

Anthropic sees the message text at submission time

Moderation sends the text (and nothing else) to the Claude API, subject to Anthropic's data retention policy. No user ID, team ID, or timestamp travels with it, but the submission moment is implicit in the API call itself. That record lives with Anthropic, not in our database.

The operator plus the secret can test membership

Someone with both the database and the server secret could compute tokens for every member of a workspace and check who voted on a message (not which way), or whether someone posted recently (not what they wrote, since text is never linked to any token). This design protects against leaked databases and backups. It does not protect against a fully malicious operator, and no server-side design can. That is why the secret is kept out of the database and backups.

Small groups leak socially

In a three-person channel, “somebody” is not very anonymous no matter what the database stores. Content itself can also identify its author.

Message text is stored in plaintext

Anonymity here means unlinkable to you, not encrypted.

How to check this

Everything on this page describes what the code does: the token construction, the schema, and a test suite that makes sure raw user IDs never reach the database layer. The source code is not public. If you are reviewing Sincere for your company's security team, ask us and we will give you read access. If you find a hole in the design, we want to hear about it.