Build Fillout

YESreplaces $19/mosaves $228/yrback to the verdict

0%0 of 20 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 builder where the interesting part is the logic engine: questions that appear based on earlier answers, validation that runs the same rules on both sides, partial responses that survive a closed tab, and a submission pipeline that never loses an answer because a webhook failed.

estimated effort multi-daythe files for this build are in the project pack

RuntimeTypeScript with Fastify and PostgreSQLLogicA declarative rule tree evaluated identically on client and serverStorageAnswers as rows keyed by question, not a JSON blobPaymentsStripe Checkout for paid formsDeliveryAn outbox with retries for webhooks and notifications

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 server and shares the logic evaluator with the client.

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

    Verify node --version prints v22 or higher

  2. installfree locally

    Why Versioned questions, keyed answers and an outbox all want a real relational database.

    Get it Docker or your package manager. open ↗

    Verify psql connects and creates a table

  3. free to test; per-transaction in production

    Why Phase 4 only. Hosted Checkout keeps card details off your server entirely.

    Get it Sign up, use test keys, and configure a webhook endpoint with its signing secret. open ↗

    Verify A test Checkout session completes and the webhook is received and verified

  4. installfree

    Why Both payment and delivery testing need real webhooks reaching your machine.

    Get it The Stripe CLI forwards events, or use a tunnelling tool.

    Verify A test event arrives at your local endpoint and verifies

  5. decidefree

    Why Retention is stated on the form and enforced by a job, and the two have to match. Some question types make a form collect data with a much higher duty of care.

    Get it Decide the period, decide what you will refuse to collect, and write both down before building.

    Verify A written note states the period and the categories you will not collect

Data model

Create these before the first phase that stores anything. Changing a table later is the expensive kind of change.

- forms(id, slug, title, status, version, published_at) · a published version is immutable and edits create a new version
- questions(id, form_id, form_version, key, type, label, required, config_json, ordinal) · key is stable across versions, id is not
- rules(id, form_id, form_version, target_question_id, condition_json, action) · action is show, hide or require
- responses(id, form_id, form_version, started_at, submitted_at, resume_token_hash, ip_country)
- answers(response_id, question_key, value_json, at) · keyed by the stable key so a response stays readable after the form changes
- outbox(id, response_id, kind, payload_json, attempts, next_attempt_at, delivered_at, last_error)
- A response records the form version it was submitted against, because reading old answers against a new form is how reporting goes wrong.

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_URLsecretrequiredpostgres://localhost:5432/formsContains respondents' answers. Treat the connection string and backups accordingly.
RESUME_TOKEN_TTL_HOURSrequired72How long a resume link works. It is a bearer credential, so shorter is safer.
RESPONSE_RETENTION_DAYSrequired365Must match what the form tells respondents.
STRIPE_SECRET_KEYsecretoptionalsk_test_...Only for paid forms. Test keys until you are certain the webhook path is right.
STRIPE_WEBHOOK_SECRETsecretoptionalwhsec_...Verifies incoming events. An unverified webhook endpoint is an open endpoint.
WEBHOOK_SIGNING_KEYsecretrequireda random 32-byte hex stringSigns your outgoing webhooks so receivers can verify them.

The build, in order

  1. Questions, answers and versions

    A form collects answers that stay readable after the form changes.

    1. The key is what an answer refers to and it must not change when the question is edited.

      Files src/schema.sql

    2. Editing a live form mid-collection is otherwise how responses become inconsistent.

      Files src/versions.ts

    3. Reading old answers against a new form is a reporting bug waiting to happen.

      Files src/responses.ts

    done when · tick each as it passes
    watch out
    • Storing answers as a JSON blob against the current form means renaming or deleting a question silently breaks every historical response, and this is discovered months later when someone runs a report. Store answers as rows keyed by a stable key and record the form version.
  2. Conditional logic that runs on both sides

    Questions appear based on earlier answers, and the server agrees.

    1. Shared module. Two implementations diverge and the divergence is a security hole.

      Files src/logic/evaluate.tssrc/logic/evaluate.test.ts

    2. Reject answers to hidden questions. Test with a crafted post, not the browser.

      Files src/submit.ts

    3. Static analysis of the rule tree. A cycle at runtime hangs a respondent.

      Files src/logic/analyse.ts

    done when · tick each as it passes
    watch out
    • Logic evaluated only in the browser means a submitted response can contain answers to questions that should have been hidden, or omit ones that should have been required, because anyone can post directly to the endpoint. Share one evaluator and run it server-side on submission.
  3. Partial responses that survive a closed tab

    Someone answers half a long form, comes back tomorrow and continues.

    1. Per answer, not per page. A long form loses more than a page's worth otherwise.

      Files src/partial.ts

    2. It is a bearer credential for personal information. Treat it like one.

      Files src/resume.ts

    3. Which questions were visible, not just the values.

      Files src/partial.ts

    done when · tick each as it passes
    watch out
    • A resume link is a bearer credential granting access to whatever has already been answered, which on many forms is personal information. Store a hash rather than the token, keep the expiry short and stated, and never put the token in a page the respondent might share.
  4. Taking payment inside a form

    A form can charge, and a paid submission is never lost.

    1. Card details on your server is a different project with a compliance scope.

      Files src/payments.ts

    2. Not the redirect. Test by killing the browser after paying.

      Files src/webhooks.ts

    3. Keyed on the event id. Retries are normal.

      Files src/webhooks.tstests/webhook-idempotency.test.ts

    done when · tick each as it passes
    watch out
    • Marking the response complete on the browser redirect loses submissions whenever someone pays and closes the tab before the redirect completes, which is a paid customer with no record of their answers. The webhook is the source of truth and the redirect is only a convenience.
  5. Getting answers where they need to go

    Notifications and webhooks fire reliably, and a failure never loses a submission.

    1. The response and the intent to deliver commit together or not at all.

      Files src/outbox.ts

    2. Never in the request path. The respondent should not wait on someone else's server.

      Files src/worker.ts

    3. A timestamped signature the receiver can verify.

      Files src/sign.ts

    done when · tick each as it passes
    watch out
    • Calling a webhook inside the submission request means a slow or failing endpoint either delays the respondent or loses the delivery entirely when the request times out. Commit the response and the delivery intent together, then deliver separately.
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.