Reliable form ingestion and webhook delivery for developers.
Every project with a contact form, a waitlist, or a feedback box ends up needing the same small backend: something that accepts an HTML form POST, validates it, stores it, and forwards it somewhere useful. Writing that once is easy. Running it reliably, with retries, delivery logs and an audit trail, is not.
Hymical Forms is that backend, self-hostable and open source. Point an HTML
form's action at it and every submission is validated, stored, and delivered to
your webhook with an HMAC signature and a bounded retry schedule. The submission
and the obligation to deliver it are written in one database transaction, so a
crash cannot lose work the service already acknowledged.
- Public form ingestion that works from a plain
<form action="...">, with explicit validation and request limits - Idempotent submission retries through an
Idempotency-Keyheader - Durable transactional outbox in PostgreSQL, so a
202means the delivery is already promised - Signed webhook delivery, HMAC-SHA256 over the raw body
- A separate retrying worker with leases, exponential backoff and crash recovery
- Management API keys, created by an operator CLI and stored only as digests
- Endpoint and delivery operations: reconfigure, inspect attempt history, replay a failed delivery
- Submission retrieval and export: browse, filter by endpoint and time, read one back, export a filtered range as JSON or CSV
- Retention cleanup driven by an operator command, which never deletes a submission a delivery could still need
- Distributed rate limiting on public ingestion, per source address and per endpoint, shared across API processes
- Alembic migrations with a startup revision check and model drift tests
- Real PostgreSQL integration testing, including genuine concurrency
flowchart TD
Form["Browser / HTML form"] -->|"POST /f/{endpoint_id}, public"| API["FastAPI API"]
Operator["Operator"] -->|"authenticated management API"| API
API -->|"submission + delivery job, one transaction"| DB[("PostgreSQL")]
DB --> Worker["Delivery worker"]
Worker -->|"HMAC signed webhook"| Receiver["Developer endpoint"]
Worker --> DB
The API stores the submission and its delivery job in one transaction and never makes an outbound request. The worker claims owed deliveries, sends them, and retries the ones that fail. PostgreSQL is the queue: there is no broker and no Redis.
See Architecture.
git clone https://github.com/hymical/forms.git && cd forms
python -m venv .venv && . .venv/bin/activate && pip install -e ".[dev]"export FORMS_DATABASE_URL=postgresql+psycopg://forms:forms@localhost:5432/forms
alembic upgrade headpython -m hymical_forms.cli create-key --name local-adminSave the key it prints. It is shown once and stored only as a digest.
uvicorn hymical_forms.main:app --reload # terminal one
python -m hymical_forms.worker # terminal twoRegister an endpoint, then submit to it:
curl -X POST http://127.0.0.1:8000/endpoints \
-H "Authorization: Bearer $HYMICAL_KEY" \
-H 'Content-Type: application/json' \
-d '{"id": "contact-form", "name": "Contact form",
"webhook_url": "https://example.com/hooks/forms"}'curl -i -X POST http://127.0.0.1:8000/f/contact-form \
-d email=dev@example.com -d message=hello{
"submission_id": "sub_48984534f33749c49a88de2d59400dce",
"endpoint_id": "contact-form",
"received_at": "2026-08-24T14:34:27.651841Z",
"field_count": 2,
"idempotent_replay": false,
"delivery": { "queued": true }
}Submitting needs no credential: that URL goes straight in an HTML form.
Full walkthrough: Getting Started.
| Section | Covers |
|---|---|
| Getting Started | Install, configure, migrate, first submission |
| Guides | Ingestion, idempotency, webhooks, rate limiting, endpoints, replay, submissions, export |
| API Reference | Every route, its parameters and responses, and the complete error table |
| Operations | Worker, migrations, retention, reverse proxy, every configuration variable |
| Architecture | Transactional outbox, delivery semantics, concurrency, security |
| Data handling | Where submitted values go, and where they never go |
| Limitations | An honest list of what this build does not do yet |
Early development, under active development. Everything below is implemented and covered by tests, including a PostgreSQL suite that exercises real concurrency.
| Capability | Status |
|---|---|
| Form ingestion, validation and request limits | Implemented |
| Idempotent submission retries | Implemented |
| Submission persistence and durable delivery queue | Implemented |
| Signed webhook delivery with retries and backoff | Implemented |
| Management API keys and authentication | Implemented |
| Endpoint management | Implemented |
| Delivery inspection and manual replay | Implemented |
| Public ingestion rate limiting | Implemented |
| Schema migrations | Implemented |
| Submission retrieval and filtering | Implemented |
| Submission export, JSON and CSV | Implemented |
| Retention cleanup, operator-run | Implemented |
| Scheduled retention | Not implemented |
| Submission search | Not implemented |
| Endpoint deletion | Not implemented |
| Spam handling, CAPTCHA | Not implemented |
| Dashboards | Not implemented |
Four things are worth knowing before you deploy it:
- Delivery is at-least-once, not exactly-once. Deduplicate on the submission
idin the signed payload. - Rate limiting is traffic protection, not spam protection. It bounds volume and has no opinion about content.
- SSRF protection is partial. Webhook hostnames are not resolved.
- Nothing is deleted until you delete it. Retention is a command an operator runs, and it never removes a submission a delivery could still need.
The full list is in Limitations.
pytest # run the test suite
ruff check . # lint
ruff format --check . # formatting check
mypy # type checkpytest needs no services: the fast suite runs on in-memory SQLite. A smaller
suite runs against a real PostgreSQL database for locking, constraints and
concurrency, and skips itself unless you point it at one:
export HYMICAL_TEST_POSTGRES_URL=postgresql+psycopg://forms:forms@localhost:5432/forms_test
pytest tests/integration -m postgresSee Testing and Contributing.
