Skip to content

feat: webhooks with svix

Surya Manoj Pentakota requested to merge feat/webhook into develop

Webhook implementation (#115 )

Issue 1 — Set up Svix self-hosted service

Tasks:

  • Add Svix to docker-compose.yml (redis + svix-server services)
  • Create dedicated svix database via init_db.py
  • Add SVIX_SERVER_URL, SVIX_JWT_SECRET, SVIX_AUTH_TOKEN, SVIX_APP_ID to config / .env
  • Install Svix Python SDK (svix)
  • Create app/webhooks/svix_client.py — singleton SvixAsync client
  • Create EHRS Application in Svix on startup via get_or_create
  • Register all 17 event types as Svix EventType objects on startup
  • Add health check — degrades gracefully if Svix is unavailable
  • svix-server jwt generate provides valid auth token

Issue 2 — Create WebhookDispatcher service

Tasks:

  • Create app/webhooks/dispatcher.py with WebhookDispatcher class
  • Use FastAPI BackgroundTasks to enqueue the Svix call (non-blocking)
  • Build standard envelope fields (webhook_id, event_id, timestamp, api_version)
  • Catch and log all Svix exceptions — never crashes originating API call
  • Inject WebhookDispatcher as FastAPI dependency (get_webhook_dispatcher)

Issue 3 — Patient route event hooks

Event Trigger endpoint
patient.registered POST /api/v1/patients/
patient.updated PATCH /api/v1/patients/{book_no}
patient.camp_registered POST /api/v1/patients/{book_no}/register
vitals.recorded PUT /api/v1/patients/vitals/{book_no}

Issue 4 — Consultation route event hooks

Event Trigger endpoint
consultation.created POST /api/v1/doctors/assign-doctor
consultation.updated PUT /api/v1/consultations/{consultation_id}
consultation.completed PUT /api/v1/queue/status-update when status=completed

Issue 5 — Prescription route event hooks

Event Trigger
prescription.added POST /api/v1/consultations/{consultation_id}/prescription
prescription.dispensed PUT /api/v1/consultations/prescriptions/{id} when dispatch_status=given
prescription.out_of_stock PUT /api/v1/consultations/prescriptions/{id} when dispatch_status=out_of_stock
prescription.replaced PUT /api/v1/consultations/prescriptions/{id} when replaced_by_medicine_id set

Events are mutually exclusive based on state after DB write.

Issue 6 — Queue route event hooks

Event Trigger endpoint
queue.status_changed PUT /api/v1/queue/status-update (all transitions)
queue.priority_changed PUT /api/v1/queue/priority

Issue 7 — Camp route event hooks

Event Trigger endpoint
camp.created POST /api/v1/medical-camps/
camp.updated PUT /api/v1/medical-camps/{camp_id}
camp.deleted DELETE /api/v1/medical-camps/{camp_id}
camp.attendance_updated POST /api/v1/medical-camps/{camp_id}/update-attended-count

Issue 8 — Inbound webhook receiver

Models:

  • InboundWebhookSource DB model (id, name, allowed_event_types, secret/bcrypt, is_active, action_mapping)

Receiver endpoint:

  • POST /api/v1/webhooks/inbound/{source_id} — validates source exists & active
  • X-EHRS-Timestamp check — rejects if older than 5 minutes
  • X-EHRS-Signature HMAC-SHA256 verification — returns 401 if invalid
  • event_type validated against allowed_event_types — returns 422 if not permitted
  • Enqueues to ARQ and returns 202 Accepted

ARQ Worker:

  • lab.result.ready → updates extra_note on patient's latest vitals record
  • pharmacy.stock.update → updates medicine inventory quantity

Admin CRUD:

  • GET /api/v1/webhooks/inbound-sources — list all
  • POST /api/v1/webhooks/inbound-sources — create (secret shown once)
  • GET /api/v1/webhooks/inbound-sources/{id} — get details
  • PUT /api/v1/webhooks/inbound-sources/{id} — update
  • DELETE /api/v1/webhooks/inbound-sources/{id} — delete

Issue 9 — Subscription & delivery log proxy

All endpoints require admin role, proxied to Svix API:

Method Path
GET /webhooks/subscriptions
POST /webhooks/subscriptions (rejects non-HTTPS URLs)
GET /webhooks/subscriptions/{id}
PUT /webhooks/subscriptions/{id}
DELETE /webhooks/subscriptions/{id}
PUT /webhooks/subscriptions/{id}/toggle
POST /webhooks/subscriptions/{id}/test
POST /webhooks/subscriptions/{id}/rotate-secret
GET /webhooks/logs
GET /webhooks/logs/{subscription_id}
POST /webhooks/logs/{msg_id}/retry

Issue 10 — Auto-pause on repeated failures

  • POST /api/v1/webhooks/internal/svix-ops — Svix operational webhook listener
  • Tracks consecutive message.attempt.fail events per endpoint in Redis
  • Resets counter on message.attempt.success
  • Auto-disables endpoint after 5 consecutive failures (lock-protected)
  • GET /webhooks/subscriptions/{id}/failure-stats for admin visibility

Issue 11 — OpenAPI webhook schema declarations

Not yet implemented.

Dependency Graph

Issue 1  ──── Svix self-hosted setup ✅

Issue 2  ──── WebhookDispatcher ✅

   ├── Issue 3  ── Patient route hooks ✅
   ├── Issue 4  ── Consultation route hooks ✅
   ├── Issue 5  ── Prescription route hooks ✅
   ├── Issue 6  ── Queue route hooks ✅
   └── Issue 7  ── Camp route hooks ✅

Issue 1  ──── Issue 8   ── Inbound webhook receiver ✅
Issue 1  ──── Issue 9   ── Subscription & log proxy ✅
Issue 9  ──── Issue 10  ── Auto-pause on failures ✅
Issues 3-7 ── Issue 11  ── OpenAPI schemas ❌

Commits

857b598 feat(webhooks): add all remaining event hooks (Issues 3-7)
4caa366 fix(webhooks): add SVIX_AUTH_TOKEN back, fix Svix integration
8734ab7 fix(webhooks): align Svix SDK usage with docs
f23aca5 refactor(webhooks): remove SVIX_AUTH_TOKEN
bf53a16 refactor(webhooks): move SVIX_APP_ID to env config
ea948d1 test(webhooks): add subscription proxy and auto-pause tests
aec0e36 feat(webhooks): add subscription proxy and auto-pause (Issues 9-10)
b4d3406 test(webhooks): add comprehensive unit tests (97% coverage)
552ad03 feat(webhooks): add patient.camp_registered hook + inbound receiver
9e309b4 feat(webhooks): add Svix client singleton + startup registration
ad76bb4 feat(webhooks): add Svix Docker Compose + config

Usage

  1. Set SVIX_AUTH_TOKEN in .env (run docker exec ehrs-svix svix-server jwt generate)
  2. docker compose up
  3. Create subscribers via Svix API or dashboard for desired event types
  4. Events fire automatically when routes are called

Tests

1315 tests pass, 90% coverage.

Edited by Surya Manoj Pentakota

Merge request reports

Loading