Server Guide

Host your own SHL endpoints using Express, Fastify, or AWS Lambda. The SDK provides middleware adapters that handle the SMART Health Links protocol.

Express

import express from "express";
import { expressMiddleware } from "@fhirfly-io/shl/express";
import { ServerLocalStorage } from "@fhirfly-io/shl/server";

const storage = new ServerLocalStorage({
  directory: "./shl-data",
  baseUrl: "http://localhost:3000/shl",
});

const app = express();
app.use(express.json());
app.use("/shl", expressMiddleware({ storage }));

app.listen(3000, () => {
  console.log("SHL server running on http://localhost:3000");
});

Fastify

import Fastify from "fastify";
import { fastifyPlugin } from "@fhirfly-io/shl/fastify";
import { ServerLocalStorage } from "@fhirfly-io/shl/server";

const storage = new ServerLocalStorage({
  directory: "./shl-data",
  baseUrl: "http://localhost:3000/shl",
});

const app = Fastify();
app.register(fastifyPlugin({ storage }), { prefix: "/shl" });

app.listen({ port: 3000 });

AWS Lambda

import { lambdaHandler } from "@fhirfly-io/shl/lambda";
import { ServerS3Storage } from "@fhirfly-io/shl/server";

const storage = new ServerS3Storage({
  bucket: process.env.SHL_BUCKET!,
  region: process.env.AWS_REGION!,
  baseUrl: process.env.SHL_BASE_URL!,
});

export const handler = lambdaHandler({
  storage,
  pathPrefix: "/shl",
});

Server Storage Adapters

Server-side storage extends the base storage with read() and updateMetadata() methods needed for the manifest protocol.

AdapterPackageDescription
ServerLocalStorage@fhirfly-io/shl/serverFilesystem storage for development
ServerS3Storage@fhirfly-io/shl/serverAWS S3 storage
ServerAzureStorage@fhirfly-io/shl/serverAzure Blob Storage
ServerGCSStorage@fhirfly-io/shl/serverGoogle Cloud Storage

Endpoints

The middleware provides these endpoints:

MethodPathDescription
POST/:shlIdManifest endpoint — flag L (returns file URLs)
GET/:shlIdDirect retrieval — flag U (PSHD and direct-mode SHLs)
GET/:shlId/contentEncrypted content download
GET/:shlId/attachment/:indexEncrypted attachment download

Both the manifest and direct endpoints validate expiration, check access limits, and increment counters automatically. The direct GET /:shlId route returns 405 Method Not Allowed for manifest-mode SHLs, preserving backward compatibility.

CORS

The SDK's createHandler() adds permissive CORS headers (Access-Control-Allow-Origin: *) to all responses by default, so browser-based SHL viewers can access your server cross-origin. You can customize or disable this:

// Custom origin
const handler = createHandler({
  storage,
  cors: { origin: "https://viewer.example.com" },
});

// Disable CORS (if your reverse proxy handles it)
const handler = createHandler({
  storage,
  cors: false,
});

Access Counter Concurrency

The SDK's updateMetadata() uses a read-modify-write pattern to atomically check access limits and increment counters. The correctness of this depends on your storage backend:

  • FhirflyStorage — Uses MongoDB $inc for true atomic increments. No race conditions. Recommended for production.
  • ServerS3Storage — Uses a plain read-modify-write pattern (no conditional writes or ETags). Concurrent requests may see stale counts under heavy load, potentially allowing a few extra accesses beyond maxAccesses. Acceptable for most use cases.
  • ServerLocalStorage — No concurrency protection. Suitable for development only.
  • ServerAzureStorage / ServerGCSStorage — Same as S3: plain read-modify-write, best-effort consistency.

If you're self-hosting with strict access count enforcement (e.g., maxAccesses: 1 for one-time links), consider using FhirflyStorage or adding your own atomic counter (Redis, DynamoDB) in the onAccess callback.

Audit Logging

onAccess callback

Pass an onAccess callback to log every successful access:

app.use("/shl", expressMiddleware({
  storage,
  onAccess: (event) => {
    console.log(`[SHL] ${event.mode} access to ${event.shlId}`, {
      recipient: event.recipient,
      count: event.accessCount,
    });
  },
}));

AuditableStorage

For storage-level audit logging, implement the AuditableStorage interface. This is opt-in — existing SHLServerStorage implementations work unchanged.

import { AuditableStorage, AccessEvent, isAuditableStorage } from "@fhirfly-io/shl/server";

class AuditedStorage extends ServerLocalStorage implements AuditableStorage {
  async onAccess(shlId: string, event: AccessEvent): Promise<void> {
    await db.auditLog.insert({
      shlId,
      recipient: event.recipient,
      ip: event.ip,
      userAgent: event.userAgent,
      timestamp: event.timestamp,
    });
  }
}

// The handler detects AuditableStorage at runtime via isAuditableStorage()
app.use("/shl", expressMiddleware({ storage: new AuditedStorage({ ... }) }));

Both mechanisms can be used together. Errors in the storage-level audit callback are caught silently — they never break the response.

AccessEvent

FieldTypeDescription
timestampnumberEpoch milliseconds
recipientstring?From ?recipient= query parameter
ipstring?Client IP address
userAgentstring?Client User-Agent header
© 2026 FHIRfly.io LLC. All rights reserved.