feat: webhooks with svix
Webhook implementation (#115 )
Issue 1 — Set up Svix self-hosted service ✅
Tasks:
-
Add Svix to docker-compose.yml(redis + svix-server services) -
Create dedicated svixdatabase viainit_db.py -
Add SVIX_SERVER_URL,SVIX_JWT_SECRET,SVIX_AUTH_TOKEN,SVIX_APP_IDto config /.env -
Install Svix Python SDK ( svix) -
Create app/webhooks/svix_client.py— singletonSvixAsyncclient -
Create EHRS Application in Svix on startup via get_or_create -
Register all 17 event types as Svix EventTypeobjects on startup -
Add health check — degrades gracefully if Svix is unavailable -
svix-server jwt generateprovides valid auth token
Issue 2 — Create WebhookDispatcher service ✅
Tasks:
-
Create app/webhooks/dispatcher.pywithWebhookDispatcherclass -
Use FastAPI BackgroundTasksto 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 WebhookDispatcheras 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:
-
InboundWebhookSourceDB 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-Timestampcheck — rejects if older than 5 minutes -
X-EHRS-SignatureHMAC-SHA256 verification — returns 401 if invalid -
event_typevalidated againstallowed_event_types— returns 422 if not permitted -
Enqueues to ARQ and returns 202 Accepted
ARQ Worker:
-
lab.result.ready→ updatesextra_noteon 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.failevents per endpoint in Redis -
Resets counter on message.attempt.success -
Auto-disables endpoint after 5 consecutive failures (lock-protected) -
GET /webhooks/subscriptions/{id}/failure-statsfor 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
- Set
SVIX_AUTH_TOKENin.env(rundocker exec ehrs-svix svix-server jwt generate) docker compose up- Create subscribers via Svix API or dashboard for desired event types
- Events fire automatically when routes are called
Tests
1315 tests pass, 90% coverage.
Edited by Surya Manoj Pentakota