Build Alchemer
KINDAback to the verdict
A survey builder and collector whose hard problems are versioning a questionnaire without invalidating collected answers, evaluating branching on the server, and exporting data that spreadsheets will not execute.
Before step 1
Everything below is assumed from the first step. Tick each one when you actually have it, not when you plan to.
- installfree
Why Runs the application and the tests.
Get it nodejs.org, or nvm install 22. open ↗
Verify
node --version prints v22 or higher - installfree
Why The CSV injection fix cannot be verified by reading code. You need to open a crafted export and see the cell displayed as text.
Get it Any spreadsheet that people in your organisation actually use to open exports.
Verify
A crafted export opens showing the value as text rather than evaluating it - have readya few pounds a month
Why A survey link has to be reachable by respondents, which means a public address and a certificate.
Get it A small VPS or any platform that serves a Node application over HTTPS.
Verify
A survey link opens from a device on a different network - decidefree
Why How long partials live, what the response cap is, and how long submitted responses are kept are all asserted by tests, and choosing them after launch means choosing whatever the code already does.
Get it Write down three numbers and the reason for each.
Verify
All three are in the configuration and named in tests
Data model
Create these before the first phase that stores anything. Changing a table later is the expensive kind of change.
- surveys(id, name, created_at, current_version_id, state) · draft, collecting or closed - versions(id, survey_id, ordinal, schema_json, logic_json, published_at) · immutable once published - questions(version_id, id, kind, prompt, required, options_json, validation_json) · derived from the schema, kept for querying - responses(id, survey_id, version_id, token, started_at, submitted_at, state, source) · state is partial, submitted or discarded - answers(response_id, question_id, value_json, answered_at) · the value as given, not coerced to the current schema - submissions(token, response_id, claimed_at) · the one-shot token that makes a double post idempotent - events(response_id, at, kind, detail) · page views, saves, validation failures, the trail when someone says their answer vanished - Answers reference a question id within a version, so a later version renaming or removing that question leaves the old answer readable.
Environment variables
These go in a .env file the app reads at startup. The pack's .env.example is this table as a file · copy it, never commit the filled-in version.
| Variable | Needed | Example | Where the value comes from |
|---|---|---|---|
DATABASE_PATH | required | ./data/surveys.db | Versions, responses and the events trail. |
PUBLIC_BASE_URL | required | https://surveys.example.com | Used to build share links. A wrong value produces links nobody can open. |
TOKEN_BYTES | required | 24 | Entropy for survey and response tokens. Enumeration is the failure this prevents. |
PARTIAL_TTL_HOURS | required | 72 | After this a partial response expires, and the expiry is logged. |
RATE_LIMIT_PER_HOUR | required | 60 | Per address, per survey. Reaching it alerts rather than failing silently. |
WEBHOOK_SIGNING_SECRETsecret | optional | whsec-...redacted | Signs outgoing deliveries so the receiver can verify them. |
EXPORT_DIR | required | ./data/exports | Staged exports. Deletion has to reach in here too. |
The build, in order
A questionnaire you can edit while it is collecting
Publishing a change creates a version, and answers already collected still mean what they meant.
Immutability in the schema, not in a code comment.
Files
src/lib/versions.tssrc/db/schema.sqlThis one column is what makes the whole model work.
Files
src/lib/responses.tsProve it by reading one back after two schema changes.
Files
tests/version-integrity.test.ts
done when · tick each as it passeswatch out- Editing the question list in place while a survey is collecting silently rewrites history: yesterday's respondents answered a five point scale, today it has seven points, and the export now averages two different questions into one number. Freeze a version on publish and stamp every response with it.
Branching evaluated where it cannot be edited
The server decides which questions are asked, which are required, and which answers are acceptable.
The browser renders what it is told and decides nothing.
Files
src/lib/logic.tssrc/app/s/[token]/route.tsThe check that turns skip logic from decoration into a rule.
Files
src/lib/validate-submission.tstests/hidden-question.test.tsCycles and forward references caught before a respondent meets them.
Files
src/lib/logic-check.ts
done when · tick each as it passeswatch out- Implementing skip logic with a hidden attribute in the page means a respondent who opens developer tools, or a script posting the form directly, submits answers to questions that were never shown, and your required-field rule lets them through because the browser is the only thing enforcing it. Decide server-side and reject answers to questions the server did not ask.
Collecting on a bad connection
A partial answer survives a closed tab, and a double submit produces one response.
Fifteen minutes of someone's attention should not depend on their connection.
Files
src/lib/partial.tsIdempotency belongs in the database, not in a debounce.
Files
src/lib/submit.tstests/double-submit.test.tsThe answer to 'it did not save' is a query, not a shrug.
Files
src/lib/events.ts
done when · tick each as it passeswatch out- A respondent on a train taps submit, the request times out, they tap again, and you now have two responses from one person because the endpoint treats each post as new. Claim a one-shot submission token inside the transaction that records the submission, so the second post finds it already claimed and returns the same result.
An export that a spreadsheet will not execute
CSV output is safe to open and stable across schema versions.
Equals, plus, minus and at sign, per OWASP's list.
Files
src/lib/csv.tstests/csv-injection.test.tsAn export whose columns move is an export nobody can chart.
Files
src/lib/export-columns.tsThe only test of this that counts is the manual one.
Files
docs/export-verification.md
done when · tick each as it passeswatch out- A respondent types a value starting with an equals sign into a free-text box, and when a colleague opens your export in a spreadsheet it runs as a formula, which OWASP documents as reaching data exfiltration and worse depending on the application. Neutralise every cell whose first character is one of those four, and open a crafted export in a real spreadsheet before you believe your fix.
Sharing it without giving it awayproduct builder
A public link collects responses without becoming an open door, and data can be deleted on request.
Two lines that prevent the two ways a public form goes wrong.
Files
src/lib/tokens.tssrc/lib/limits.tsA webhook nobody can verify is a webhook nobody should trust.
Files
src/lib/webhooks.tsA deleted response sitting in a CSV on disk is not deleted.
Files
src/lib/delete.tstests/deletion-reaches-exports.test.ts
done when · tick each as it passeswatch out- A survey link that is only a sequential id lets anyone enumerate your other surveys, including the internal one you thought was private, and a survey with no cap and no rate limit becomes the target of a script that fills your database overnight. Use unguessable tokens, a response cap and a rate limit, and alert when the cap is reached rather than closing silently.
That is the whole plan for Alchemer. What it deliberately does not cover is below · check the gaps before you call it a replacement.
- The integration catalogue.
- Panel and audience purchasing.
- Compliance certifications and residency guarantees.
- Bot defence at public scale.
- large integration and template catalog
- compliance controls and guaranteed delivery
- resilient polling of malformed and rate-limited feeds
- enterprise workflow logic
- spam and abuse defense at scale
- Piping earlier answers into later question text, which reuses the logic evaluator and nothing else
- A second language per version, since versions are already immutable and can carry translations
- Scheduled exports to a folder, once deletion is proven to reach staged files
Need the files? The project pack on the verdict page hands your agent the whole brief · more forms.