Build Youform

YESreplaces $29/mosaves $348/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 conversational form builder whose hard problems are changing a form without corrupting answers already collected, getting every submission onto disk before anything else is attempted, and accepting file uploads larger than a serverless request body.

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

RuntimeNext.js 15 with TypeScript, plus one worker process for the outboxStorageSQLite through better-sqlite3, one file, written synchronously on submitSchemaVersioned JSON with stable question ids, frozen on publishUploadsPresigned direct-to-storage, never through the API routeDeliveryAn outbox table drained with backoff, each delivery carrying a stable id

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 outbox worker.

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

    Verify node --version prints v22 or higher

  2. pennies per month at personal volume

    Why Uploads have to bypass your API route, and presigned direct upload is the documented way to do it.

    Get it Any S3-compatible provider, or MinIO locally for development.

    Verify A presigned PUT from a browser stores a file you can then read by key

  3. API keyfree tier at personal volume on most providers

    Why Notification email goes out from the outbox, and a provider gives you delivery reporting you would otherwise have to build.

    Get it Create an account, verify a sending domain, and issue a key limited to sending.

    Verify A test message arrives and the provider reports it accepted

  4. decidefree

    Why Each of these is asserted by a test, and choosing them after seeing a failure means choosing whatever passes.

    Get it Read your hosting platform's current published request body limit, then write down your per-form response cap and how long uploads are kept.

    Verify All three are in the configuration and named in tests

  5. have readyfree

    Why The outbox is only testable against a receiver that times out, returns errors, and then recovers.

    Get it A tiny local server with switchable behaviour, or any request bin you control.

    Verify You can make it fail for an hour and then succeed

Data model

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

- forms(id, title, slug, current_version_id, created_at, closes_at, response_cap)
- form_versions(id, form_id, ordinal, schema_json, published_at) · published rows are never updated, only superseded
- schema_json holds questions(id, type, prompt, required, validation_json, logic_json) · ids stay stable across versions so answers can be compared
- responses(id, form_id, form_version_id, started_at, submitted_at, resume_token_hash, state)
- answers(response_id, question_id, value_json) · stored against the version that asked the question
- uploads(id, response_id, question_id, storage_key, filename, bytes, sniffed_type, sha256)
- outbox(id, response_id, kind, payload_json, delivery_id, attempts, next_attempt_at, delivered_at, last_error)
- Answers key on question id plus version, so renaming a question never silently merges two different questions into one export column.

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, versions, responses and the outbox. The whole product.
STORAGE_ENDPOINTrequiredhttps://s3.eu-west-1.amazonaws.comWhere presigned uploads go. Never through the API route.
STORAGE_ACCESS_KEYsecretrequiredAKIA...redactedUsed only to sign upload URLs and to delete on request.
STORAGE_SECRET_KEYsecretrequiredredactedNever reaches the browser. Presigned URLs do.
UPLOAD_MAX_BYTESrequired26214400Enforced in the storage policy as well as the page.
EMAIL_API_KEYsecretrequiredre_...redactedNotification email, sent from the outbox worker.
OUTBOX_MAX_ATTEMPTSrequired12After this a row is dead lettered and shown, not retried forever.
RATE_LIMIT_PER_HOURrequired30Per address, per form. The thing that stops the overnight script.

The build, in order

  1. A schema you can edit without rewriting history

    Publishing freezes a version and every response records which version it answered.

    1. Immutability is enforced in the data layer, not by remembering.

      Files src/lib/versions.tstests/immutable-version.test.ts

    2. The id is the column; the prompt is just the label.

      Files src/lib/schema.ts

    3. Two versions of one question are two columns, not an average.

      Files src/lib/export.tstests/export-versions.test.ts

    done when · tick each as it passes
    watch out
    • Editing the live form's questions in place rewrites the meaning of answers already collected, so an export shows text people typed when the prompt said something else entirely, and there is no record of what they were actually asked. Freeze the schema on publish and point each response at its version.
  2. Filling it in, and coming back tomorrow

    One question at a time, resumable, with validation that the browser cannot talk its way past.

    1. One rule set, applied twice, the second time authoritatively.

      Files src/lib/validate.tstests/server-validation.test.ts

    2. A long form abandoned at question nine should not start again at one.

      Files src/app/api/answer/route.ts

    3. Unreachable questions and skipped required ones are publish-time errors.

      Files src/lib/logic.tstests/logic-graph.test.ts

    done when · tick each as it passes
    watch out
    • Validating only in the browser means the public submit endpoint accepts anything a script sends, because the page is not a security boundary; and a conditional branch that routes around a required question produces responses missing an answer forever, with no error anywhere. Validate on the server against the version's schema, and walk the logic graph at publish for unreachable and skipped-required questions.
  3. Store first, then everything else

    Submitting commits the response before any webhook or email is attempted.

    1. No network call may sit between the respondent and a success page.

      Files src/app/api/submit/route.tstests/no-network-on-submit.test.ts

    2. The id is what lets a receiver ignore the duplicate.

      Files src/lib/outbox.ts

    3. A row failing forever should be visible, not invisible.

      Files worker/drain.tssrc/app/deliveries/page.tsx

    done when · tick each as it passes
    watch out
    • Calling the webhook inside the submit request means a slow receiver turns into a timeout the respondent sees, so they press submit again and you get two responses, or they give up and you get none. Commit first, return immediately, and deliver from the outbox.
  4. Uploads that never touch your API route

    A large file attaches to a response without passing through the request body.

    1. A reusable URL is an open upload endpoint.

      Files src/app/api/upload-url/route.tstests/presign-scope.test.ts

    2. A page attribute is a suggestion; the policy is the limit.

      Files src/lib/storage.ts

    3. The filename is whatever the respondent typed.

      Files src/lib/sniff.tstests/sniff.test.ts

    done when · tick each as it passes
    watch out
    • Posting the file to your own endpoint works on a laptop and fails in production, because the hosting platform caps the request body well below the size of an ordinary attachment: Vercel Functions, for instance, reject a request or response body over 4.5 MB with a 413 (checked September 2026), and the respondent sees a generic failure with no idea which file was too big. Upload direct to storage with a presigned URL, and enforce your own cap in the storage policy rather than in a form attribute.
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.