F POLARIS · FHIR
sdk · integration

Two integration modes.

Use @polaris/fhir-de directly against your own Aidbox installation, or build on the full Polaris platform. Both modes use the same resource builders and extension constants.

Mode A

Standalone Aidbox

Use createFhirDeClient directly against your own Aidbox FHIR server. You bring your own transport, your own Aidbox credentials, and your own deployment. The SDK handles profile-typed CRUD — you handle the infrastructure.

  • Direct FHIR CRUD against your Aidbox FHIR endpoint
  • You implement the FhirTransport SPI (e.g. fetch-based)
  • No Polaris platform dependency required
  • Same resource builders and extension constants as platform mode

Mode B

Polaris Platform

Build on the Polaris platform, which handles Aidbox lifecycle, multi-tenant identity, adapter orchestration, and MCP agent access. The adapter-sdk (internal, separate package) wraps the platform-level concerns — your application code stays at the FHIR resource level.

  • PVS adapter sync (x.isynet, Charly) managed by the platform
  • Multi-org Aidbox with identity and access control at the platform layer
  • MCP agent access with anonymization and audit by construction
  • Platform docs at polaris-platform.de

Standalone Aidbox — createFhirDeClient

Profile-typed CRUD against any FHIR server.

The createFhirDeClient factory creates a client that wraps FHIR operations with profile-typed methods. The client API is stable since v0.1.0. You supply the transport — no default HTTP client is included.

import {
  buildPatient,
  createFhirDeClient,
  FhirDeHttpError,
  KBV_PR_FOR_PatientProfile,
} from '@polaris/fhir-de'
import type { FhirTransport } from '@polaris/fhir-de'

// Implement the FhirTransport SPI with your HTTP client
const baseUrl = 'https://aidbox.example.com/fhir'

const transport: FhirTransport = {
  async request(method, path, init) {
    const normalizedPath = path.startsWith('/') ? path : `/${path}`
    const url = new URL(`${baseUrl.replace(/\\/$/, '')}${normalizedPath}`)

    if (init?.query) {
      for (const [key, value] of Object.entries(init.query)) {
        const values = Array.isArray(value) ? value : [value]
        for (const item of values) url.searchParams.append(key, item)
      }
    }

    const res = await fetch(url, {
      method,
      headers: {
        'Content-Type': 'application/fhir+json',
        Accept: 'application/fhir+json',
        'Authorization': `Bearer ${process.env.AIDBOX_TOKEN}`,
        ...init?.headers,
      },
      body: init?.body == null ? undefined : JSON.stringify(init.body),
    })

    const body = res.status === 204 ? null : await res.json().catch(() => null)
    if (!res.ok) throw new FhirDeHttpError(res.status, body)
    return body
  },
}

// Create a client
const client = createFhirDeClient({
  baseUrl,
  transport,
})

// Get a profile-scoped resource client
const patients = client.forProfile(KBV_PR_FOR_PatientProfile, 'Patient')

// Typed CRUD operations
const patient = await patients.read('pat-001')      // returns null on 404
const results = await patients.search({ family: 'Müller' })

const draft = buildPatient({
  id: 'pat-002',
  firstName: 'Anna',
  lastName: 'Müller',
  birthDate: '1985-04-12',
})

const created = await patients.createResource(draft)
await patients.update('pat-002', { ...draft, id: 'pat-002' })
await patients.delete('pat-001')                    // treats 404 as success

read(id)

Returns the profile-wrapped resource, or null on 404. A missing resource is a valid absence — not an error.

search(params)

Returns an array of profile-wrapped resources from the FHIR Bundle. An empty bundle returns [], not an error. A server 404 is a FhirDeHttpError.

delete(id)

Idempotent: a 404 on delete is treated as success (resource was already absent). Non-404 errors throw FhirDeHttpError.

Transport SPI

Bring your own HTTP client.

