POC IZNC - Integrated Care Network Communication
0.1.2 - ci-build
POC IZNC - Integrated Care Network Communication - Local Development build (v0.1.2) built by the FHIR (HL7® FHIR® Standard) Build Tools. See the Directory of published versions
| Page standards status: Draft |
⚠️ STATUS: DRAFT - FOR REVIEW
This specification is currently in draft status and is open for review and feedback. Implementation details may change based on community input and practical experience.
Author: roland@headease.nl
This document is released under the Attribution-ShareAlike 4.0 International (CC BY-SA 4.0) license.
Simple webhook endpoints that the Chat Application Backend must implement to receive real-time event notifications from the Matrix Bridge. This follows the FHIR subscription pattern used in the OZO messaging implementation.
The Chat Application Backend should:
Matrix Event Occurs (new message, read receipt, etc.)
↓
Matrix Bridge detects event
↓
Matrix Bridge checks active subscriptions
↓
Matrix Bridge POST to webhook endpoint
↓
Chat Application Backend receives notification
↓
Chat Application Backend optionally fetches details via Matrix Bridge API
↓
Chat Application Backend pushes to frontend clients via WebSocket
POST /webhooks/matrix-events
Purpose: Receive all types of events from Matrix Bridge in a single endpoint.
Request Headers:
Content-Type: application/json
X-Subscription-Id: sub-uuid-1234
Request Body:
{
"subscriptionId": "sub-uuid-1234",
"eventType": "message.new",
"careNetworkId": "!space123:homeserver.example.com",
"timestamp": "2025-01-15T12:00:00Z",
"data": {
"threadId": "!room456:homeserver.example.com",
"messageId": "$event125",
"sender": {
"userId": "@dr.smith:homeserver.example.com",
"name": "Dr. Smith"
}
}
}
Response:
{
"status": "received",
"timestamp": "2025-01-15T12:00:01Z"
}
Expected Behavior:
message.newNew message was sent in a thread.
Webhook Payload:
{
"subscriptionId": "sub-uuid-1234",
"eventType": "message.new",
"careNetworkId": "!space123:homeserver.example.com",
"timestamp": "2025-01-15T12:00:00Z",
"data": {
"threadId": "!room456:homeserver.example.com",
"messageId": "$event125",
"sender": {
"userId": "@dr.smith:homeserver.example.com",
"name": "Dr. Smith"
},
"hasAttachments": false,
"preview": "Ja, u kunt het innemen..."
}
}
Recommended Handling:
// Pseudo-code
async function handleMessageNew(event) {
// 1. Identify users to notify (thread participants)
const subscription = await getSubscription(event.subscriptionId);
const affectedBsn = subscription.bsn;
// 2. Optional: Fetch full message details if needed
const fullMessage = await matrixBridgeApi.getMessage(
event.data.threadId,
event.data.messageId,
affectedBsn
);
// 3. Find connected WebSocket clients for this user
const wsClients = getConnectedClients(affectedBsn);
// 4. Push notification to clients
wsClients.forEach(client => {
client.send({
type: 'message.new',
threadId: event.data.threadId,
message: fullMessage // or just use the preview
});
});
// 5. Update unread counts, badges, etc.
await incrementUnreadCount(affectedBsn, event.data.threadId);
}
message.readMessage was marked as read by a user.
Webhook Payload:
{
"subscriptionId": "sub-uuid-1234",
"eventType": "message.read",
"careNetworkId": "!space123:homeserver.example.com",
"timestamp": "2025-01-15T12:05:00Z",
"data": {
"threadId": "!room456:homeserver.example.com",
"messageId": "$event124",
"reader": {
"userId": "@jan.jansen:homeserver.example.com",
"name": "Jan Jansen",
"bsn": "123456789"
}
}
}
Recommended Handling:
async function handleMessageRead(event) {
// 1. Get subscription details
const subscription = await getSubscription(event.subscriptionId);
// 2. Find connected clients for users in this thread
const threadParticipants = await getThreadParticipants(event.data.threadId);
// 3. Push read receipt to other participants
threadParticipants.forEach(participantBsn => {
if (participantBsn !== event.data.reader.bsn) {
const wsClients = getConnectedClients(participantBsn);
wsClients.forEach(client => {
client.send({
type: 'message.read',
threadId: event.data.threadId,
messageId: event.data.messageId,
reader: event.data.reader
});
});
}
});
}
thread.newNew conversation thread was created in the care network.
Webhook Payload:
{
"subscriptionId": "sub-uuid-1234",
"eventType": "thread.new",
"careNetworkId": "!space123:homeserver.example.com",
"timestamp": "2025-01-15T12:10:00Z",
"data": {
"threadId": "!newroom789:homeserver.example.com",
"topic": "Nieuwe vraag over medicatie",
"creator": {
"userId": "@jan.jansen:homeserver.example.com",
"name": "Jan Jansen",
"bsn": "123456789"
},
"participants": [
"@jan.jansen:homeserver.example.com",
"@dr.smith:homeserver.example.com"
]
}
}
Recommended Handling:
async function handleThreadNew(event) {
// 1. Fetch full thread details
const subscription = await getSubscription(event.subscriptionId);
const thread = await matrixBridgeApi.getThread(
event.data.threadId,
subscription.bsn
);
// 2. Notify all participants
event.data.participants.forEach(async userId => {
const bsn = await resolveToBsn(userId);
const wsClients = getConnectedClients(bsn);
wsClients.forEach(client => {
client.send({
type: 'thread.new',
careNetworkId: event.careNetworkId,
thread: thread
});
});
});
}
participant.joinedUser joined a thread or care network.
Webhook Payload:
{
"subscriptionId": "sub-uuid-1234",
"eventType": "participant.joined",
"careNetworkId": "!space123:homeserver.example.com",
"timestamp": "2025-01-15T12:15:00Z",
"data": {
"threadId": "!room456:homeserver.example.com",
"participant": {
"userId": "@nurse.jones:homeserver.example.com",
"name": "Verpleegkundige Jones",
"role": "care-professional"
}
}
}
participant.leftUser left a thread or care network.
Webhook Payload:
{
"subscriptionId": "sub-uuid-1234",
"eventType": "participant.left",
"careNetworkId": "!space123:homeserver.example.com",
"timestamp": "2025-01-15T12:20:00Z",
"data": {
"threadId": "!room456:homeserver.example.com",
"participant": {
"userId": "@nurse.jones:homeserver.example.com",
"name": "Verpleegkundige Jones"
}
}
}
The Matrix Bridge will:
Webhooks may be delivered multiple times. The Chat Application Backend should:
subscriptionId + timestamp + data.messageId/data.threadId as idempotency keyExample idempotency check:
async function handleWebhook(event) {
const eventKey = `${event.subscriptionId}:${event.eventType}:${event.data.messageId}:${event.timestamp}`;
if (await isEventProcessed(eventKey)) {
console.log('Duplicate event, ignoring');
return { status: 'received' };
}
await processEvent(event);
await markEventProcessed(eventKey);
return { status: 'received' };
}
If the Chat Application Backend webhook endpoint is unavailable:
When Chat Application Backend comes back online:
GET /api/v1/subscriptions/{subscriptionId}/missed-events?since={timestamp}
const express = require('express');
const app = express();
app.use(express.json());
// Webhook endpoint
app.post('/webhooks/matrix-events', async (req, res) => {
const event = req.body;
try {
// Validate subscription exists
const subscription = await getSubscription(event.subscriptionId);
if (!subscription) {
return res.status(404).json({ error: 'Subscription not found' });
}
// Return 200 immediately (process async)
res.status(200).json({
status: 'received',
timestamp: new Date().toISOString()
});
// Process event asynchronously
processEventAsync(event).catch(err => {
console.error('Failed to process event:', err);
// Log for manual investigation
});
} catch (error) {
console.error('Webhook error:', error);
res.status(500).json({ error: 'Internal server error' });
}
});
async function processEventAsync(event) {
switch (event.eventType) {
case 'message.new':
await handleMessageNew(event);
break;
case 'message.read':
await handleMessageRead(event);
break;
case 'thread.new':
await handleThreadNew(event);
break;
case 'participant.joined':
case 'participant.left':
await handleParticipantChange(event);
break;
default:
console.warn('Unknown event type:', event.eventType);
}
}
app.listen(3000, () => {
console.log('Webhook server listening on port 3000');
});
Use curl to simulate Matrix Bridge webhook calls:
curl -X POST http://localhost:3000/webhooks/matrix-events \
-H "Content-Type: application/json" \
-H "X-Subscription-Id: test-sub-123" \
-d '{
"subscriptionId": "test-sub-123",
"eventType": "message.new",
"careNetworkId": "!testspace:matrix.local",
"timestamp": "2025-01-15T12:00:00Z",
"data": {
"threadId": "!testroom:matrix.local",
"messageId": "$testevent123",
"sender": {
"userId": "@test:matrix.local",
"name": "Test User"
}
}
}'
Implement a health check for the Matrix Bridge to verify webhook endpoint:
GET /webhooks/health
Response:
{
"status": "healthy",
"timestamp": "2025-01-15T12:00:00Z"
}
Since this is an internal network deployment, authentication is not required. However, for production:
Optional: Implement webhook signature verification:
X-Webhook-Signature headerconst crypto = require('crypto');
function verifyWebhookSignature(body, signature, secret) {
const expectedSignature = crypto
.createHmac('sha256', secret)
.update(JSON.stringify(body))
.digest('hex');
return crypto.timingSafeEqual(
Buffer.from(signature),
Buffer.from(expectedSignature)
);
}
Implement rate limiting on webhook endpoints to prevent abuse:
Track these metrics for webhook health:
Log all webhook calls with:
console.log({
timestamp: new Date().toISOString(),
subscriptionId: event.subscriptionId,
eventType: event.eventType,
careNetworkId: event.careNetworkId,
processingTime: processingTimeMs,
status: 'success' // or 'error'
});
matrix-bridge-api.md