v0.2.0¶
The first release meant to be evaluated by someone other than its own author. This is not a claim that the service is production-ready. It is a maturity pass over what had already been built: version consistency, a landing-page README, one architecture diagram, an end-to-end demo, a security and operational review, and a documentation audit, all against the system as it actually stands.
What this release is¶
Reliable form ingestion and signed webhook delivery, self-hostable and PostgreSQL-backed.
- Form ingestion and validation. A public
POST /f/{endpoint_id}route that accepts a plain HTML form submission, with explicit limits on body size, field count, field name and value length. No credential, because the URL sits in a form'sactionattribute. - PostgreSQL persistence. Every submission is stored, together with the durable obligation to deliver it if its endpoint has a webhook.
- Idempotency. An
Idempotency-Keyheader makes a retried submission resolve to the original rather than storing the form twice, scoped per endpoint and enforced by a database constraint. - A transactional webhook outbox. The submission and its delivery job are
written in one database transaction. A
202means the delivery is already promised; a crash between accepting a form and queuing its delivery cannot happen. - Signed webhook delivery. HMAC-SHA256 over the exact bytes transmitted, a destination allow-list that rejects loopback, private, link-local and multicast literals, and no redirects followed.
- A separate worker, with leases, exponential backoff and crash recovery.
It claims due deliveries with
SELECT ... FOR UPDATE SKIP LOCKEDon PostgreSQL, so any number of workers can run against the same queue without coordinating. - Management API keys, minted by an operator CLI and stored only as a SHA-256 digest. The credential exists in the process for exactly as long as the request that carried it.
- Endpoint and delivery management. Create, list, inspect and reconfigure endpoints; list deliveries and their attempt history; replay a failed delivery, which resumes the same logical delivery rather than starting a new one.
- Distributed rate limiting on public ingestion, per source address and per endpoint, enforced through an atomic database counter so it holds across every API process rather than per replica.
- Alembic migrations, with a startup check that refuses to serve against a schema this build was not written for, and a test asserting the migrations and the models describe the same schema.
- Real PostgreSQL integration testing, including genuine concurrent-claim and concurrent-replay behaviour against independent connections.
- Submission retrieval and export. Browse and filter stored submissions by endpoint and time, read one back in full, and export a filtered range as streamed JSON or as CSV with formula-injection escaping.
- Retention cleanup, run deliberately by an operator, which never removes a submission a delivery could still need.
- A documentation site, covering installation, every guide, the full API and error reference, operations, and the architecture.
What changed in this release¶
This was a release-quality pass, not a feature interval, so the list above is what the previous eleven intervals had already built. What this release adds on top of that:
- The package version is
0.2.0everywhere it appears:pyproject.toml(dynamic from__init__.py), the/healthresponse, OpenAPI metadata, the webhookUser-Agent, and the documentation. - One architecture diagram, shown identically on the README and in the documentation, covering public ingestion, the authenticated management boundary, the transactional outbox, the PostgreSQL-backed queue, and signed outbound delivery.
- An end-to-end demo added to Quick Start: install, configure PostgreSQL, migrate, create a key, run the API and the worker, register an endpoint, submit a form, inspect the delivery, and optionally force a delivery to fail and replay it.
- A security review across API key handling, the management authentication boundary, webhook signing and SSRF guardrails, rate limit accounting, export and CSV formula escaping, idempotency, error responses and retention semantics. No exploitable issue was found; see Security for what was checked and what remains a known, documented gap rather than a defect.
- A correction to the documented SQLite story: a fresh SQLite database can no
longer reach the current schema through
alembic upgrade head, because revision0005alters two mutually-referencing tables directly rather than through Alembic's batch mode, which is what SQLite needs to change a column at all. That revision is written for PostgreSQL, which does not share the restriction, and PostgreSQL migrates and runs exactly as before. SQLite still backs the test suite, which builds its schema from the models rather than migrating it. See Database migrations. .claude/settings.local.json, a local tool-permission file that is not part of this project, is now excluded from the repository and from the built source distribution.
Verified before this release¶
- The fast test suite (in-memory SQLite) and the PostgreSQL integration suite both pass in full.
- Ruff lint, Ruff format check, and mypy in strict mode all pass with no findings.
mkdocs build --strictsucceeds.- The wheel and the source distribution both build from a clean tree, the wheel carries every migration including the Alembic script template, and installing the wheel into a fresh virtual environment is enough to run the CLI, the API and the worker against PostgreSQL end to end.
Limitations worth knowing before you deploy this¶
- Delivery is at-least-once, never exactly-once. Deduplicate on the
submission
idin the signed payload. - A failed delivery is never retried on its own. It stays
faileduntil an operator replays it; there is no alerting and no automatic sweep. - SSRF protection is partial. Webhook hostnames are checked as literals and never resolved, so a name that resolves to a private address still passes.
- There is no user, account or role model. Every valid management key can do everything a management key can do.
- Rate limiting bounds volume, not content. There is no CAPTCHA, no spam classification and no email verification.
- Retention is never automatic. Nothing is deleted until an operator runs the cleanup command, and it never removes a submission a delivery could still need.
- A fresh SQLite database cannot reach this schema. Use PostgreSQL; SQLite is a test-suite backend only. See above.
The full, honest list is in Limitations, and it is worth reading in full before you point this at real traffic.
Upgrading from a pre-release checkout¶
There is no prior tagged release, so there is no upgrade path to document yet.
A database already migrated through the previous revisions reaches this one
with an ordinary alembic upgrade head.