No JavaScript on your site. No account. No third party between your visitor and your server.
Name it, give it an address to notify, and copy the endpoint. About twenty seconds.
A plain action attribute. Works on Netlify, Pages,
S3, or a file on a USB stick.
Email, a Slack webhook, CSV export, or the admin's own searchable inbox.
A weighted filter with no configuration to get wrong. Anything it holds stays reviewable for thirty days, with the exact reason attached — because the expensive failure is not spam getting through, it is a real customer disappearing.
Link markup, injection probes, keyword pile-ups, synthetic-looking text and repeat-IP bursts — each weighted, never one signal deciding alone.
Every held submission keeps its breakdown: which rule, which field, which characters, worth how many points.
Restore puts it back in the inbox as unread and sends the notification that was withheld.
Block or allow an address, a domain, an IP or a CIDR range. Allow always wins.
message · matchedmessage · matched{
"form_id": "4b91c0",
"form_name": "Contact",
"submitted_at": "2026-09-09T12:04:11Z",
"data": {
"name": "Jane Doe",
"email": "jane@example.com",
"message": "Loved the article."
}
}
$ curl -X POST https://forms.you.dev/w/launch \ -d "email=jane@example.com" { "success": true, "position": 2481, "already_joined": false }
Signing up twice returns the original position instead of creating a second row.
Handed out in signup order, and shown back so you can put them in a confirmation email.
A throttled queue with per-recipient retries. Restarting the server resumes it.
A dark, compact dashboard: what arrived, what is unread, what got held, and why. Server-rendered — no JavaScript framework anywhere in it.
Submissions over time, spam rate, per-form breakdown and rate-limit activity.
Click a row and it opens over the list. Step through with the arrows without losing your place.
Everything the filter held, with its score breakdown and one click to put it back.
Block or allow addresses, domains, IPs and CIDR ranges. Add your own keywords.
Point an MCP client at /mcp and it can list what you have not read, read one, file spam, or ask what is in the database. Off until you turn it on.
{
"mcpServers": {
"dsforms": {
"type": "http",
"url": "https://forms.you.dev/mcp",
"headers": {
"Authorization": "Bearer dsf_…"
}
}
}
}
Shown once, stored as a hash, revocable one at a time. Scoped to read, write or delete — delete is its own scope, because a client that files spam should not also be able to erase it.
For clients that cannot carry a token, turn on OAuth: the client finds where to sign in, you approve it on a consent page that starts at read only, and it shows up under Connected apps until you disconnect it.
A bound token does not see the others at all — not refused, absent. Every listing is filtered in the query itself, and an id from another form answers exactly as an id that does not exist.
Chat-template markers and invisible Unicode are stripped from a submission before a client sees it, and the client is told what went — and told that the list of recognised markers is not exhaustive. What is stored never changes, and the admin flags it.
Refuses to start over plain http, since a bearer token rides every request — override it for localhost or a private network and it warns on every boot instead. Full setup, the tool list and what each scope costs: docs/mcp.md.
Sent asynchronously, so a slow SMTP server never delays the visitor.
Slack, Discord or generic JSON, testable from the form's settings page.
Throttled, retried per recipient, and resumed after a restart.
One daily summary of what was held — never one email per spam message.
Everything lives in it. Copy it and you have moved house.
A consistent snapshot while the server keeps running. API tokens, sessions and OAuth connections are stripped both ways.
SQLite FTS5 across every field. No external search service.
Every field, with formula injection neutralised on the way out.
Opaque tokens, hashed at rest. Changing a password ends every session.
Per-IP token bucket on the public endpoints, plus a login lockout.
A hidden input bots fill in and people never see.
bcrypt at cost 12, managed from the admin or the CLI.
Scoped bearer tokens or OAuth sign-in, limitable to single forms. Off unless you enable it.
Docker Compose with a published image, or go build
if you would rather. The container is roughly 20MB and needs one writable
volume for the database.
$ git clone https://github.com/barancezayirli/dsforms $ cd dsforms && cp .env.example .env # set SECRET_KEY and your SMTP details
$ docker compose up -d
# admin / admin — change it immediately $ open http://localhost:8080/admin