Appearance
Pulse survey anonymity โ
WS9 (GAC-52) ยท gates sub-task 9.1. This is the privacy review the spec (ยง12) requires before storing pulse responses.
The guarantee โ
For an anonymous survey, a stored response is not re-identifiable to the member who gave it. There is no column, foreign key, or join path from an answer back to a person.
A survey's anonymity is chosen by the admin when the survey is opened (pulse_surveys.anonymous). It cannot be changed after responses exist (closing, not editing, is the lifecycle).
How it's enforced (schema) โ
Two facts are stored separately:
| Table | Holds | Identity |
|---|---|---|
pulse_participation | that a member submitted (survey_id, user_id) | member id โ no answers |
pulse_responses | what was answered (survey_id, question_id, value) | respondent_id |
- Anonymous survey:
pulse_responses.respondent_idis NULL. Participation records only that the member submitted (for the one-per-member guard and the response rate) โ never their answers. The two tables share no key that ties a participation row to a response row, so "who answered what" cannot be reconstructed by any query. - Non-anonymous survey:
respondent_idis the member, and results may be attributed.
Residual risks (accepted for v1) โ
- Timing correlation. A response and its participation row are written in the same transaction, so a privileged operator with raw DB + write-time access could correlate by timestamp. Accepted for a cooperative-internal tool; a future hardening is to batch/jitter response timestamps or commit responses separately from participation.
- Small-N inference. Aggregates over very few respondents can be de-anonymising. The results layer should suppress breakdowns below a minimum respondent count (left as a follow-up; not enforced in this pass).
Tested invariant โ
After an anonymous submission, every pulse_responses row for that survey has respondent_id IS NULL, and no query joins a response to a member. A regression that introduces such a link fails the anonymity test in apps/api/internal/router/pulse_test.go.