| title | Easy Form | ||||||
|---|---|---|---|---|---|---|---|
| description | Self-hosted form and lead-capture backend with PostgreSQL storage, analytics, an HTMX admin dashboard, an MCP server for AI assistants, and optional email forwarding. Deploy on Railway in one click. | ||||||
| tags |
|
A self-hosted form and lead-capture backend with analytics, an HTMX admin dashboard and an MCP server. Two services (app + Postgres). Deploy on Railway in one click.
┌─────────────────────────────────────────────────┐
│ Your Website │
│ ┌───────────────────────────────────────────┐ │
│ │ Embedded Contact Form │ │
│ │ Name: [________] Email: [________] │ │
│ │ Message: [___________________________] │ │
│ │ [Send] │ │
│ └──────────┬────────────────────────────────┘ │
└─────────────┼────────────────────────────────────┘
│ POST /form/{slug}
▼
┌─────────────────────────────────────────────────┐
│ Railway (this app) │
│ ┌───────────────────┐ ┌──────────────────┐ │
│ │ FastAPI backend │────│ PostgreSQL │ │
│ │ forms · admin │ │ (asyncpg) │ │
│ │ analytics · /mcp │ └──────────────────┘ │
│ └─────────┬─────────┘ │
│ ▼ │
│ ┌────────────────┐ │
│ │ SendGrid / SMTP│ (optional) │
│ └────────────────┘ │
└─────────────────────────────────────────────────┘
▲ ▲
│ GET /admin │ POST /mcp (API key)
┌──────┴───────────┐ ┌──────┴───────────┐
│ Admin Dashboard │ │ AI assistant │
│ (HTMX-powered) │ │ (Claude, etc.) │
└──────────────────┘ └──────────────────┘
- Receive form submissions from your static site, blog, or any HTML page
- Block spam with an invisible honeypot field and per-IP rate limiting
- Store submissions in PostgreSQL for review
- Notify you via email (SendGrid or SMTP)
- Admin dashboard to view, search, and manage submissions (HTMX-powered, no page reloads)
- Analytics — trends, world map of where people filled in, per-field answer breakdowns and custom charts
- MCP server — let an AI assistant create forms and query leads and analytics
This template uses 2 services — both wired and ready on deploy:
- Form API — The FastAPI backend (this repository)
- PostgreSQL — Database for submissions and form definitions
DATABASE_URL is wired automatically with a reference variable. ADMIN_PASSWORD, SESSION_SECRET_KEY and MCP_API_KEY are generated on deploy.
See Eary Form in action on a real site: Contact Page
This Blogger blog embeds the form and accepts submissions live — no page reload, no third-party data sharing.
- A Railway account
- A PostgreSQL database (Railway can provision one automatically)
Click the "Deploy on Railway" button above. Railway will:
- Clone the repository
- Detect the Python/FastAPI project (via Nixpacks)
- Provision a PostgreSQL database
- Deploy the app
After deployment, set these variables in Railway:
| Variable | Required | Default | Description |
|---|---|---|---|
DATABASE_URL |
✅ | — | Postgres connection string (auto-set by Railway) |
ADMIN_PASSWORD |
✅ | — | Password for the admin dashboard |
CAPTCHA_MODE |
❌ | auto (template sets honeypot) |
Spam protection mode (honeypot, pow, off, auto, cap) |
FORM_RECIPIENT_EMAIL |
❌ | — | Where to forward submissions |
SENDGRID_API_KEY |
❌ | — | SendGrid API key for email |
SMTP_HOST |
❌ | — | SMTP host (fallback if SendGrid unset) |
SMTP_PORT |
❌ | 587 |
SMTP port |
SMTP_USER |
❌ | — | SMTP username |
SMTP_PASS |
❌ | — | SMTP password |
FROM_EMAIL |
❌ | noreply@contact-form.app |
Sender address |
RATE_LIMIT |
❌ | 10 |
Max submissions per IP per hour |
RATE_LIMIT_BACKEND |
❌ | memory |
memory or db |
MCP_API_KEY |
❌ | — | Enables the MCP server at /mcp (see below) |
PUBLIC_BASE_URL |
❌ | Railway domain | Base URL used in form links returned by MCP tools |
Note: When deployed via the template, all database and service URLs are pre-configured. Only set
ADMIN_PASSWORD(for the admin dashboard) and optional email settings.
Visit your app's URL (https://your-app.railway.app/) to see the embed snippet page with the correct endpoint URL.
Copy the HTML snippet from the embed page and paste it into your website:
<form action="https://your-app.railway.app/form/contact" method="post">
<input type="text" name="name" placeholder="Your Name" required>
<input type="email" name="email" placeholder="Your Email" required>
<textarea name="message" placeholder="Message" required></textarea>
<button type="submit">Send</button>
</form>For HTMX-powered submission (no page reload):
<form
hx-post="https://your-app.railway.app/form/contact"
hx-target="#form-response"
hx-swap="innerHTML"
>
<input type="text" name="name" placeholder="Your Name" required>
<input type="email" name="email" placeholder="Your Email" required>
<textarea name="message" placeholder="Message" required></textarea>
<button type="submit">Send</button>
</form>
<div id="form-response"></div>Access your admin dashboard at https://your-app.railway.app/admin
Features:
- Submissions — View all form submissions in a paginated table
- Search — Search across all submission data
- Read/unread — Toggle submission status
- Delete — Remove spam or old submissions
- CSV Export — Download all submissions as a CSV file
- Forms — Create and manage multiple form definitions
- Custom fields — Define your own form fields (text, email, textarea, tel, number, select, checkbox)
No CAPTCHA widget, no extra services:
| Layer | Method | Description |
|---|---|---|
| 1 | Honeypot | Hidden form field that bots fill in but humans don't see. Included in the hosted form and the embed snippet. |
| 2 | Rate limit | Per-IP cap on submissions (RATE_LIMIT, default 10). |
| 3 | PoW (optional) | Built-in proof-of-work challenge (SHA-256). Enable with CAPTCHA_MODE=pow. |
CAPTCHA_MODE accepts honeypot (template default), pow, off, or auto. If you run your own Cap server you can still point CAP_ENDPOINT / CAP_SECRET_KEY at it; it is no longer part of the Railway template.
POST /form/{slug}
Content-Type: application/x-www-form-urlencoded
Body: Form fields as defined in the form configuration.
Response (201):
{"status": "ok", "message": "Form submitted successfully"}GET /health
Response (200):
{"status": "healthy", "database": "connected"}GET /pow-challenge?difficulty=4
Response:
{"secret": "abc123...", "nonce": 42, "difficulty": 4}The app includes a Model Context Protocol server at /mcp, so Claude or any MCP client can manage forms and leads in plain language — "create a lead form with name, work email and budget", "who filled the event form from India this week?", "ticket types per month for Q3".
- Set
MCP_API_KEYon the service (generate one withpython -c "import secrets; print(secrets.token_urlsafe(32))"). The endpoint returns 503 until it is set. - Connect a client to
https://your-app.railway.app/mcp(Streamable HTTP) with the headerAuthorization: Bearer <MCP_API_KEY>(orX-API-Key).
Claude Code:
claude mcp add --transport http easy-form https://your-app.railway.app/mcp --header "Authorization: Bearer YOUR_KEY"| Tool | What it does |
|---|---|
list_forms, get_form |
Forms with fields, submission/unread counts and public URL |
create_form, update_form |
Build or edit forms (text, email, textarea, select, checkbox, tel, number, url) |
delete_form |
Delete a form and its submissions (requires confirm=true) |
list_submissions, get_submission |
Who filled a form — filter by form, text search, unread, country, date range |
mark_submissions_read, delete_submission |
Triage leads (delete requires confirm=true) |
get_analytics |
Totals vs previous period, trend, weekday/hour, countries, per-field answer breakdowns |
build_chart |
Cross-tabulate fields by day/week/month/weekday/hour/country, count/sum/avg |
See examples/blogger-embed.html for a complete Blogger-compatible form with AJAX submission (no HTMX needed — Blogger doesn't support it).
# Install dependencies
pip install -r requirements.txt
# Copy env file
cp .env.sample .env
# Edit .env with your DATABASE_URL and ADMIN_PASSWORD
# Run locally
uvicorn app.main:app --reload --port 8000MIT