APIs and backends

Durable webhook relay with retries and replay

Hosted FastAPI relay that durably stores provider webhooks, retries delivery with backoff, supports replay, signature checks, and usage metering.

fastapipythonpostgresredissqlalchemyalembic

Built 1 time, downloaded 0 times.

Install from your terminal

Run this in your project folder. It signs you in, shows what changes and asks before it overwrites a file.

npx claude-lab add durable-webhook-relay-with-retries-and-replay-829db4
10 filesDownload zip
CLAUDE.md
# Webhook relay

## Purpose

This is a hosted webhook relay. It receives provider webhooks (Stripe first), stores them durably, delivers them to customer endpoints with retries, and supports replay. The selling point is that we never lose an event. When durability, idempotency, and speed conflict, durability and idempotency win.

## Stack

- Python 3.12, FastAPI, SQLAlchemy, Alembic, psycopg
- Postgres: durable source of truth (event store)
- Redis: queue, scheduling, rate limits, dedupe cache only
- pytest, ruff, mypy
- Ask before adding any other dependency.

## Commands

- Install: `python -m venv .venv`, activate it, then `pip install -e ".[dev]"`
- Local services: `docker compose up -d`
- Test services: `docker compose -f docker-compose.test.yml up -d`
- Migrations (local): `alembic upgrade head`
- Dev server: `uvicorn app.main:app --reload`
- Worker: `python -m app.workers.run`
- Tests: `pytest -q`
- Lint: `ruff check . && ruff format --check .`
- Type check: `mypy app cli`
- Full verification: run the `/verify` skill

## Durability rules

- Ingest writes the event to Postgres and commits BEFORE returning 2xx.
- Redis is never the only copy of an event.
- If Redis loses data, the recovery job re-enqueues events from Postgres.
- If enqueue fails after commit, mark the event `pending_enqueue` for recovery.
- Delivery is at-least-once.
- Every delivery carries a stable idempotency header derived from the event id.
- Retries use exponential backoff with jitter, a max attempt count, and per-endpoint circuit state.
- After max attempts, the event goes to dead-letter.

## Idempotency rules

- Dedupe on provider event id plus source id with a unique DB constraint.
- A duplicate provider delivery returns 200 with the original event id and creates no row.
- A replay creates a new delivery attempt, never a new event.

## Signatures

- Verify signatures over raw request body bytes with constant-time compare.
- Never parse JSON before verifying.
- Stripe first. Other providers are added as adapters (see the `webhook-provider` skill).

## Retention and billing

- Retention: 30 days on paid plans, 3 days on free.
- A scheduled job deletes expired events. Deletion must be tested and logged.
- Metering counts each accepted event once.
- All time is UTC.

## Layout

- `app/api/`: HTTP routes (ingest, sources, endpoints, replay)
- `app/workers/`: delivery worker, recovery job
- `app/providers/`: signature adapters
- `app/billing/`: usage metering and plan limits
- `app/retention/`: deletion job
- `app/models/`: SQLAlchemy models
- `cli/`: localhost forwarding CLI
- `tests/`: pytest suite
- `migrations/`: Alembic revisions (forward-only)

## Gotchas

- Never log payload bodies or secrets.
- Use placeholders like `YOUR_API_KEY` in docs and examples.
- Never commit `.env`.
- Migrations are forward-only. Never edit an applied migration.
- Tests must not call real provider APIs. Use fixtures.
- Outbound calls to customer URLs need timeouts and SSRF protection (see REVIEW.md).

## Workflow

- Explore, plan, code, then run `/verify`.
- Use `/clear` between unrelated tasks.
- Phases are in PLAN.md. Open product questions are in SPEC.md.

## Do not

- Do not use in-memory queues.
- Do not use fire-and-forget background tasks for ingest.
- Do not return 200 and then save.

@REVIEW.md
Use for my project