How to build a Typeform alternative
How to build a Typeform alternative with one question at a time, a response table, and a CSV. Follow the prompts, check each step, and skip the visual builder.
You can learn how to build a Typeform alternative as one question at a time and a table of answers. Typeform is that product. The catalog line is “A polished form for the seven questions you actually ask.”
You pay $29–$99 a month. That is $348–$1,188 a year. The verdict is “Build it tonight”. The first estimate is 2 hours: one-question-at-a-time form, response table.
I already wrote the teardown, Vibe code your own Typeform. This guide is the walk. Prompts in order, then what to look at.
The audience is a small business collecting leads or client intake. If you take payments in the form, or you run research with quotas and panels, keep paying. The same v1 also replaces Jotform and Youform.
How to build a Typeform alternative without a builder
The Typeform page splits the work into three rows.
The first row is the one you want. Four questions. One on screen. A public share URL. A table you can search. That is the 2-hour v1.
The second row is 6 hours. Logic jumps, theming, a polished CSV, webhooks. Do that after someone has submitted.
The third row is 2 weeks and up. A visual builder, payment fields, an integrations gallery. Skip it.
If you tell an agent to “clone Typeform”, it aims at the third row. Pin every prompt to the first one.
Here is the shape of the data. Nothing else has to exist at 2 hours.
const form = {
slug: 'studio-intake',
title: 'New client intake',
intro: 'A few questions before we book a call.',
thankYou: 'Got it. I will email you this week.',
}
const questions = [
{ id: 'name', type: 'text', label: 'What should I call you?', required: true, position: 1 },
{ id: 'email', type: 'email', label: 'Where should I reply?', required: true, position: 2 },
{ id: 'work', type: 'select', label: 'What do you need?', options: ['Site', 'Newsletter', 'Something else'], position: 3 },
{ id: 'notes', type: 'long', label: 'Anything I should read first?', required: false, position: 4 },
]
A visitor hits https://forms.flaviocopes.com/studio-intake and answers those one at a time. You open the table. The share URL you already sent still works.
LogicRule waits. The card lists Form, Question, LogicRule, Submission, and Answer. Tonight you need the first two plus Submission and Answer.
Pick the stack you already write
Every product prompt ships in four flavors: AHA (Astro, HTMX, Alpine.js), Next.js, Laravel, and Rails. The capability line for Typeform is Astro, HTMX, and D1.
This site is an Astro app on Cloudflare, so that is the path I default to. Use Next.js if that is your weekday language.
Open Build your own Typeform. The guardrail is the useful sentence: validate and rate-limit submissions server-side, and include a honeypot field. The non-goal sits next to it. No visual workflow builder, no payment fields.
Pick one stack and stay on it.
Prompt 1. Scaffold Goodform and stop
Copy the AHA prompt from the product page. Paste it into your coding tool. Put this block above it.
Ship the 2-hour row only. One-question-at-a-time form,
response table.
Project name: Goodform.
Owner is one person. Respondents do not create accounts.
Do not add logic jumps, theming, or webhooks yet.
Do not add a visual builder or payment fields.
Do not add an integrations gallery.
When the plan is done, stop and wait. Do not write
implementation code until I say go.
The official prompt already asks for a short technical plan first: routes, tables, authorization, jobs. Let it produce that plan. Read it.
You want pages like these:
/formsfor the owner list/forms/studio-intake/editfor questions and copy/forms/studio-intakefor the public responder/forms/studio-intake/resultsfor the table
You want tables like forms, questions, submissions, answers. logic_rules should not appear. payments should not appear.
If the plan includes Stripe, a drag-and-drop canvas, or a users table for respondents, send it back. Paste the non-goal again.
When the plan is narrow, say go. Let it scaffold the app, the migrations, and empty screens.
What to check after the scaffold
Start the app. Sign in as the owner. You should see an empty forms list and a create action.
Open the migrations. forms.slug is unique. There is a questions table with position, not a JSON blob that the agent plans to “figure out later”. If the README is a placeholder, stop and make the agent write a real one before you add logic.
Prompt 2. Write the four questions
Now fill the builder. Keep the public URL off until you publish.
Implement the form editor for Goodform.
The owner can save slug, title, intro, and thank-you
copy. Slug is studio-intake in v1. The public URL is
https://forms.flaviocopes.com/studio-intake.
The owner can add, edit, reorder, and delete questions.
Each question has id, type, label, required, position,
and options when the type is select. Types in v1: text,
email, select, long. No file upload. No payment. No
rating yet.
Seed this form:
- slug studio-intake
- title New client intake
- intro A few questions before we book a call.
- thank-you Got it. I will email you this week.
- questions in this order:
name / text / What should I call you? / required
email / email / Where should I reply? / required
work / select / What do you need? / Site, Newsletter,
Something else
notes / long / Anything I should read first?
Phone-sized preview uses the same template as the live
form. Do not publish yet. Saving the draft must leave
https://forms.flaviocopes.com/studio-intake on a
coming-soon state.
Done when I can save those fields, refresh, and see the
same values in the editor and the preview.
The preview has to share the public template. A second “editor preview” is how you ship a form that looks fine in admin and wraps badly on a phone.
What to check after the questions save
Create the form. Refresh. Title, intro, and the four labels are still there. The preview is 390 pixels wide, not a desktop column.
Change the email label to “Best email?”. The preview follows it. The public URL still 404s or shows coming soon.
If the agent adds Stripe, a file picker, or a “logic” tab, delete those screens. They belong to later rows.
Prompt 3. Ask one question and refuse junk
The official journeys mention one question at a time, back and next, and a required email that keeps the answer. Stay on that. Add the honeypot from the guardrail.
Build the public responder for
https://forms.flaviocopes.com/studio-intake.
Show the intro first. Then one question per view.
Progress reads 2 of 4. Back and next keep every answer
already typed. A refresh mid-form restores the session.
On final submit, validate on the server:
- required name and email
- email has a local part, an @, and a host
- work is one of Site, Newsletter, Something else
- notes is optional, max 2000 characters
- honeypot field website must be empty
- at most 20 accepted submissions an hour per form
A filled honeypot returns the thank-you screen and
writes no row. A rate-limited human sees a clear
try-later message. A bad email keeps the name and
notes they already typed and focuses the email
explanation.
Issue a submission token when the form loads. One
token, one row. A double click returns the original
success. Do not send email yet.
Publish the seed form once so the public URL works.
Done when Jordan Lee can finish the four questions,
[email protected] is required, javascript in
the name field is stored as text, and a second click
does not invent a second row.
Agents give you a pretty form and a wide-open POST. The catalog already told you to validate on the server.
function shouldAccept({ honeypot, submissionsLastHour }) {
if (honeypot.trim() !== '') {
return { ok: false, reason: 'ignored' }
}
if (submissionsLastHour >= 20) {
return { ok: false, reason: 'rate-limited' }
}
return { ok: true }
}
shouldAccept({ honeypot: 'buy followers', submissionsLastHour: 3 })
//{ ok: false, reason: 'ignored' }
Twenty an hour per form is high enough for a launch day. A bot can get the thank-you screen and no row.
function emailLooksUsable(raw) {
const email = raw.trim().toLowerCase()
const at = email.indexOf('@')
if (at < 1) return false
const host = email.slice(at + 1)
return host.includes('.') && !host.startsWith('.') && !host.endsWith('.')
}
emailLooksUsable('[email protected]')
//true
You are checking shape, not sending mail. A DNS lookup on every submit turns the form into a slow open resolver.
What to check after the responder works
Open https://forms.flaviocopes.com/studio-intake in a private window. You see the intro, then one question. Progress says 2 of 4 on the email step.
Type a name. Go next. Go back. The name is still there. Submit jordan as the email. The field explanation appears. The name did not vanish.
Submit as Jordan Lee, [email protected], Newsletter, and a short note. You see “Got it. I will email you this week.” Mash submit. You still have one submission.
Fill the hidden website field in the inspector and submit again. Thank-you shows. No second row.
If the public page dumps all four questions on one long scroll, the agent built a survey. Send prompt 3 back.
Prompt 4. Read the table and download a CSV
The first estimate stops at a response table. The estimate table puts a polished CSV on the 6-hour row. Download the current table tonight anyway. The mini-prompt is “The CSV export that can’t attack a spreadsheet”. Use a tighter version so the agent does not stream 50,000 rows.
Build results for /forms/studio-intake/results.
Show a table of submissions: submitted-at, name, email,
work, notes, and submission id. Search by email.
A row opens a detail page with the exact question
wording the respondent saw.
Publishing freezes the current questions as a JSON
snapshot on the form. New submissions store
form_version. Editing a label after publish must not
rewrite old rows. No versioning UI in v1. Keep the
last snapshot so old answers stay readable.
CSV download for the current rows:
- columns in form order, plus submitted-at and id
- prefix = + - @ tab or CR with a single quote
- quote commas, quotes, and newlines
- CRLF row endings
- UTF-8 BOM
- filename studio-intake-2026-10-24.csv
Do not stream 50,000 rows. Do not add webhooks.
Seed Jordan's row if it is missing. Then edit the
email label to Best email? without publishing again.
Done when the table shows Jordan, search finds
[email protected], the CSV opens as text in
Excel, and the detail page still says Where should
I reply?
Respondents type =HYPERLINK into a notes box. Spreadsheet apps will run that as a formula if you let them.
function escapeCsvCell(value) {
let text = String(value)
if (/^[=+\-@\t\r]/.test(text)) {
text = `'${text}`
}
if (/[",\n\r]/.test(text)) {
text = `"${text.replaceAll('"', '""')}"`
}
return text
}
escapeCsvCell('=2+2')
//'=2+2
Prefix a leading =, +, -, or @ with a single quote so the cell stays text. Columns follow the published snapshot, not the draft sitting in the editor.
function publishForm(draft) {
return {
slug: draft.slug,
title: draft.title,
intro: draft.intro,
thankYou: draft.thankYou,
version: draft.version + 1,
questions: draft.questions.map((question) => ({
id: question.id,
type: question.type,
label: question.label,
required: question.required,
position: question.position,
options: question.options ?? [],
})),
}
}
The public responder reads that snapshot. It never reads the draft questions.
What to check after the table exists
Open /forms/studio-intake/results. Jordan is there. Search [email protected]. One row.
Download the CSV. Open it. The email column is text. Paste =2+2 into notes on a new submit and export again. Excel shows '=2+2, not 4.
Edit the email label to Best email? Refresh the public form. It still says Where should I reply? until you publish. Jordan’s detail page still shows the old wording.
If old rows pick up the new label, answers are joining on question id against the live draft. Point them at the snapshot.
Failures the agent ships anyway
A wide-open POST
Client-side required is not a lock. Turn JavaScript off and submit. The server has to refuse a missing email. The honeypot and the 20-an-hour cap live there too.
All four questions on one page
Keep one question on screen, with back, next, and a progress mark. A long scroll is a survey, and that is a different product.
Live-editing questions someone already answered
Rename “Sometimes” to “Rarely” and every old row silently changes meaning. Freeze the published questions. The 6-hour mini-prompt “Edit the form without gaslighting old answers” is the fuller version of that snapshot.
Fetching the email host
An MX lookup on submit is slow and can be pointed at an address you did not mean to poke. Check shape. Send mail later, on the 6-hour row.
Stripe on the thank-you screen
Payment fields are the 2-week row. Delete the checkout session if it appears during scaffold.
Respondent accounts
People type a name and an email. Magic links for respondents are a second app.
A waitlist with a position number is a different job. That is GetWaitlist, $15–$50 a month, about 3 hours.
Deploy the share URL
The product pages put hosting this way: a focused replacement costs your build time plus close-to-zero hosting.
Goodform wants Astro, HTMX, and D1. Put it on a subdomain you already own, forms.flaviocopes.com. The AHA prompt points at Cloudflare D1 for the tables and Workers for the endpoints.
Attach the custom domain on the Worker. The share URL is the product, so forms.flaviocopes.com/studio-intake has to resolve on the public internet, not only on localhost.
Ship with the seed form published. Fill it from a phone on cellular, not the same Wi-Fi as your laptop. Confirm the results table shows that row after a refresh. Point one real intake at it for a week.
If you miss a jump or a webhook, those are the 6-hour row and the other mini-prompts on the card. Add them after a respondent asks.
Take a D1 backup, then cancel Typeform, or Jotform, or Youform, whichever one you pay for.
What to skip on purpose
Leave out the visual builder, payment fields, and anything that looks like an integrations gallery.
Logic jumps are the honest next feature, not a v1 requirement. Each rule says: if question X matches, jump to question Y or end. Jumps go forward. The Typeform walkthrough covers that 6-hour row.
Tally is $29–$89 a month, about 8 hours, because the product is a document-style block builder. If Tally is the logo on the invoice, this Typeform-shaped v1 still covers a name and an email.
SurveyMonkey is $39–$75 per person per month, about 4 hours for a question form, a table, and a CSV. Use that page for occasional surveys. Use this v1 for seven intake questions and a thank-you screen.
A booking page after the intake is a different job. That is the Calendly guide. Weekly hours and a slot lock, not four questions.
A page of links is the Linktree guide. A short URL in front of the share link is the link shortener guide.
More writeups land on the guides index. The audit is the catalog if you want a smaller first ship.
Start now
Go to Build your own Typeform. Copy the AHA prompt, or the stack you already write.
Tell the agent the first estimate only: one-question flow, response table, a CSV. Two hours.
Publish forms.flaviocopes.com/studio-intake. Point one real intake at it tonight.