A small, production-oriented TypeScript foundation for subscription lifecycle processing. It focuses on the parts that are hardest to retrofit later: explicit state transitions, durable webhook ingestion, idempotent processing, transactional audit/outbox writes, and authorization boundaries.
This is a clean-room reference project. It contains no customer records, provider fixtures, production metrics, or deployment claims.
- An explicit subscription state machine with terminal states and same-state no-ops.
- HMAC verification over the raw webhook body before persistence.
- A PostgreSQL inbox with a unique
(provider, event_id)boundary. Duplicate delivery exits before subscription, audit, or outbox effects. - A worker that locks pending events with
FOR UPDATE SKIP LOCKEDand commits the subscription update, audit entry, outbox message, and inbox completion in one transaction. - Optimistic subscription versions plus row locks to make concurrent updates visible.
- A deterministic outbox deduplication key per aggregate version.
- A deny-by-default RBAC policy for change, audit, and replay capabilities.
- Unit tests plus a real PostgreSQL integration test in CI.
flowchart LR
Provider[Webhook provider] --> Verify[Verify raw-body HMAC]
Verify --> Claim[Insert inbox row]
Claim -->|unique conflict| Duplicate[Return duplicate; no effects]
Claim -->|accepted| Worker[Worker locks pending row]
Worker --> Transition[Validate state transition]
Transition --> Commit[Atomic commit]
Commit --> Subscription[(Subscription)]
Commit --> Audit[(Audit log)]
Commit --> Outbox[(Outbox)]
The HTTP process does not mutate a subscription. Its responsibility ends after a verified event is durably accepted. That separation closes the crash window between acknowledging a webhook and recording work to be done.
Requirements: Node.js 22+, npm, and Docker.
cp .env.example .env
docker compose up -d postgres redis
npm install
set -a; source .env; set +a
npm run db:migrate
npm test
npm run start:devRun the inbox worker in a second terminal:
set -a; source .env; set +a
npm run workerThe liveness endpoint is GET /health/live. Webhooks are accepted at
POST /webhooks/:provider with x-webhook-signature: sha256=<hex digest>. The body contract is:
{
"eventId": "provider-event-id",
"eventType": "subscription.state_requested",
"occurredAt": "2026-01-01T00:00:00Z",
"data": {
"subscriptionId": "uuid-from-your-system",
"targetState": "ACTIVE",
"actorReference": "provider:account-reference"
}
}No seed data is included. Insert a local-only subscription explicitly if you want to exercise the worker end to end:
INSERT INTO subscriptions (id, state)
VALUES ('00000000-0000-0000-0000-000000000001', 'PENDING');| Current | Allowed next states |
|---|---|
PENDING |
ACTIVE, CANCELED, EXPIRED |
ACTIVE |
PAST_DUE, PAUSED, CANCELED |
PAST_DUE |
ACTIVE, PAUSED, CANCELED, EXPIRED |
PAUSED |
ACTIVE, CANCELED, EXPIRED |
CANCELED |
terminal |
EXPIRED |
terminal |
Re-delivering the current state is an idempotent no-op. Illegal transitions fail processing and remain available for a bounded retry policy; they do not partially update domain state.
src/domain: state machine and RBAC policy with no framework dependency.src/application: webhook use cases and persistence ports.src/infrastructure: HMAC and PostgreSQL adapters.migrations: inbox, subscription, audit, and outbox schema.test: transition, authorization, signature, duplicate-delivery, and PostgreSQL tests.docs: architecture decisions, failure behavior, security boundary, and honest limitations.
npm run format:check
npm run lint
npm run build
npm testSee Architecture, Failure modes, Security, and Limitations before adapting this code.