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

questions and question_options

The question text, type (single choice, rating, free text), and options for choice questions. No respondent data.

responses

That's the whole row. No respondent ID, no stored cookie value, no IP, no user agent, no referrer, no fingerprint.

answers

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

  1. The browser sends a regular HTML form POST to /s/{public_id}.
  2. The server checks the survey exists and isn't expired.
  3. Each answer is validated (option IDs must belong to the question, ratings must be 1–5, encrypted text must match QuietPart's ciphertext format).
  4. The server inserts one responses row with survey_id and 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.
  5. For each question, one answers row is inserted, linked to that response. Written answers are already ciphertext by this point.
  6. 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.
  7. 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:

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:

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:

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:

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

  1. src/server.js — HTTP routes, validation, response handling.
  2. src/db.js — SQLite schema and connection. The schema is the source of truth for what can be stored.
  3. src/cleanup.js — the deletion job.
  4. views/ — templates. The trust bar on every respondent page lives in views/partials/trustbar.ejs and isn't editable by survey creators.
  5. AGENTS.md — the rules anyone making changes to QuietPart has to follow, including which fields the schema is allowed to grow.