An open-source form builder that optimizes for revenue, not submissions.
Your form isn't the endpoint. The closed deal is.
Every form builder on the market reports the same number: completion rate. But nobody is paid on completion rate. You get paid on deals that close — and the two regularly point in opposite directions. A form variant that converts 40% better can quietly produce worse leads, and every tool in the category will congratulate you for it.
Worse, completion rate cannot tell a buyer from a bot. Both submit. Both get counted. Both look like success right up until sales starts calling.
Endpoint Forms is built on a different premise: a submission is not an outcome, and the only honest way to judge a form is by what its submissions turned out to be worth.
Endpoint Forms was built end to end in the AI Marketing Masterclass — the category research, the naming and domain, the positioning, the site and its SEO, the emails and social, and then the product itself.
The reasoning is all in docs/, written down as decisions were made rather than reconstructed afterwards — including the research that falsified the first wedge we tried, and what each feature still cannot do. If you want to see how a thing like this actually gets built and marketed, that is the course.
1. Your form knows which door was used. Every Endpoint form publishes two surfaces from one definition: a human UI, and a machine-callable tool surface (MCP / WebMCP) an agent can submit against directly — no DOM scraping, no brittle selectors. Because we know which surface was used, every submission is stamped with provenance: Human, Agent, or Unverified. The mechanism that lets legitimate agents through is the same one that records that they came through it.
The two halves of that are not symmetric, and we are precise about it. Agent is structural — calling the tool surface is the declaration, so there is nothing to forge. Human is a judgement from request headers, and headers are set by the caller: plain curl with copied Chrome headers is stamped Human. We have measured this, written it down, and asserted the failure in the test suite on purpose.
Endpoint does not detect bots and we never claim it does. What it does is separate automation that did not try to look like a browser — which is most commodity form spam — from browser sessions, give real agents a real door, and store its reasons so the judgement is auditable a year later. The full model, with the numbers: docs/27-provenance.md.
This matters more every month. Automated requests are now roughly 57% of HTML traffic, bad bots were 40% of internet traffic in 2025, and around 30% of leads bought from third-party vendors are outright fake — all of it arriving in your dashboard labeled "conversion."
2. The form learns from what happened next. Submissions carry a downstream outcome — won, lost, disqualified, and a value — synced from your CRM or posted to an outcome webhook. Variants rank on quality-adjusted conversion rate, so the variant that produces pipeline wins even when it produces fewer submissions.
Plenty of good marketers already pipe outcomes back to their ad platform. That loop teaches the ad platform who to target and teaches the form nothing. Endpoint closes the other half: which variant, which question, which field.
Pre-launch, but built. Not announced yet — the product itself is running.
The submission endpoint, the agent-callable surface, the form builder, conditional logic, outcome tracking, Yield, split tests scored on outcomes, spam defenses, destinations with signed delivery and retries, and one-command self-host are all shipped and tested. What is not done is the commercial side: there is no billing, and email delivery needs a Resend key that is deliberately unset, so an email destination says so rather than failing quietly.
Positioning and brand are settled and live in docs/, along with the decisions
behind each feature and — deliberately — what each one still cannot do.
Needs Node 22+ and Docker. Nothing else.
git clone https://github.com/coreyhaines31/endpointforms.git
cd endpointforms
bash scripts/setup.sh
npm run devThen open http://localhost:3000 and sign up at /signup.
scripts/setup.sh installs dependencies, generates the four secrets, starts Postgres, applies the migrations, seeds a sample workspace, and verifies that the database role the app connects as cannot bypass row-level security — which is the one mistake that switches off tenant isolation while every test still passes.
It is idempotent. --no-seed for an empty database, --dev to start the server when it finishes, --help for the rest.
If you run several projects at once, portless avoids port collisions:
npx portless endpointforms npm run dev
# → http://endpointforms.localhost:1355Full guide, including a hosted Postgres, production notes, and an honest account of what self-hosting does not give you: docs/24-self-hosting.md.
scripts/setup.sh generates all four secrets, so for local development you can skip this entirely. It matters when you deploy.
# generate any of the secrets below
node -e "console.log(require('node:crypto').randomBytes(32).toString('base64url'))"Required in production
| Variable | What it does | If unset |
|---|---|---|
DATABASE_URL |
Postgres connection. The role must not be a superuser and must not have BYPASSRLS — a superuser silently ignores FORCE ROW LEVEL SECURITY, which switches off tenant isolation while every test still passes |
The app refuses to start rather than falling back to a dev database |
AUTH_SECRET |
Signs session cookies. Read by Auth.js, and — unless UPLOAD_LINK_SECRET is set — the file download links for #66 |
Auth.js throws, and file uploads are refused rather than accepted with no way to hand them back |
SUBMISSION_IP_SALT |
Salts the hash of submitter IPs. The raw IP is never stored | Falls back to a built-in constant and warns — hashes become guessable |
ORIGIN_TOKEN_SECRET |
Signs the client token, one of nine form-surface origin signals | Falls back to a built-in key and warns |
VERDICT_API_KEY_SECRET |
Signs the outcome API keys. The key is derived, not stored — an HMAC over the workspace id, so there is no key table to leak | POST /api/v1/verdict answers 503. This one refuses rather than degrading, because it guards writes into someone else's data |
Set before building — both are baked into the client bundle
| Variable | Default |
|---|---|
NEXT_PUBLIC_SITE_URL |
https://endpointforms.com |
NEXT_PUBLIC_RENDER_DOMAIN |
endpointforms.app — the separate registrable domain customer forms are served from |
Optional — VERDICT_API_KEY_SECRET_PREVIOUS (still accepted on verify, so a rotation does not break live integrations), VERDICT_DEFAULT_CURRENCY, AUTH_GOOGLE_ID / AUTH_GOOGLE_SECRET, AUTH_EMAIL_FROM, RESEND_API_KEY / MAIL_FROM, ENDPOINT_DEFAULT_THANKS_URL, ALLOW_INSECURE_DESTINATIONS, ALLOW_PRIVATE_DESTINATIONS, DATABASE_POOL_MAX, DB_TARGET / NEON_DEV_DATABASE_URL, the UPLOAD_* caps and retention (docs/24 §3.6a), the INGEST_RATE_LIMIT_* / VERDICT_RATE_LIMIT_* / AUTH_RATE_LIMIT_* limits, and NEXT_PUBLIC_WAITLIST_ENDPOINT_URL / WAITLIST_ENDPOINT_URL.
Every one of them, with defaults and what happens when you leave it out: docs/24-self-hosting.md §3.
One thing worth reading before you build an outcome integration: because the verdict key is derived rather than stored, rotation is fleet-wide — revoking one workspace's key rotates everyone's — and renaming a workspace invalidates its key. Both are explained in §3.1.
| Self-hosting | Run your own instance |
| The Manifest specification | The MCP surface every form publishes — the public protocol |
| HTTP API | The submission endpoint and POST /api/v1/verdict |
| Provenance | What Human · Agent · Unverified means, and what it does not |
| Origin findings | The adversarial write-up behind that model |
| Contributing |
Endpoint Forms is licensed under the GNU AGPL v3.
The core is, and will remain, open source and self-hostable. A managed hosted version will be offered for teams who would rather not run it themselves. The agent-facing capture spec in particular is meant to be copied — a standard is only useful if other people implement it.
| Framework | Next.js 16 (App Router) + React 19 |
| Language | TypeScript |
| Styling | Tailwind CSS v4 + shadcn/ui |
| Database | Postgres, Drizzle ORM, row-level security for tenant isolation |
| Auth | Auth.js — email and password, Google, magic link |
| Hosting | Vercel (hosted), anywhere that runs Node and Postgres (self-hosted) |
npm run verifyOne command: lint, typecheck, build and tests, each judged by exit code, with a summary that cannot be misread. next build prints "Compiled successfully" before it type checks, so grepping its output reports a passing build for one that fails — this exists because that cost us hours.
CI runs the same checks on every push, and builds with no DATABASE_URL and no secrets, so a green run proves a stranger can clone the repo and build it. Vercel's check only proves it builds with Vercel's environment variables set.
Outside pull requests are not being merged yet — the foundations are moving too fast for that to be a good use of your time. Issues, self-hosting reports, and Manifest implementations are very welcome. See CONTRIBUTING.md.