Transparency
How anonymity works.
An anonymous feedback tool only works if people trust it. So instead of asking for trust, here is the exact design: what we store, how the math works, and where the limits are. The source code is public.
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:
- You can only vote once per message.
- 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.
What is actually in the database
| Table | Contents | Identity? |
|---|---|---|
| messages | posted text, Slack timestamp, agree/disagree counters | none |
| message_votes | message timestamp + voter token | HMAC token only |
| post_locks | poster 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 |
Where the limits are
We'd rather you know these upfront:
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.
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.
Verify it yourself
Everything on this page can be checked against the source code: the token construction, the schema, and a test suite that makes sure raw user IDs never reach the database layer. If you find a hole in the design, we want to hear about it.