Formweh Documentation
Open-source forms, waitlists, and surveys you run on your own Cloudflare account. Build a form or bring your own; every response lands in one inbox you own, on your own site. Cloudflare's free tier, one Worker, one D1 database, no build step.
Set it up
The fastest path is your coding agent. Tell Claude Code, Cursor, or any agent:
Read https://formweh.com/agent.md and set up Formweh on my own Cloudflare account. It has every step and check; ask me only for what it says to ask.
It hands you a one-time link at the end; you choose your password in your own browser. Or deploy it yourself:
One-click deploy
- Click Deploy to Cloudflare on the landing page or the repo. A free Cloudflare account is all you need.
- Cloudflare copies Formweh into a new repository on your own GitHub and keeps it in sync.
- Cloudflare creates your database, runs the migrations, and publishes the Worker. You never touch a command line.
- Open the Worker's
*.workers.devURL and choose your dashboard password on the setup screen. - Make your first form, or start from a template.
From the command line
git clone https://github.com/duskresearch/formweh.git
cd formweh && npm install
npx wrangler login
npm run db:create
npm run deploy
Paste the printed database_id into wrangler.jsonc under d1_databases, and remove the project's own routes block, before deploying. Then open the Worker URL and set your password.
Your own domain
Your dashboard and forms work on the workers.dev address right away. To put them on your own subdomain, like forms.yourbrand.com:
- Open your Worker, go to Settings, Domains and Routes, Add, Custom Domain, and add
forms.yourbrand.com. Cloudflare provisions DNS and SSL. - Everything then lives on that one host: your dashboard, your hosted form pages (
forms.yourbrand.com/f/your-slug), and the endpoints your own forms POST to.
If your domain is not on Cloudflare, you only need that one subdomain there. Your main site can stay wherever it is.
Features
- Build a form or survey in the builder, hosted on your own site, or bring your own markup and keep it.
- Templates: contact, waitlist, survey, RSVP, coming-soon, feedback.
- Waitlists with a referral loop: every signup gets a share link and a place in line; friends who join move them up.
- One inbox for every response, with read, spam, and export.
- Notifications by email through your own Cloudflare, plus Slack, Discord, and webhooks.
- Spam protection: Cloudflare Turnstile and a honeypot, filed aside rather than dropped.
- Own your data: every response lives in your own D1 database. Export CSV or pull the HTTP API.
- One password guards the dashboard. No accounts, no user table.
- Light and dark, with a toggle across the landing, dashboard, and hosted forms.
Bring your own form
Create a "bring your own" form and point your markup at the endpoint:
<form action="https://forms.yourbrand.com/f/contact" method="POST">
<input name="email" type="email" required />
<textarea name="message"></textarea>
<button>Send</button>
</form>
It also accepts JSON, so a React app can fetch() it and get { "ok": true } back:
await fetch("https://forms.yourbrand.com/f/contact", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ email, message }),
})
Notifications
Emailing your own verified address is free on any Cloudflare plan. To turn it on:
- In Settings, set your notify email.
- Verify it once in Cloudflare: your account, Email, Email Routing, Destination Addresses, add your address, click the link.
- Back in Formweh Settings, hit Send test email.
Autoresponders (auto-replying to whoever submits, from your own domain) use Cloudflare Email Sending, which needs the Workers Paid plan ($5/mo) and a sending domain onboarded in Cloudflare. Everything else stays free.
Spam protection
A honeypot works out of the box. For stronger protection, add free Turnstile keys in Settings, then turn on spam protection per form. Failed challenges are filed as spam rather than dropped, so a real visitor is never lost. Hosted forms need both Turnstile keys to show the widget; bring-your-own forms are only checked when they actually send a token.
HTTP API
A read API for pulling forms and responses into scripts. Off until you generate a token in Settings. Send it as a bearer token.
| Method | Path | Does |
|---|---|---|
GET | /api/v1/forms | List your forms |
GET | /api/v1/forms/:slug | Fetch one form |
GET | /api/v1/forms/:slug/submissions | List a form's responses |
Run it locally
git clone https://github.com/duskresearch/formweh.git
cd formweh && npm install
npm run db:migrate:local
npm run dev
Open http://localhost:8787.
Troubleshooting
- A "choose a password" screen keeps appearing: nobody has set the password yet. Set it there. Claim the instance right after deploying so nobody else can.
- Real submissions landing in the spam folder: Turnstile keys are set but the form never shows the widget. Hosted forms need both keys.
- "Database not initialised": run
npx wrangler d1 migrations apply formweh-db --remote. - "Exceeded D1's free tier daily limit": another D1 database on the same account spent the shared daily allowance. It resets at midnight UTC.
How it is built
Cloudflare Workers for the runtime, Hono for routing, D1 (SQLite) for storage, and Cloudflare Email Service and Turnstile for notifications and spam. No build step: server-rendered HTML with hand-written CSS, light and dark.
More
- The agent runbook: formweh.com/agent.md
- Source (MIT): github.com/duskresearch/formweh
- The lab: Dusk Research