The FhirTransport interface is the extension point for HTTP behavior. This design decision (arch-council F#2) keeps the SDK independent of any specific HTTP library. A fetch-based reference implementation is available in src/client/examples/fetch-transport.ts in the source package.

import type { FhirTransport } from '@polaris/fhir-de'

// The FhirTransport SPI interface
interface FhirTransport {
  request(
    method: 'GET' | 'POST' | 'PUT' | 'DELETE',
    path: string,          // FHIR path, e.g. '/Patient/123'
    init?: {
      headers?: Record<string, string>
      body?: unknown
      query?: Record<string, string | string[]>
    }
  ): Promise<unknown>
}

Routing policy

baseUrl must point directly to a FHIR server endpoint (e.g. https://aidbox.example.com/fhir). The client appends FHIR resource paths to this URL. Do not use a non-FHIR API proxy as the base URL.

PII-Modus — piiMode

Anonymisierte Antworten mit piiMode: 'anonymized'.

createFhirDeClient akzeptiert ein optionales piiMode-Feld. Der Standardwert ist 'real'. Mit piiMode: 'anonymized' wird der Transport intern in einen AnonymizingTransport eingehüllt, der alle FHIR-Antworten (GET und Write-Echoes) vor der Rückgabe bereinigt. Das Verhalten ist fail-loud: fehlt die Umgebungsvariable POLARIS_ANON_SALT, wirft der Konstruktor synchron.

import { createFhirDeClient } from '@polaris/fhir-de'

// POLARIS_ANON_SALT must be set in the environment before calling this.
// The factory throws synchronously if the variable is missing.
const client = createFhirDeClient({
  baseUrl: 'https://aidbox.example.com/fhir',
  transport,
  piiMode: 'anonymized',
})

// All responses from client.forProfile(...).read/search are now anonymized.
// No further configuration required.

Was anonymisiert wird

  • Strukturelle PII-Felder: Patient.name, .identifier, .address, .telecom, .photo, Practitioner.name usw. werden vollständig entfernt (PII_FIELDS aus @polaris/adapter-common)
  • Freitext-Scrubbing: DocumentReference.description und AllergyIntolerance.note[].text — betitelte Namen (Dr./Hr./Fr.), Datumsangaben (TT.MM.JJJJ), Telefonnummern (+49/0…) und KV-Nummern (A123456789) werden durch Tokens ersetzt
  • ID-Hashing: SHA-256(POLARIS_ANON_SALT + ':' + id) → anon-<12hex> (deterministisch)
  • Referenz-Rewriting: Patient/p-123Patient/anon-<hash> (auch absolute URLs)
  • Display-Felder: subject.display, participant[].individual.display[NAME]
  • Write-Echoes: POST/PUT-Antworten werden ebenfalls anonymisiert

Konfiguration und Anforderungen

  • POLARIS_ANON_SALT muss als Umgebungsvariable gesetzt sein — ein geheimes Salt-String, das die ID-Hashes deterministisch macht
  • Fehlt das Salt, wirft createFhirDeClient synchron bei der Initialisierung — kein stiller Datenleck
  • Das Salt kann alternativ direkt an new AnonymizingTransport(transport, salt) übergeben werden (z. B. in Tests)
  • Die exportierten Konstanten FREE_TEXT_FIELDS und PII_FIELDS definieren den genauen Scrubbing-Scope
  • Freitext-Scrubbing erfasst nur betitelte Namen (Dr./Hr./Fr.) — Bare-Bigrams werden bewusst nicht gematcht, um klinische Termini (z. B. "Diabetes Mellitus") nicht zu scrubben

Polaris platform

Platform integration — what the platform provides.

When building on the Polaris platform, the adapter-sdk (an internal package, separate from @polaris/fhir-de) manages the platform-layer concerns. Application code still uses the same resource builders and extension constants from @polaris/fhir-de.

What the platform handles

  • Aidbox lifecycle and configuration
  • Multi-tenant identity and access control (Practitioner JWT)
  • PVS adapter sync scheduling and reconciliation
  • MCP agent surface with anonymization and audit trail

What your application provides

  • Business logic and UI
  • FHIR resource construction using @polaris/fhir-de builders
  • Domain-specific workflows on top of the FHIR contract
  • Extension constants imported — never hardcoded