A promise about privacy is worth what its enforcement is worth. Everything in this section is a property of the response engine running today, read off the real database schema and the real route code, and each card names the exact table, column, constraint or policy that enforces it so a board member or a district counsel can check it rather than take our word.
A response row has four columns, and none of them is a person
Enforced in the running system today
When somebody answers a foundation's question set, the row that gets written holds an opaque id, the organization that owns the question set, the hour it arrived, and a moderation verdict. That is the entire row. There is no name field, no email field, no address field, no account field, and no share-token field for a later query to join back to a person, because none of those columns exists to be filled in.
EvidenceCREATE TABLE qt_submission (id, owner_org_id, submitted_at, moderation_state) -- migration 1103_question_technology. No identity column of any kind.
The time is coarsened to the hour by the database, not by a policy
Enforced in the running system today
A board member who wants to know who complained can usually get most of the way there from a precise timestamp and the moment a link went out. That reconstruction is unavailable here: the stored time keeps only the hour, and a database check refuses any row that carries more precision than that. A bug in a route cannot store a precise moment, because the row is rejected.
Evidencesubmitted_at DEFAULT date_trunc('hour', now()) with CONSTRAINT ck_qt_submission_submitted_at_hour_coarse CHECK (submitted_at = date_trunc('hour', submitted_at)).
The owning organization is set by the server, never by the sender
Enforced in the running system today
A foundation's responses belong to its organization because the server resolves the owner from the question set being answered. The value is never read off the payload the responder submits, so a crafted request cannot write into another organization's data, and the column is a real foreign key rather than a loose string.
Evidenceowner_org_id inherited server-side from the resolved question set; CONSTRAINT qt_submission_owner_fkey FOREIGN KEY (owner_org_id) REFERENCES partner_org(id) ON DELETE RESTRICT.
Row-level security is on, and forced
Enforced in the running system today
Isolation between organizations is not left to whichever query somebody writes next. The response table has row-level security enabled and forced, with a policy that admits only the system role, so an application bug that forgets a WHERE clause still does not hand one organization another organization's rows.
EvidenceALTER TABLE qt_submission ENABLE ROW LEVEL SECURITY; FORCE ROW LEVEL SECURITY; POLICY qt_submission_system_only USING (is_system()) WITH CHECK (is_system()).
Three answer shapes, and the database enforces which three
Enforced in the running system today
A listening round, a grant-feedback form and a program review all compose from the same three shapes: open text, a one-to-five scale, and single choice. The type column is constrained to exactly those three, and a scale question is required to say in words what its low end and its high end mean, so a board never has to guess whether a 4 was good.
EvidenceCONSTRAINT ck_qt_question_type CHECK (type IN ('text','scale','choice')); SCALE_RANGE = {min:1,max:5} exported from packages/shared/src/question-technology.ts.
Moderation runs on the server before anything is stored
Enforced in the running system today
Open-text community feedback is where a survey gets ugly. The engine screens free text server-side before the row is written: hate, sexual content and spam are refused outright and never stored, while a borderline response is stored with a flag on it rather than thrown away, so a foundation reviewing its own results is not quietly having them edited.
EvidencemoderateQtSubmission() in apps/api/src/routes/question-technology.ts -- reject verdicts are never stored; a flag writes moderation_state 'flagged' under CHECK IN ('accepted','flagged').
The column set is pinned by a test against the real table
Enforced in the running system today
The promise above is only worth as much as the thing that stops it drifting. The exact column set is asserted against the applied database schema by a test in the estate, so adding an identity column to this table turns a test red instead of shipping quietly on a Tuesday.
Evidencepackages/db/src/migration-1103-zero-pii.test.ts asserts the qt_submission column set against information_schema.
These are storage-layer properties, not settings. Changing one means a migration and a red test, which is the point of stating them this way.