Dashboard

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.

Adapter Package Description
ServerLocalStorage @fhirfly-io/shl/server Filesystem storage for development
ServerS3Storage @fhirfly-io/shl/server AWS S3 storage
ServerAzureStorage @fhirfly-io/shl/server Azure Blob Storage
ServerGCSStorage @fhirfly-io/shl/server Google Cloud Storage

Endpoints

The middleware provides these endpoints:

Method Path Description
POST /:shlId Manifest endpoint — flag L (returns file URLs)
GET /:shlId Direct retrieval — flag U (PSHD and direct-mode SHLs)
GET /:shlId/content Encrypted content download
GET /:shlId/attachment/:index Encrypted 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

Field Type Description
timestamp number Epoch milliseconds
recipient string? From ?recipient= query parameter
ip string? Client IP address
userAgent string? Client User-Agent header