Skip to content

About

Self-hosted, branded forms with your own database. A Typeform alternative you control.

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Repository files navigation

Builders Forms

Self-hosted, branded forms with your own database. Built as a replacement for Typeform when you want the responses to live somewhere you control.

  • Form builder — short and long text, email, multiple choice, checkboxes, dropdowns, yes/no, file upload, and section breaks
  • Branding per form — colours, logo, header image, fonts, button style, corner radius, with a live preview
  • Rich text — bold, headings and lists in your intro and question text, edited visually
  • Workspaces — group forms and give each group its own password, so people only see their own forms
  • Responses — dashboard and CSV export, each response labelled by the person who filled it in
  • Notifications — get a message the moment someone submits. Point it at a Slack webhook and it posts to your channel; point it at Zapier, Make or n8n and route submissions anywhere. One webhook per workspace covers every form in it, and past responses can be re-sent
  • Files — uploads land in your own storage

MIT licensed. Made by Builders Studio.


Deploy it in about 3 minutes

Deploy with Vercel

  1. Click the button. Vercel copies the repo and asks for two values:
    • ADMIN_PASSWORD — the password you'll log in with
    • ENCRYPTION_KEY — any long random string (openssl rand -base64 32)
  2. When the first deploy finishes, open the project on Vercel → Storage → Create Database → Neon (Postgres) and connect it to the project. Leave the "Custom Prefix" field empty.
  3. Optional, only if your forms accept file uploads: Storage → Create → Blob, connected to the same project.
  4. Redeploy (Deployments → ⋯ → Redeploy). The tables are created for you during the deploy.

Log in with your ADMIN_PASSWORD and create your first form.

What you need

  • A Vercel account (the free plan is enough), or any Node host
  • A Postgres database — Neon's free plan works, and Vercel sets it up for you
  • Optionally a Blob store for file uploads, and a Slack/Zapier webhook for notifications
  • Nothing else. No terminal needed for the steps above.

To develop locally you also need Node 24.

Run it locally

npm install
cp .env.example .env          # then set ADMIN_PASSWORD and ENCRYPTION_KEY
npm run db:dev &              # throwaway local Postgres, nothing to install
npm run db:push               # create the tables
npm run dev

The last command prints the URL to open:

  Builders Forms  ->  http://localhost:31847

How you know it worked: that URL loads a page with a single password box. Enter your ADMIN_PASSWORD and you land on an empty forms list with a New form button. That is a working install — no forms yet, which is correct on a fresh database.

The port is picked at random above 20000 on first run and remembered in .dev-port, so the URL stays the same next time. Port 3000 is shared by almost every local project, and browsers send localhost cookies across all ports — enough of them and every request fails with a 431. Staying off 3000 avoids the whole class of problem. Want a specific port? PORT=3000 npm run dev.

If something goes wrong

What you see What it means
ECONNREFUSED ... 5434 The database isn't running. Start it with npm run db:dev.
431 Request Header Fields Too Large Cookie overflow from other localhost projects. Already handled by .npmrc; if it persists, clear localhost cookies.
ENCRYPTION_KEY is not set Add it to .env. Set it once and never change it — rotating it makes stored workspace passwords unreadable.
Login says "Wrong password" .env isn't loaded. Confirm the file is named .env and restart npm run dev.

Database

Any Postgres works — the app only ever needs a DATABASE_URL. It connects with drizzle-orm over node-postgres, so there is no provider SDK to swap.

Provider Notes
Neon Recommended. Plain serverless Postgres, no public API surface. vercel integration add neon sets DATABASE_URL for you.
Supabase Fully supported. See the lockdown note below. vercel integration add supabase.
Anything else Aurora, RDS, Railway, or your own box. Paste the connection string.
Local npm run db:dev — embedded Postgres 18 (PGlite), nothing to install.

The Supabase lockdown

Supabase serves a public Data API at https://<project>.supabase.co/rest/v1 using the anon key, which is meant to be public. Tables in the public schema with no row-level security are readable and writable through it, which would walk straight past this app's login.

This app never uses that API — every query runs server-side over DATABASE_URL. So drizzle/harden.sql shuts the door:

  1. Revokes every anon / authenticated privilege on tables, sequences, routines and the schema itself.
  2. Turns off OpenAPI discovery, so /rest/v1/ stops listing your tables.
  3. Sets default privileges, so a future table can't quietly re-open the hole.
  4. Enables row-level security on all seven tables with no permissive policies — deny by default for anyone who is not the table owner.

It runs automatically as part of npm run build, is idempotent, and detects the provider: on Neon or plain Postgres the anon role doesn't exist, so steps 1–3 are skipped and only RLS is applied. The same file is correct everywhere.

Adapted from vsi-conversation-core's security_lockdown.sql, which solved this for the same reason: all access through one trusted role, PostgREST unused.

A note on isolation

Workspace isolation is enforced in application code, not by RLS policies. The browser never touches the database; every query runs in a server action behind a requireFormAccess / requireFolderAccess / requireMaster check. RLS here is a backstop against the Data API, not the primary control. tests/isolation.test.ts pins the behaviour so a forgotten check fails CI.

How access works

There is one login page, and the password decides what you see.

Password Sees
ADMIN_PASSWORD Everything: all workspaces, forms and responses
A team password The same as above. Create these under Team
A workspace password Only that workspace's forms. Create these under Workspaces

Workspace and team passwords are generated for you, encrypted in the database, and can be revoked at any time — the person is logged out immediately.

Nobody sees a workspace they don't have the password for, or even learns that it exists.

Sharing a form

Every form has one unguessable link, created with the form. Publish the form, copy the link from the forms list, and send it. Duplicate a form to make a tweaked version for a different customer; duplicates group into a folder.

Notifications

Set one webhook per workspace (Notifications, next to New form). Every submission in that workspace posts to it with the form name, who it was for, and the answers. Slack incoming webhooks work out of the box, as do Zapier, Make and n8n. Past responses can be re-sent from the Responses tab.

Security

  • Login locks out for 15 minutes after 8 wrong attempts
  • Passwords are encrypted at rest, and compared in constant time
  • Sessions are tied to the password, so revoking one ends the session
  • Admin pages are excluded from search engines
  • Webhook URLs are checked so they can't be pointed at internal addresses
  • Admin endpoints reject cross-site requests
  • Submissions are rate limited

There is no two-factor authentication — a password alone is enough to log in. Treat ADMIN_PASSWORD accordingly.

Built with

Next.js, Tailwind CSS, Drizzle ORM, Postgres. Deploys to Vercel; any Node host with a Postgres database works too.

About

Self-hosted, branded forms with your own database. A Typeform alternative you control.

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages