dsforms

Self-hosted form backend for static sites — one Go binary, one SQLite file, with spam quarantine, waitlists, webhooks, and email notifications.

6stars
GNU AGPLv3

Repository Health

Pre-computed score based on development activity, maintenance, community, maturity, and trend momentum.How we score it →
46/100Fair
Development Activity100
Maintenance52
Community8
Maturity24
Momentum0

Technical Analysis

AI-assessed by reading the actual repository — architecture, code quality, innovation, and documentation.How we score it →
86/100Excellent
Architecture92
Code Quality93
Innovation75
Learning Curve85

dsforms is a self-hosted backend for HTML forms on static sites. Point a <form action> at it and every submission lands in a single SQLite file on your own server — no per-submission fee, no vendor, no Redis or Postgres to run alongside it. It ships as one ~20MB Docker image with a health check built in, aimed squarely at the Hugo/Astro/Jekyll/hand-written-HTML crowd who don’t want their contact-form data living on someone else’s infrastructure.

Submissions aren’t just accepted or dropped — they’re scored against markup, SQL-injection probes, keyword lists, repeat-IP activity, and synthetic tokens, then held in a quarantine queue with the exact breakdown that triggered it, so a false positive is one click away instead of gone for good. Operators can layer allow/block rules on top (email, domain, IP, or CIDR — allow always wins), route notifications to SMTP, Slack, Discord, or a generic webhook, and run confirmation-email waitlists with restart-safe broadcast sends.

The admin UI covers search, bulk actions, CSV export, and backup/restore with a rollback path if a restore fails midway. Security defaults are conservative rather than configurable-but-off: SHA-256-hashed session tokens, bcrypt at cost 12, HMAC-signed flash cookies, per-IP rate limiting, login lockout, and a real CSP — with no CAPTCHA in the mix. It’s licensed AGPL-3.0 as of the release after v0.5.2 (earlier releases stay MIT), which only matters if you turn dsforms itself into a hosted service for others.

What You Get

  • A drop-in HTML form endpoint — paste one action URL, no JS SDK or client library required
  • Spam quarantine with a visible scoring breakdown (markup, SQL probes, keywords, gibberish, repeat-IP, synthetic tokens) instead of a silent bin
  • Operator-defined filter rules — block or allow by email, domain, IP, or CIDR, with allow always overriding block
  • Waitlist support — unique signups by email, position tracking, confirmation emails, and a restart-safe broadcast for launch day
  • Email and webhook notifications on every submission — SMTP plus Slack, Discord, or generic JSON
  • An admin UI with search, bulk actions, CSV export, and a submission reading drawer
  • Backup and restore from the UI, with automatic rollback if a restore fails partway through
  • Honeypot fields, per-IP rate limiting, and login lockout built in — no CAPTCHA dependency

Common Use Cases

  • Contact and inquiry forms on a Hugo, Astro, Jekyll, or hand-written static marketing site
  • Pre-launch waitlists that need confirmation emails and ordered signup positions without a SaaS subscription
  • Agencies or freelancers self-hosting one instance to serve form submissions for multiple client sites
  • Privacy-conscious teams that want form data to never leave infrastructure they control
  • Replacing a per-submission-billed hosted form service (Formspree, Basin, Getform) with a fixed-cost VPS

Under The Hood

Architecture dsforms wires dependencies explicitly in main.go: every handler declares a narrow interface for exactly the store methods it calls (handler.QuarantineStore, handler.WaitlistStore, handler.SubmitStore, and a dozen more), and main.go pins compile-time assertions (var _ handler.AdminStore = (*store.Store)(nil)) so a renamed store method fails the build naming the interface it broke, rather than surfacing as a runtime struct-literal error. The spam-scoring logic went further: after three separate review rounds each found a filter bypass in the seam between the HTTP handler, the store, and the matcher, the decision logic was consolidated into internal/screen with its implementation moved under internal/screen/internal — a path Go’s own visibility rules forbid any other package from importing, making the three-way split structurally impossible to recreate rather than just discouraged by convention.

Tech Stack Go 1.25 on go-chi/chi for routing, modernc.org/sqlite (a pure-Go, cgo-free SQLite driver) for storage, and golang.org/x/crypto for bcrypt hashing — a deliberately small dependency set with no Redis, Postgres, or ORM. The frontend is vanilla JS and CSS (static/app.js, static/app.css) with a vendored Inter font, embedded into the binary via go:embed rather than fetched from a CDN, which also keeps the app compatible with its own default-src 'self' Content-Security-Policy. Templates are Go’s html/template, parsed once at startup and cloned per page to avoid content-block name collisions across the shared template namespace.

Code Quality The test suite runs 570+ Test* functions across unit, integration, golden-page, wiring, and fuzz tests, including a fuzz target in internal/screen. GitHub Actions runs the full suite on every push to main and every pull request, with an explicit comment in the workflow file noting these guards previously only ran when someone remembered to run them locally — until CI was added, ‘a guard nobody runs is the same as no guard.’ Error handling favors named sentinels (store.ErrNotFound = sql.ErrNoRows, documented as a store-owned contract rather than a private arrangement between two concrete types) over swallowed errors, and comments throughout explain why a design choice was made, often citing the specific bug or review finding that prompted it.

What Makes It Unique Most open-source form tools double as form builders — dsforms deliberately stays a backend only, with no drag-and-drop form designer, matching its stated goal of being ‘the boring alternative you run yourself.’ The spam-quarantine model (hold with a visible, tunable score breakdown, rather than a binary accept/reject) and the single-binary-plus-SQLite deployment story — explicitly avoiding Redis or Postgres — are its clearest points of difference from both hosted competitors and heavier self-hosted alternatives.

Self-Hosting

Licensing Model AGPL-3.0 licensed as of the release following v0.5.2 (earlier releases remain available under MIT) — every feature is available in self-hosted deployments with no license keys or paid tiers. The AGPL obligation only applies if you modify dsforms and offer it as a network service to others; running it privately or for clients carries no such requirement.

Self-Hosting Restrictions None found — no ee/, enterprise/, or pro/ directories, and no license-gating code in the source.

License Key Required No.

Join founders buildingwith open source

Opinionated takes, migration guides, cost-saving tips, and insights from the open source ecosystem.

Subscribe on Substack
Join 750+ subscribers

Search