Skip to content

Repository files navigation

Hymical Forms logo

Reliable form ingestion and webhook delivery for developers.

Documentation CI Python 3.11+ Apache License 2.0 FastAPI PostgreSQL

Read the documentation

Overview

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.

Key features

  • Public form ingestion that works from a plain <form action="...">, with explicit validation and request limits
  • Idempotent submission retries through an Idempotency-Key header
  • Durable transactional outbox in PostgreSQL, so a 202 means 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

Architecture

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
Loading

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.

Quick start

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 head
python -m hymical_forms.cli create-key --name local-admin

Save 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 two

Register 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.

Documentation

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

Project status

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 id in 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.

Development

pytest                    # run the test suite
ruff check .              # lint
ruff format --check .     # formatting check
mypy                      # type check

pytest 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 postgres

See Testing and Contributing.

License

Apache License 2.0.

About

Reliable form submission infrastructure with validation, storage, webhooks, retries, and delivery tracking

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages