Build Opinion Stage

YESreplaces $25/mosaves $300/yrback to the verdict

0%0 of 17 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 form, poll and quiz builder whose hard problems are surviving inside an iframe on somebody else's site without cookies or a known height, never losing a submission to a failed notification, and reporting results that are honest about being self-selected.

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

RuntimeOne server rendering both the builder and the public form, no client framework on the public pageStorageSQLite with the response written in one transaction before any notificationEmbeddingAn iframe plus a tiny script that posts its height to the parent, with an origin allowlistAnti-spamHoneypot, timing check and rate limiting, no third-party challengeAccessibilityLabels, autofill tokens and a minimum target size, checked in the suite

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 builder, the public form and the notification worker in one process.

    Get it nodejs.org, or your version manager. open ↗

    Verify node --version prints v22 or higher

  2. have readya few pounds a month plus the domain

    Why The form is embedded on other people's pages over a secure connection, so it needs a stable public address.

    Get it Any small virtual server with automatic certificates.

    Verify The form loads in an iframe on a test page over a secure connection

  3. API keyfree tiers exist; check the current limits before relying on one

    Why Notifications are the one outbound dependency, and it must be able to fail without costing a response.

    Get it Any transactional sending service or an SMTP account you control.

    Verify A test notification arrives and a deliberate failure leaves the response intact

  4. have readyfree

    Why Height messaging, the framing header and blocked third-party storage are only testable from a genuinely different origin.

    Get it Any static page on a second domain or a local server on another port.

    Verify The embed resizes correctly and an unlisted origin is refused

  5. decidefree

    Why Each becomes a test and a line in the privacy notice, and choosing them afterwards means choosing what the code already did.

    Get it Write down how long responses are kept, the minimum count for showing a percentage, and what makes a submission spam.

    Verify All three are in 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.

- forms(id, slug, kind, title, intro, thanks_text, status, allowed_origins_json, created_at) · kind is poll, quiz or survey
- questions(id, form_id, ordinal, kind, label, help_text, required, options_json, autocomplete_token, branch_json)
- responses(id, form_id, client_key, started_at, submitted_at, completed, user_agent_family, spam_reason) · client_key makes a retry idempotent
- answers(id, response_id, question_id, value_text, value_number, option_ids_json) · one row per answered question, so partials are real data
- quiz_keys(question_id, option_id, correct, points) · an answer key, which is the only kind of quiz score with a basis
- notifications(id, response_id, kind, attempts, last_error, delivered_at) · retried separately, never blocking the response
- views(form_id, day, loads, starts, submissions) · counted without cookies, for a drop-off funnel rather than for tracking people
- The response row is committed before anything else happens. Everything downstream is a retryable side effect.

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/forms.dbForms, responses and answers. The responses are the only irreplaceable part.
PUBLIC_BASE_URLrequiredhttps://forms.example.comUsed in embed snippets and in the framing header.
MAIL_API_KEYsecretoptionalre_...redactedOwner notifications only. A failure here never affects a stored response.
NOTIFY_TOrequiredyou@example.comWhere submission notifications go. The values themselves are not in the log.
UPLOAD_DIRrequired./data/uploadsFiles attached to responses. Deleted with the response and by retention.
RESPONSE_RETENTION_DAYSrequired365Stated in the privacy notice on the form and enforced by a job.
MIN_COUNT_FOR_PERCENTrequired20Below this, the report shows counts only and says why.

The build, in order

  1. A form people can actually complete

    Every question is properly labelled, keyboard-operable and sized for a thumb.

    1. A placeholder is an example, not a name.

      Files src/render/question.jstests/a11y.test.js

    2. It is one attribute and it satisfies a published criterion.

      Files src/render/question.js

    3. Measured, because a thumb is bigger than a mouse pointer.

      Files tests/a11y.test.js

    done when · tick each as it passes
    watch out
    • Placeholder text instead of a label looks clean and disappears the moment somebody types, so a respondent who is interrupted comes back to a form of unlabelled boxes, and a screen reader user never had a label at all. Use a real label element for every control and treat the placeholder as an example, not a name.
  2. A submission that cannot be lost or doubled

    A response survives a failed notification, a double click and a flaky connection.

    1. This ordering is the entire guarantee.

      Files src/submit.jstests/submit.test.js

    2. Double clicks and retries are the normal case.

      Files src/submit.js

    3. A mail outage should not cost you an answer.

      Files src/notify.jstests/notify.test.js

    done when · tick each as it passes
    watch out
    • Sending the owner email inside the request handler and rolling back when it fails throws away the answer because a mail server was briefly down, and the respondent saw an error and will not return. Commit the row, then queue the notification with its own retries, and show the respondent the thank-you screen either way.
  3. Living inside someone else's page

    The embed sizes itself correctly, talks only to origins you allow, and does not rely on storage it will not get.

    1. An unchecked message handler is somebody else's script talking to yours.

      Files src/public/embed.jssrc/public/host-snippet.js

    2. So your form does not appear on a site you have never heard of.

      Files src/headers.js

    3. For a large share of visitors it already does not.

      Files src/session.jstests/no-storage.test.js

    done when · tick each as it passes
    watch out
    • Preventing repeat responses with a cookie set inside the iframe quietly does nothing in browsers that block third-party storage, so the protection appears to work in your testing and is absent for a large share of real visitors. Decide what one response per person actually means for you, implement it server-side where you can, and state plainly in the report that an embedded public form cannot enforce it.
  4. Results that say what they are

    Every figure carries its count, and nothing implies a sample of a population.

    1. A percentage without an n is a rumour with a decimal point.

      Files src/report.js

    2. Below a threshold the number misleads more than it informs.

      Files src/report.jstests/report.test.js

    3. The report is what gets shared.

      Files src/templates/report.html

    done when · tick each as it passes
    watch out
    • A results page showing a clean percentage split with no count invites the number to be screenshotted and quoted, and by then nobody knows it came from eleven answers on a blog sidebar. Print the count next to every percentage, suppress percentages below a minimum, and put the self-selection sentence in the report rather than in the documentation.
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.