vintasend-medplum
v0.13.3
Published
A complete VintaSend implementation using [Medplum](https://www.medplum.com/) as the backend, leveraging FHIR resources for notification management, file storage, and healthcare integration.
Readme
VintaSend Medplum Implementation
A complete VintaSend implementation using Medplum as the backend, leveraging FHIR resources for notification management, file storage, and healthcare integration.
Overview
This implementation uses FHIR (Fast Healthcare Interoperability Resources) standards to store and manage notifications, making it ideal for healthcare applications that need to integrate notifications with patient care workflows.
Key Components
- MedplumNotificationBackend: Stores notifications as FHIR
Communicationresources - MedplumNotificationAdapter: Sends email notifications via Medplum's email API
- MedplumAttachmentManager: Manages file attachments using FHIR
BinaryandMediaresources - PugInlineEmailTemplateRenderer: Renders Pug email templates from pre-compiled JSON (ideal for production)
- MedplumLogger: Simple console-based logger
Quick Start
# Install the package
npm install vintasend-medplum @medplum/core
# Compile your Pug templates
npx compile-pug-templates ./templates ./src/compiled-templates.jsonimport { MedplumClient } from '@medplum/core';
import {
MedplumNotificationBackend,
MedplumNotificationAdapter,
PugInlineEmailTemplateRenderer,
MedplumLogger
} from 'vintasend-medplum';
import { NotificationService } from 'vintasend';
import compiledTemplates from './compiled-templates.json';
// Initialize Medplum client
const medplum = new MedplumClient({
baseUrl: 'https://api.medplum.com',
clientId: 'your-client-id',
clientSecret: 'your-client-secret',
});
// Create services
const renderer = new PugInlineEmailTemplateRenderer(compiledTemplates);
const adapter = new MedplumNotificationAdapter(medplum, renderer);
const backend = new MedplumNotificationBackend(medplum);
const logger = new MedplumLogger();
// Initialize notification service
const notificationService = new NotificationService(backend, [adapter], logger);
// Send a notification
await notificationService.createNotification({
userId: 'Patient/123',
notificationType: 'EMAIL',
contextName: 'welcome',
contextParameters: { firstName: 'John' },
title: 'Welcome!',
bodyTemplate: 'welcome.pug',
subjectTemplate: 'subjects/welcome.pug',
sendAfter: new Date(),
});
await notificationService.processPendingNotifications();How It Works
FHIR Resource Mapping
The implementation maps VintaSend concepts to FHIR resources:
Notifications → Communication Resources
{
resourceType: "Communication",
status: "in-progress", // PENDING_SEND
sent: "2024-01-15T10:00:00Z", // sendAfter
topic: { text: "Welcome!" }, // title
recipient: [{ reference: "Patient/123" }], // userId
payload: [{
contentString: "Hello {{name}}", // bodyTemplate
extension: [{
url: "http://vintasend.com/fhir/StructureDefinition/email-notification-subject",
valueString: "Welcome {{name}}" // subjectTemplate
}]
}],
note: [{ text: '{"userId": "123"}' }], // contextParameters
meta: {
tag: [
{ code: "notification" },
{ code: "user-welcome" }, // contextName
{ code: "EMAIL" } // notificationType
]
}
}File Attachments → Binary + Media Resources
Files are stored using two FHIR resources:
- Binary Resource: Stores the actual file data (base64 encoded)
- Media Resource: Stores metadata and links to the Binary
// Binary resource
{
resourceType: "Binary",
contentType: "application/pdf",
data: "base64EncodedData..."
}
// Media resource
{
resourceType: "Media",
status: "completed",
content: {
contentType: "application/pdf",
url: "Binary/binary-id",
size: 12345,
title: "invoice.pdf"
},
identifier: [{
system: "http://vintasend.com/fhir/attachment-checksum",
value: "sha256-checksum"
}]
}Status Mapping
| VintaSend Status | FHIR Communication Status | |-----------------|---------------------------| | PENDING_SEND | in-progress | | SENT | completed | | FAILED | stopped |
Installation
npm install vintasend-medplum @medplum/coreSetup
Template Compilation
VintaSend Medplum uses pre-compiled Pug email templates that are embedded in your application as JSON. This approach ensures templates are bundled with your code and don't require file system access at runtime.
Step 1: Organize Your Templates
Create a directory structure for your templates:
templates/
welcome.pug
password-reset.pug
notifications/
order-confirmation.pug
shipment-update.pugStep 2: Compile Templates
Run the compilation script using npx:
npx compile-pug-templates [input-directory] [output-file]Both arguments are optional:
input-directory: Directory containing .pug templates (default:./templates)output-file: Output JSON file path (default:compiled-templates.json)
Examples:
# Use default values (./templates → compiled-templates.json)
npx compile-pug-templates
# Specify only input directory (output to compiled-templates.json)
npx compile-pug-templates ./email-templates
# Specify both arguments
npx compile-pug-templates ./templates ./src/compiled-templates.jsonOr add it to your package.json scripts:
{
"scripts": {
"compile-templates": "compile-pug-templates ./templates ./src/compiled-templates.json"
}
}This generates a JSON file where keys are relative paths and values are template contents:
{
"welcome.pug": "doctype html\nhtml\n body\n h1 Welcome {{firstName}}!",
"notifications/order-confirmation.pug": "doctype html\n..."
}Step 3: Import and Use Compiled Templates
import { PugInlineEmailTemplateRenderer } from 'vintasend-medplum';
import compiledTemplates from './compiled-templates.json';
// Create the template renderer with compiled templates
const templateRenderer = new PugInlineEmailTemplateRenderer(compiledTemplates);
// Use with notification adapter
const adapter = new MedplumNotificationAdapter(medplum, templateRenderer);Basic Configuration
import { MedplumClient } from '@medplum/core';
import { MedplumNotificationBackend } from 'vintasend-medplum';
import { MedplumNotificationAdapter } from 'vintasend-medplum';
import { MedplumAttachmentManager } from 'vintasend-medplum';
import { MedplumLogger } from 'vintasend-medplum';
import { PugTemplateRenderer } from 'vintasend-pug';
import { NotificationService } from 'vintasend';
// Initialize Medplum client
const medplum = new MedplumClient({
baseUrl: 'https://api.medplum.com',
clientId: 'your-client-id',
clientSecret: 'your-client-secret',
});
// Create template renderer
const templateRenderer = new PugTemplateRenderer({
templatesDir: './templates',
});
// Create notification adapter
const adapter = new MedplumNotificationAdapter(medplum, templateRenderer);
// Create backend
const backend = new MedplumNotificationBackend(medplum, {
emailNotificationSubjectExtensionUrl: 'http://your-domain.com/fhir/StructureDefinition/email-notification-subject',
});
// Create attachment manager (optional)
const attachmentManager = new MedplumAttachmentManager(medplum);
// Create logger
const logger = new MedplumLogger();
// Initialize notification service
const notificationService = new NotificationService(
backend,
[adapter],
logger,
attachmentManager,
);With Custom Configuration
// Custom extension URL for email subjects
const backend = new MedplumNotificationBackend(medplum, {
emailNotificationSubjectExtensionUrl: 'http://example.com/fhir/email-subject',
});Usage Examples
Configuring Templates
When creating notifications, reference your templates using the same paths used during compilation:
// If you compiled templates/welcome.pug
await notificationService.createNotification({
userId: 'Patient/123',
notificationType: 'EMAIL',
contextName: 'user-welcome',
contextParameters: {
firstName: 'John',
lastName: 'Doe',
},
title: 'Welcome to our platform!',
bodyTemplate: 'welcome.pug', // Path from compiled templates
subjectTemplate: 'subjects/welcome.pug', // Can be in subdirectories
sendAfter: new Date(),
});
// If you compiled templates/notifications/order-confirmation.pug
await notificationService.createNotification({
userId: 'Patient/456',
notificationType: 'EMAIL',
contextName: 'order-confirmation',
contextParameters: {
orderNumber: '12345',
totalAmount: '$99.99',
},
title: 'Order Confirmation',
bodyTemplate: 'notifications/order-confirmation.pug',
subjectTemplate: 'notifications/subjects/order-confirmation.pug',
sendAfter: new Date(),
});Template Example (welcome.pug):
doctype html
html
head
title Welcome
body
h1 Welcome #{firstName} #{lastName}!
p Thank you for joining our platform.
p
| If you have any questions, feel free to
a(href="mailto:[email protected]") contact usSending a Simple Notification
// Create a notification
await notificationService.createNotification({
userId: 'Patient/123',
notificationType: 'EMAIL',
contextName: 'user-welcome',
contextParameters: {
firstName: 'John',
lastName: 'Doe',
},
title: 'Welcome to our platform!',
bodyTemplate: 'Hello {{firstName}} {{lastName}}!',
subjectTemplate: 'Welcome {{firstName}}!',
sendAfter: new Date(),
});
// Process pending notifications
await notificationService.processPendingNotifications();Sending Notifications with Attachments
import { readFile } from 'fs/promises';
// Create notification with file attachments
await notificationService.createNotification({
userId: 'Patient/123',
notificationType: 'EMAIL',
contextName: 'lab-results',
contextParameters: {
patientName: 'John Doe',
},
title: 'Your lab results are ready',
bodyTemplate: 'Dear {{patientName}}, your lab results are attached.',
subjectTemplate: 'Lab Results - {{patientName}}',
sendAfter: new Date(),
attachments: [
{
file: await readFile('./lab-results.pdf'),
filename: 'lab-results.pdf',
contentType: 'application/pdf',
},
],
});One-Off Notifications
Send notifications to users without storing them in the system:
await notificationService.createOneOffNotification({
emailOrPhone: '[email protected]',
firstName: 'Jane',
lastName: 'Smith',
notificationType: 'EMAIL',
contextName: 'appointment-reminder',
contextParameters: {
appointmentDate: '2024-02-01',
doctorName: 'Dr. Johnson',
},
title: 'Appointment Reminder',
bodyTemplate: 'Hi {{firstName}}, reminder for your appointment on {{appointmentDate}}.',
subjectTemplate: 'Appointment on {{appointmentDate}}',
sendAfter: new Date(),
});Managing Attachments
// Upload a file
const fileRecord = await attachmentManager.uploadFile(
buffer,
'document.pdf',
'application/pdf',
);
// Retrieve file metadata
const file = await attachmentManager.getFile(fileRecord.id);
// Get file data
const attachmentFile = attachmentManager.reconstructAttachmentFile(
file.storageMetadata,
);
const fileBuffer = await attachmentFile.read();
// Generate temporary URL
const url = await attachmentFile.url(3600); // 1 hour expiry
// Delete file
await attachmentManager.deleteFile(fileRecord.id);Querying Notifications
// Get pending notifications
const pending = await notificationService.getPendingNotifications(0, 10);
// Get future notifications for a user
const future = await notificationService.getFutureNotificationsFromUser(
'Patient/123',
0,
10,
);
// Get unread in-app notifications
const unread = await notificationService.filterInAppUnreadNotifications(
'Patient/123',
0,
10,
);
// Mark as read
await notificationService.markAsRead(notificationId);Canceling Notifications
// Cancel a scheduled notification
await notificationService.cancelNotification(notificationId);Healthcare Integration
This implementation is designed for healthcare applications and integrates naturally with Medplum's FHIR-based infrastructure.
Linking Notifications to Patients
// Create notification linked to a patient
await notificationService.createNotification({
userId: 'Patient/123', // FHIR Patient reference
notificationType: 'EMAIL',
contextName: 'medication-reminder',
contextParameters: {
medicationName: 'Aspirin',
dosage: '100mg',
},
// ...
});Linking Notifications to Practitioners
// Notify a healthcare provider
await notificationService.createNotification({
userId: 'Practitioner/456', // FHIR Practitioner reference
notificationType: 'EMAIL',
contextName: 'new-patient-alert',
// ...
});Searching Notifications by Resource
// Get all notifications for a patient
const communications = await medplum.searchResources('Communication', {
_tag: 'notification',
recipient: 'Patient/123',
});Features
✅ Supported Features
- Email notifications via Medplum's email API
- File attachments using FHIR Binary and Media resources
- One-off notifications (no user account required)
- Scheduled notifications (send later)
- Notification templates with context parameters
- File deduplication via checksum
- Attachment cleanup for orphaned files
- FHIR-compliant data storage
❌ Not Yet Supported
- SMS notifications (Medplum limitation)
- Push notifications (Medplum limitation)
- In-app notification UI
- Real-time notification delivery (requires polling or webhooks)
API Reference
MedplumNotificationBackend
class MedplumNotificationBackend<Config extends BaseNotificationTypeConfig>Main backend for storing notifications as FHIR Communication resources.
Constructor:
constructor(
medplum: MedplumClient,
options?: {
emailNotificationSubjectExtensionUrl?: string;
}
)Key Methods:
persistNotification(notification)- Create a new notificationpersistOneOffNotification(notification)- Create a one-off notificationgetNotification(id)- Retrieve a notification by IDgetPendingNotifications(page, pageSize)- Get notifications ready to sendmarkAsSent(id)- Mark notification as successfully sentmarkAsFailed(id)- Mark notification as failedcancelNotification(id)- Cancel a scheduled notification
MedplumNotificationAdapter
class MedplumNotificationAdapter<
TemplateRenderer extends BaseEmailTemplateRenderer<Config>,
Config extends BaseNotificationTypeConfig
>Adapter for sending email notifications via Medplum.
Constructor:
constructor(
medplum: MedplumClient,
templateRenderer: TemplateRenderer
)Properties:
supportsAttachments: boolean- Returnstrue
Key Methods:
send(notification, context)- Send an email notification with attachments
PugInlineEmailTemplateRenderer
class PugInlineEmailTemplateRenderer<Config extends BaseNotificationTypeConfig>
implements BaseEmailTemplateRenderer<Config>Template renderer that compiles Pug templates from pre-compiled JSON strings instead of reading from file paths. This is ideal for production deployments where templates are embedded in the application.
Constructor:
constructor(generatedTemplates: Record<string, string>)Parameters:
generatedTemplates- Object mapping template paths to template content strings (generated bycompile-pug-templatesscript)
Key Methods:
render(notification, context)- Compile and render both subject and body templates using the notification's template paths
Example:
import compiledTemplates from './compiled-templates.json';
const renderer = new PugInlineEmailTemplateRenderer(compiledTemplates);
const adapter = new MedplumNotificationAdapter(medplum, renderer);MedplumAttachmentManager
class MedplumAttachmentManager extends BaseAttachmentManagerManages file attachments using FHIR Binary and Media resources.
Constructor:
constructor(medplum: MedplumClient)Key Methods:
uploadFile(file, filename, contentType?)- Upload a file to Medplum storagegetFile(fileId)- Retrieve file metadata by Media resource IDdeleteFile(fileId)- Delete file and its Binary resourcereconstructAttachmentFile(storageMetadata)- Create AttachmentFile from metadata
MedplumAttachmentFile
class MedplumAttachmentFile implements AttachmentFileProvides access to files stored in FHIR Binary resources.
Methods:
read()- Read entire file into a Bufferstream()- Get a ReadableStream for the fileurl(expiresIn?)- Generate URL for file accessdelete()- Delete the file from storage
Best Practices
1. Template Organization
Organize your templates in a clear directory structure:
templates/
subjects/ # Email subject templates
welcome.pug
order-confirmation.pug
bodies/ # Or organize by feature
welcome.pug
order-confirmation.pug
notifications/ # Group related templates
orders/
confirmation.pug
shipped.pug
users/
welcome.pug
password-reset.pug2. Template Naming Conventions
Use consistent, descriptive names:
// ✅ Good - clear and descriptive
bodyTemplate: 'notifications/orders/confirmation.pug'
subjectTemplate: 'subjects/order-confirmation.pug'
// ❌ Bad - unclear purpose
bodyTemplate: 'template1.pug'
subjectTemplate: 'subj.pug'3. Template Variables
Document the expected context variables in each template:
//- templates/welcome.pug
//- Expected variables: firstName, lastName, loginUrl
doctype html
html
body
h1 Welcome #{firstName} #{lastName}!
a(href=loginUrl) Login to your account4. Compilation in Build Process
Add template compilation to your build pipeline:
{
"scripts": {
"compile-templates": "compile-pug-templates ./templates ./src/compiled-templates.json",
"build": "npm run compile-templates && tsc"
}
}This ensures templates are always compiled before building your application.
5. Use FHIR References Consistently
Always use proper FHIR reference format for user IDs:
// ✅ Good
userId: 'Patient/123'
userId: 'Practitioner/456'
// ❌ Bad
userId: '123'
userId: 'user-456'6. Configure Custom Extension URLs
Use your own domain for extension URLs in production:
const backend = new MedplumNotificationBackend(medplum, {
emailNotificationSubjectExtensionUrl: 'http://your-domain.com/fhir/email-subject',
});7. Handle Large Attachments Carefully
For large files, consider:
- Using file size limits
- Implementing file compression
- Using streaming for file operations
// Check file size before upload
const maxSize = 10 * 1024 * 1024; // 10MB
if (buffer.length > maxSize) {
throw new Error('File too large');
}8. Clean Up Orphaned Files
Regularly run cleanup to remove orphaned attachment files:
// Get orphaned files
const orphaned = await backend.getOrphanedAttachmentFiles();
// Delete them
for (const file of orphaned) {
await backend.deleteAttachmentFile(file.id);
}9. Use Pagination
Always paginate when fetching large result sets:
// ✅ Good
const notifications = await notificationService.getPendingNotifications(0, 50);
// ❌ Bad - could return thousands of records
const notifications = await notificationService.getAllPendingNotifications();License
MIT
Support
For issues and questions:
- VintaSend: GitHub Issues
- Medplum: Documentation
