Manuals / Forms gateway
Deploy a form
Put a working contact form on any site in one command. The gateway handles spam protection, storage and email notification — you just declare the form. New to terminals? Try the step-by-step walkthrough with a form builder that writes the command for you.
How it works
One gateway serves every site. A form is a definition stored in a database — not code — so adding or changing one is a data write that takes effect immediately, with no redeploy.
A page posts its fields to https://forms.flowsmith.online/f/<site>/<form>. The gateway validates, stores the submission, and emails a notification to the form's recipient. You define the form once; the markup to paste into the page is generated for you.
Authentication
Managing forms is done with a service token sent as a header:
Authorization: Bearer <FORMS_ADMIN_TOKEN>
CLI — onboard-form.mjs
The script wraps the API so you never touch raw HTTP. It needs Node 18+ and the repo it ships in. Don't have Node yet? See Set up your computer. (No tooling at all? The step-by-step guide does the same job with a single curl line — nothing to install.)
It reads two environment variables:
| Variable | Required | Meaning |
|---|---|---|
FORMS_ADMIN_TOKEN | required | Your service token. |
FORMS_BASE | optional | Gateway base URL. Default https://forms.flowsmith.online. |
Commands
| Command | What it does |
|---|---|
list | List all form definitions. |
get <site> <form> | Show one definition. |
put <site> <form> [flags] | Create or update a form (idempotent). |
snippet <site> <form> | Print the ready-to-paste HTML for the page. |
delete <site> <form> [--hard] | Deactivate a form (--hard removes it entirely). |
Flags for put
| Flag | Required | Meaning |
|---|---|---|
--recipient <email> | required | Where notifications go. Must be a verified destination in our email routing. |
--origins <a,b> | optional | Comma-separated list of sites allowed to post (apex + www). CORS allowlist. |
--require <a,b,c> | optional | Comma-separated required field names (e.g. name,email,message). |
--subject "…" | optional | Subject line of the notification email. |
--label "…" | optional | Human-readable name shown in the admin. |
--sender "Name <from@…>" | optional | From override. Defaults to the gateway sender. |
--no-turnstile | optional | Disable the Turnstile check (integration testing only). |
Example — onboard a whole site in one line
FORMS_ADMIN_TOKEN=… node scripts/onboard-form.mjs put madhouse contact \
--recipient [email protected] \
--origins https://madhouse.vip,https://www.madhouse.vip \
--require name,email,message
Then get the markup to paste into the page:
FORMS_ADMIN_TOKEN=… node scripts/onboard-form.mjs snippet madhouse contact
HTTP API
If you'd rather call it directly (or from an AI agent), the endpoints under /admin/api/forms mirror the CLI. All require the Authorization: Bearer header.
| Method | Path | Purpose |
|---|---|---|
| GET | /admin/api/forms | List all definitions |
| GET | /admin/api/forms/{site}/{form} | One definition |
| PUT | /admin/api/forms/{site}/{form} | Create or update (idempotent) |
| DELETE | /admin/api/forms/{site}/{form} | Deactivate (?hard=1 to remove) |
| GET | /admin/api/forms/{site}/{form}/snippet | Ready-to-paste HTML |
site and form are slugs: lowercase letters, digits and hyphens.
PUT body
{
"recipient": "[email protected]",
"origins": ["https://madhouse.vip", "https://www.madhouse.vip"],
"subject": "New MADHOUSE enquiry",
"label": "MADHOUSE — Contact",
"require_turnstile": true,
"active": true,
"schema": { "fields": [
{ "name": "name", "required": true },
{ "name": "email", "required": true },
{ "name": "message", "required": true }
] }
}
Response: { "ok": true, "created": true|false, "site": "…", "form": "…" }
Form fields
Any field your form posts is stored and shown in the admin. A few names are special:
| Field | Meaning |
|---|---|
email | Validated as an email; used as the Reply-To so you can reply straight from your inbox. |
message | Rendered as a textarea in the generated snippet. |
company_website | The honeypot — keep it in the markup, hidden, always empty. Bots fill it; real users don't. |
_redirect | Optional hidden field; for no-JS posts the gateway redirects here on success. |
Mark fields required either with --require (CLI) or the schema body. Required fields are enforced both in the browser and on the server.
Onboarding recipe
- Define the form —
put <site> <form> --recipient … --origins …. It's live immediately. - Get the markup —
snippet <site> <form>. Paste the two script tags once per page and the form where it should appear. - Allowed origins — the
--originsyou set are the CORS allowlist; include apex and www. - Turnstile hostname — add the site's hostname to the kh-forms Turnstile widget (currently a one-off manual step in Cloudflare).
Two common patterns
One form, many pages
Define a form once and paste the same snippet on as many pages as you like — home, about, footer. They all post to the same /f/<site>/<form>, land in one inbox and group together in the admin. Just list every hostname those pages live on in origins (apex + www, and any subdomain).
A different form for one section
When a section needs its own structure — say an academy with different fields or a different recipient — define a second form under the same site with its own slug:
node scripts/onboard-form.mjs put photorobot academy \
--recipient [email protected] \
--origins https://photorobot.com,https://academy.photorobot.com \
--require name,email,course,message
A site can have any number of forms; each has its own fields, recipient and snippet. If the section lives on a different subdomain, add that subdomain to origins.
What you get for free
Every form on the gateway is protected the same way, without you configuring anything:
- Cloudflare Turnstile — invisible bot challenge on each submission.
- Honeypot — a hidden field that silently drops bot submissions.
- Rate limiting — per-IP, across all forms, to blunt floods.
- Origin allowlist — only your listed sites may post.
- Required-field validation — enforced in the browser and again on the server.
- Stored + emailed — every submission lands in the database and as an email; delivery status is recorded.