Build Youform
YESreplaces $29/mosaves $348/yrback to the verdict
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.
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 outbox worker.
Get it nodejs.org, or nvm install 22. open ↗
Verify
node --version prints v22 or higher - accountpennies 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 - 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 - 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 - 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.
| Variable | Needed | Example | Where the value comes from |
|---|---|---|---|
DATABASE_PATH | required | ./data/forms.db | Forms, versions, responses and the outbox. The whole product. |
STORAGE_ENDPOINT | required | https://s3.eu-west-1.amazonaws.com | Where presigned uploads go. Never through the API route. |
STORAGE_ACCESS_KEYsecret | required | AKIA...redacted | Used only to sign upload URLs and to delete on request. |
STORAGE_SECRET_KEYsecret | required | redacted | Never reaches the browser. Presigned URLs do. |
UPLOAD_MAX_BYTES | required | 26214400 | Enforced in the storage policy as well as the page. |
EMAIL_API_KEYsecret | required | re_...redacted | Notification email, sent from the outbox worker. |
OUTBOX_MAX_ATTEMPTS | required | 12 | After this a row is dead lettered and shown, not retried forever. |
RATE_LIMIT_PER_HOUR | required | 30 | Per address, per form. The thing that stops the overnight script. |
The build, in order
A schema you can edit without rewriting history
Publishing freezes a version and every response records which version it answered.
Immutability is enforced in the data layer, not by remembering.
Files
src/lib/versions.tstests/immutable-version.test.tsThe id is the column; the prompt is just the label.
Files
src/lib/schema.tsTwo 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 passeswatch 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.
Filling it in, and coming back tomorrow
One question at a time, resumable, with validation that the browser cannot talk its way past.
One rule set, applied twice, the second time authoritatively.
Files
src/lib/validate.tstests/server-validation.test.tsA long form abandoned at question nine should not start again at one.
Files
src/app/api/answer/route.tsUnreachable questions and skipped required ones are publish-time errors.
Files
src/lib/logic.tstests/logic-graph.test.ts
done when · tick each as it passeswatch 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.
Store first, then everything else
Submitting commits the response before any webhook or email is attempted.
No network call may sit between the respondent and a success page.
Files
src/app/api/submit/route.tstests/no-network-on-submit.test.tsThe id is what lets a receiver ignore the duplicate.
Files
src/lib/outbox.tsA row failing forever should be visible, not invisible.
Files
worker/drain.tssrc/app/deliveries/page.tsx
done when · tick each as it passeswatch 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.
Uploads that never touch your API route
A large file attaches to a response without passing through the request body.
A reusable URL is an open upload endpoint.
Files
src/app/api/upload-url/route.tstests/presign-scope.test.tsA page attribute is a suggestion; the policy is the limit.
Files
src/lib/storage.tsThe filename is whatever the respondent typed.
Files
src/lib/sniff.tstests/sniff.test.ts
done when · tick each as it passeswatch 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.
Whose data it is, and who can knockproduct builder
Responses export whole, delete whole, and a public URL cannot be filled ten thousand times overnight.
An export nobody has imported is a guess.
Files
src/lib/bundle.tstests/roundtrip.test.tsAn orphaned upload is still personal data.
Files
src/lib/delete.tsThe cap is the thing that stops the bill.
Files
src/lib/limits.tstests/rate-limit.test.ts
done when · tick each as it passeswatch out- A public form URL with no cap is an open door: one script fills it overnight, your notification inbox has nine thousand messages, your storage bill has nine thousand files, and the real responses are somewhere in the middle. Cap responses per form, rate limit per address, and close the form at the cap rather than accepting quietly.
That is the whole plan for Youform. What it deliberately does not cover is below · check the gaps before you call it a replacement.
- Payments inside forms.
- A template gallery.
- Team accounts and approvals.
- Your own email sending infrastructure.
- spam and abuse defense at scale
- large integration and template catalog
- compliance controls and guaranteed delivery
- enterprise workflow logic
- Partial response analytics showing where people abandon, computed from the answers you already store
- A second delivery target such as a spreadsheet, added as an outbox kind and nothing else
- Scheduled exports to storage you control, reusing the bundle format
Need the files? The project pack on the verdict page hands your agent the whole brief · more forms.