Build Alchemer

KINDAback to the verdict

0%0 of 16 items done

Saved on this device only. Tick prerequisites first, then work the phases in order · do not start one until the checks above it pass.

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.

estimated effort weekendthe files for this build are in the project pack

RuntimeNext.js 15 with TypeScript, server actions for submissionStorageSQLite with responses stored as answered, never normalised into the current schemaVersioningImmutable questionnaire versions, referenced by every responseLogicA small declarative rule format evaluated server-sideExportCSV and JSON written by one module with escaping tested

Before step 1

Everything below is assumed from the first step. Tick each one when you actually have it, not when you plan to.

  1. installfree

    Why Runs the application and the tests.

    Get it nodejs.org, or nvm install 22. open ↗

    Verify node --version prints v22 or higher

  2. 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

  3. 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

  4. 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.

VariableNeededExampleWhere the value comes from
DATABASE_PATHrequired./data/surveys.dbVersions, responses and the events trail.
PUBLIC_BASE_URLrequiredhttps://surveys.example.comUsed to build share links. A wrong value produces links nobody can open.
TOKEN_BYTESrequired24Entropy for survey and response tokens. Enumeration is the failure this prevents.
PARTIAL_TTL_HOURSrequired72After this a partial response expires, and the expiry is logged.
RATE_LIMIT_PER_HOURrequired60Per address, per survey. Reaching it alerts rather than failing silently.
WEBHOOK_SIGNING_SECRETsecretoptionalwhsec-...redactedSigns outgoing deliveries so the receiver can verify them.
EXPORT_DIRrequired./data/exportsStaged exports. Deletion has to reach in here too.

The build, in order

  1. A questionnaire you can edit while it is collecting

    Publishing a change creates a version, and answers already collected still mean what they meant.

    1. Immutability in the schema, not in a code comment.

      Files src/lib/versions.tssrc/db/schema.sql

    2. This one column is what makes the whole model work.

      Files src/lib/responses.ts

    3. Prove it by reading one back after two schema changes.

      Files tests/version-integrity.test.ts

    done when · tick each as it passes
    watch 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.
  2. Branching evaluated where it cannot be edited

    The server decides which questions are asked, which are required, and which answers are acceptable.

    1. The browser renders what it is told and decides nothing.

      Files src/lib/logic.tssrc/app/s/[token]/route.ts

    2. The check that turns skip logic from decoration into a rule.

      Files src/lib/validate-submission.tstests/hidden-question.test.ts

    3. Cycles and forward references caught before a respondent meets them.

      Files src/lib/logic-check.ts

    done when · tick each as it passes
    watch 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.
  3. Collecting on a bad connection

    A partial answer survives a closed tab, and a double submit produces one response.

    1. Fifteen minutes of someone's attention should not depend on their connection.

      Files src/lib/partial.ts

    2. Idempotency belongs in the database, not in a debounce.

      Files src/lib/submit.tstests/double-submit.test.ts

    3. The answer to 'it did not save' is a query, not a shrug.

      Files src/lib/events.ts

    done when · tick each as it passes
    watch 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.
  4. An export that a spreadsheet will not execute

    CSV output is safe to open and stable across schema versions.

    1. Equals, plus, minus and at sign, per OWASP's list.

      Files src/lib/csv.tstests/csv-injection.test.ts

    2. An export whose columns move is an export nobody can chart.

      Files src/lib/export-columns.ts

    3. The only test of this that counts is the manual one.

      Files docs/export-verification.md

    done when · tick each as it passes
    watch 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.
what this build does not replace
after v1, if you want it

Need the files? The project pack on the verdict page hands your agent the whole brief · more forms.