Skip to content

vintasoftware/vintasend-medplum

Repository files navigation

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 Communication resources
  • MedplumNotificationAdapter: Sends email notifications via Medplum's email API
  • MedplumAttachmentManager: Manages file attachments using FHIR Binary and Media resources
  • 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.json
import { 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:

  1. Binary Resource: Stores the actual file data (base64 encoded)
  2. 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/core

Setup

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.pug

Step 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.json

Or 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:support@example.com") contact us

Sending 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: 'patient@example.com',
  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 notification
  • persistOneOffNotification(notification) - Create a one-off notification
  • getNotification(id) - Retrieve a notification by ID
  • getPendingNotifications(page, pageSize) - Get notifications ready to send
  • markAsSent(id) - Mark notification as successfully sent
  • markAsFailed(id) - Mark notification as failed
  • cancelNotification(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 - Returns true

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 by compile-pug-templates script)

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 BaseAttachmentManager

Manages file attachments using FHIR Binary and Media resources.

Constructor:

constructor(medplum: MedplumClient)

Key Methods:

  • uploadFile(file, filename, contentType?) - Upload a file to Medplum storage
  • getFile(fileId) - Retrieve file metadata by Media resource ID
  • deleteFile(fileId) - Delete file and its Binary resource
  • reconstructAttachmentFile(storageMetadata) - Create AttachmentFile from metadata

MedplumAttachmentFile

class MedplumAttachmentFile implements AttachmentFile

Provides access to files stored in FHIR Binary resources.

Methods:

  • read() - Read entire file into a Buffer
  • stream() - Get a ReadableStream for the file
  • url(expiresIn?) - Generate URL for file access
  • delete() - 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.pug

2. 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 account

4. 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:

About

Medplum implementations for VintaSend

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors