How QuietPart works
A walk through the actual mechanics — what happens when you create a survey, when someone answers it, and how responses disappear afterward. The goal is for nothing here to surprise you.
The short version
QuietPart is a small web app. Anyone can create a survey through the home page. The app generates a public link and a one-time admin link. Respondents open the public link, fill out the form, and submit. Their answers are saved without anything that identifies them, and written answers are encrypted in the browser before they are sent. After a retention period the creator picked, the app deletes the entire survey from its database.
That's it. The rest of this page is "how each of those steps avoids collecting identity."
What the database actually stores
The application uses five SQLite tables. Here's every column, with nothing hidden:
surveys
id— internal primary keypublic_id— the random 12-character ID in the shareable URLadmin_token_hash,results_token_hash— SHA-256 hashes of access tokens. Raw tokens are never stored.encryption_pubkey— the public key respondents use to encrypt written answers. The private key is only in the admin link fragment.title,description— what the creator enteredcreated_at,expires_at,delete_after— timestamps for retentionresults_public,allow_free_text,status— settings
questions and question_options
The question text, type (single choice, rating, free text), and options for choice questions. No respondent data.
responses
id— internal primary keysurvey_id— which survey it belongs tocreated_at— the time the response arrived, truncated to the hour in UTC
That's the whole row. No respondent ID, no stored cookie value, no IP, no user agent, no referrer, no fingerprint.
answers
id,response_id,question_id— internal linksoption_id— for multiple-choice answers, the option that was pickedrating_value— for rating answers, an integer 1–5text_value— for free-text answers, browser-encrypted ciphertextencryption_version— which written-answer encryption format was used
The schema lives in src/db.js in the repository. If a future change tried to add an identifying column, the project's AGENTS.md is the first line of defense and the schema diff is the second.
What happens when someone submits an answer
- The browser sends a regular HTML form POST to
/s/{public_id}. - The server checks the survey exists and isn't expired.
- Each answer is validated (option IDs must belong to the question, ratings must be 1–5, encrypted text must match QuietPart's ciphertext format).
- The server inserts one
responsesrow withsurvey_idand a creation time rounded down to the start of the current UTC hour. So a response submitted at 14:37:22 and another at 14:58:09 both look like 14:00:00. - For each question, one
answersrow is inserted, linked to that response. Written answers are already ciphertext by this point. - The server sets a signed, survey-scoped cookie saying this browser has submitted to this public link. The cookie is not written to the database and expires no later than the survey's deletion time.
- The respondent is redirected — to the results page if the creator chose public results, or to a "thank you" page otherwise.
At no point does the request handler touch req.ip outside of the rate limiter, and the rate limiter only keeps a counter in memory. The IP never reaches a database query, a log line, or a header that gets stored.
Why there are no unique invite links
If QuietPart generated a separate URL for each respondent, the survey creator could correlate "who I sent which link to" with "which response came back." That's a textbook way to break anonymity while telling people it's anonymous. So we don't do it. Everyone uses the same /s/{public_id}.
The tradeoff is real: we can't enforce one vote per person, because we have nothing to tie votes to. We chose not collecting identity over enforcing uniqueness.
What the duplicate-submission cookie does
After a successful submission, QuietPart sets a necessary HTTP-only cookie scoped to that survey link. If the same browser tries to submit to the same link again while the cookie is valid, the app refuses the duplicate and shows an already-submitted message.
This is friction, not identity proof. It does not tell the creator who answered, and it does not stop someone from clearing cookies or switching browsers. Stronger enforcement would require logins, unique links, IP tracking, or browser fingerprinting, which would undermine the anonymity model. QuietPart also keeps an in-memory rate limiter that caps submissions per IP per hour; it is wiped on restart and never stored in the database.
Admin tokens
When you create a survey, the server generates a 24-byte random token and shows you the admin URL once. The database stores only the SHA-256 hash of that token. When you visit the admin URL, the server hashes the token from the query string and compares it (with constant-time comparison) against the stored hash.
The same admin URL also contains a #k= fragment with the decryption key for written answers. Browsers do not send fragments to the server, and QuietPart does not store the private key. This means QuietPart cannot send you a password reset, cannot show you "your" surveys, cannot decrypt written answers for you, and cannot recover the link if you lose it. There is no account system to recover. If the link is gone, the survey will still auto-delete on schedule, but you can't manage it before then.
What an admin actually sees
From the admin page, the survey creator can: see the response count, see aggregated results (counts, percentages, rating averages, and decrypted free-text answers when the admin key is present), close the survey early, delete it immediately, and download a CSV or JSON export.
What the admin cannot see — because the database never received it:
- Who answered.
- Whether two responses came from the same person.
- The IP address, user agent, or referrer of any respondent.
- The exact time a response was submitted (only the hour bucket).
- Any order, ID, or grouping that would let them sort responses by respondent.
Server-generated CSV and JSON exports contain ciphertext for encrypted written answers. The admin page can also build decrypted CSV and JSON files in the browser when the original #k= key is present. Exports have no hidden respondent metadata columns and do not group answers by response.
How auto-deletion actually runs
Every survey has a delete_after timestamp. A small cleanup job (in src/cleanup.js) runs a single SQL query:
DELETE FROM surveys WHERE delete_after <= now();
Foreign keys with ON DELETE CASCADE then remove the survey's questions, question options, responses, and answers in the same transaction. SQLite secure_delete is enabled, and QuietPart checkpoints and truncates the WAL file after deletions, with periodic VACUUM to reduce deleted-page residue. The cleanup job runs on three occasions:
- At server startup.
- On a timer (default: every 60 minutes; configurable via
CLEANUP_INTERVAL_MINUTES). - Manually, by running
npm run cleanup.
The cleanup job logs only the count of surveys it deleted. It never logs response content.
What's outside the application database
QuietPart can only promise things about what the application itself stores and deletes. A few things sit outside that database boundary:
- Reverse proxies and server logs. These may exist outside QuietPart's application database and may have their own retention behavior.
- Database backups and host snapshots. If the SQLite file is backed up, a deleted survey may still exist in a backup until that backup expires.
- The respondent's browser. History, autofill, and screenshots are local to the device and not something the server can clean up.
- Free-text content. If someone writes "It was me, Dave from Accounting, who said this," that's identifying information sitting inside the response, and there's nothing the server can do about it after the fact.
The privacy page covers these caveats more bluntly. This page is just trying to be honest about where the application's responsibility ends.
The pieces, in order
src/server.js— HTTP routes, validation, response handling.src/db.js— SQLite schema and connection. The schema is the source of truth for what can be stored.src/cleanup.js— the deletion job.views/— templates. The trust bar on every respondent page lives inviews/partials/trustbar.ejsand isn't editable by survey creators.AGENTS.md— the rules anyone making changes to QuietPart has to follow, including which fields the schema is allowed to grow.