shngateway

package module
v0.20.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Jul 4, 2026 License: Apache-2.0 Imports: 0 Imported by: 0

README

shn-gateway

The SHN Smart Gateway is the software a participant runs to join the Smart Health Network. Each organization — a provider, a payer, or another participant — deploys its own gateway. The gateway holds that organization's keys and exchanges healthcare data with other participants through the SHN Hub, with every leg independently authorized and end-to-end encrypted. The network is workflow-general; the first workflow delivered on it is Da Vinci prior authorization.

The gateway is config-only: a single published image, pointed at the SHN discovery endpoint, with your registration bundle mounted. The common case needs no code.

This guide takes two organizations — a provider and a payer — from nothing to a real prior-authorization payload flowing between them through the live SHN Hub.

This guide targets the SHN preview sandbox. SHN is currently in a preview phase, served at shn-preview.org and seeded with synthetic personas (Linda Johansson et al.) so partners can integrate against a live network. While in preview, send only synthetic data — never production PHI. Every member id and DOB below is fabricated test data. When SHN reaches production you point the same gateway at the production substrate by changing SHN_DISCOVERY_URL — nothing else changes.


1. Prerequisites

  • Docker (recommended) or a Go 1.23+ toolchain to run the binary.
  • Access to this repository. It is private; you must be an invited collaborator with a GitHub personal access token (repo scope) or an SSH key on your account.
  • A target substrate. This guide uses the preview sandbox at shn-preview.org. Its discovery endpoint is https://accounts.shn-preview.org/discovery — a single anchor that resolves every other URL (Hub, Authorization Framework, registrar, trust anchors). If you run a private SHN deployment, substitute your own apex throughout; the discovery descriptor is the source of truth for the live URLs.
  • A sandbox invite. The sandbox is invite-gated. Submit the Request access form at https://developers.shn-preview.org (no account needed). When approved you receive an invite email with a temporary password; sign in at the portal and set your password before registering below.
  • A public https endpoint, if you run a responder. A payer (or any role that receives exchanges) must be reachable by the Hub. See §6 the payer side. A provider that only originates does not need this.

Confirm the sandbox is reachable:

curl https://accounts.shn-preview.org/discovery

You should get a JSON descriptor listing endpoints, sandboxResponders, and sandboxPersonas.


2. Install the gateway

Set GOPRIVATE so the Go toolchain does not attempt the public proxy for this private module, and make sure your Git client can authenticate to GitHub:

export GOPRIVATE=github.com/SmartHealthNetwork/shn-gateway

Option A — Docker image (recommended). Clone this repository and build the standalone image (build context is this module; the public shn-sdk resolves from the standard Go proxy):

git clone https://github.com/SmartHealthNetwork/shn-gateway.git
cd shn-gateway
docker build -t shn-gateway .

Option B — Go binary:

go install github.com/SmartHealthNetwork/shn-gateway/cmd/gateway@v0.2.0

The image runs as a non-root user (uid/gid 65532, distroless) — see §5 for the bundle permissions that implies.


3. Register and get your bundle

Before the gateway can run it needs a key bundle — your client identity in SHN. You obtain it by registering with the SHN Accounts service using the shn CLI from the public shn-sdk. Keys are generated client-side; your private keys never leave your machine.

Install the CLI:

# macOS / Linux — detects platform, verifies the published SHA-256, installs to ~/.local/bin
curl -fsSL https://developers.shn-preview.org/install.sh | sh

# …or with a Go toolchain:
go install github.com/SmartHealthNetwork/shn-sdk/cmd/shn@latest

Log in once (token caches at ~/.shn/credentials), then register one client per organization, with the --role that matches how you participate (provider, payer, facility, or phg — see §8). The holder id is server-assigned and printed on success — you'll need it to address exchanges.

Provider (originates exchanges):

shn login --accounts https://accounts.shn-preview.org

shn register --accounts https://accounts.shn-preview.org \
  --role provider --name acme-clinic --base-url https://acme-clinic.example.com \
  -out ./provider-bundle
# → Registered acme-clinic-7f3a. Keys in ./provider-bundle.

Payer (receives and adjudicates exchanges):

shn register --accounts https://accounts.shn-preview.org \
  --role payer --name acme-health --base-url https://gateway.acme-health.example.com \
  -out ./payer-bundle
# → Registered acme-health-2b9c. Keys in ./payer-bundle.

The -out directory holds the bundle the gateway loads: manifest.json (your holder id, role, public keys, baseURL) plus the private key files.

--base-url differs by role — this is the key distinction. It must be an https URL that publicly resolves (the registrar rejects private, loopback, link-local, and unresolvable addresses).

  • A provider that only originates is never dialed by the Hub — use any https URL you control, e.g. your website.
  • A responder (payer, facility, phg) is dialed: the Hub POSTs to {baseURL}/substrate/inbound, so baseURL must be the real, public, TLS-terminated address where your gateway listens, and must not redirect on that path.

List or revoke clients anytime:

shn clients --accounts https://accounts.shn-preview.org
shn revoke acme-clinic-7f3a --accounts https://accounts.shn-preview.org

4. Configure

The gateway is configured entirely by environment variables. SHN_DISCOVERY_URL resolves almost everything else; in a standard sandbox deployment you set only a handful of variables.

Required (every role)
Env var Description
SHN_DISCOVERY_URL The single anchor — resolves substrate endpoints and trust anchors. Sandbox: https://accounts.shn-preview.org/discovery
ROLE provider, payer, facility, or phg. Must match the role you registered.
SHN_SECRETS Path to the bundle directory written by shn register -out.
Validation (required to boot — FR-36)

The gateway refuses to start without a FHIR validator — every resource is validated at the edge before it crosses the substrate. The sandbox discovery descriptor does not advertise a validator, so you must supply one:

Env var Description
FHIR_VALIDATE_URL A FHIR $validate endpoint (a HAPI server with the Da Vinci CRD/DTR/PAS + US Core IGs loaded). The production path.
SHN_FAKE_VALIDATOR Set to 1 to use a no-op validator. Dev only — skips real profile validation. Use for a first wiring smoke test; never in production.

If neither is set (and discovery advertises none), the gateway exits with refusing to run without per-message validation (FR-36).

Recommended: co-locate the validator in your own boundary. Run the IG-loaded $validate as a sidecar alongside the gateway and point FHIR_VALIDATE_URL at it (e.g. http://validator:8080/fhir). Because $validate needs the full (PHI-bearing) resource, co-location keeps PHI inside your boundary — it is never sent to an SHN-operated validator. This is the config-only deployment posture: the gateway image plus a co-located IG-loaded validator, with no separate validator host to stand up.

This repository ships that wiring ready-made: deploy/bundle/compose.yml pairs the gateway with a co-located IG-loaded $validate sidecar as a config-only unit. Clone the repo, set SHN_DISCOVERY_URL / ROLE / SHN_SECRETS, and docker compose -f deploy/bundle/compose.yml up --build — no separate validator host. See deploy/bundle/README.md.

Per-role
Env var Applies to Description
PAYER_DIRECTORY provider Optional override. Path to a JSON file mapping a member's Coverage payor identity ({"system","value"}) to the payer holder id you originate to. When set, it takes precedence over the default feed-derived discovery — a static, provider-maintained map for bootstrap/testing (or when you route to a payer that does not publish its identity in the network feed). Example row: [{"system":"urn:oid:2.16.840.1.113883.6.300","value":"00001","holderId":"payer"}].

Payer routing is coverage-derived, off the network feed by default (FR-G41). A provider gateway resolves the recipient of every payer leg from the patient's own Coverage payor identity — there is no default payer. By default it uses FeedPayerRouter: it indexes the converged /holders feed, where each role=payer holder publishes its operator-attested payer identities (payerIds), and maps Coverage.payor → holder id. This is the drop-in, many-to-many property — a new payer holder self-registers its identity and providers discover it with no config change. Resolution is fail-closed: a miss (no coverage / no parseable payer / no holder claims that identity) fails closed with 422, and an ambiguous identity claimed by more than one holder also fails closed (AI-G12). Set PAYER_DIRECTORY only to override this with a static map.

How a payer publishes its identity into the feed (payer-onboarding path). A payer-identity is operator-attested, never self-asserted (AI-G12/OWD-G11): the applicant claims its payer identities on the Gate-A access request; the operator vouches them at approval (into the org's authorized grant); and Gate-B client registration enforces declared ⊆ authorized before the identity lands in the registrar (UNIQUE(system,value) globally). Only then does a provider's FeedPayerRouter route to it.

Responder roles (payer/facility/phg) need no PAYER_DIRECTORY: they receive at POST /substrate/inbound and reply to whoever the Hub delivers from. A payer adjudicates with built-in sandbox decision logic out of the box — plug in your own via §7.

Networking
Env var Description
PORT Listening port. Default 8080.
HOST Bind address. Default 0.0.0.0.
Observer stream (optional — local tooling)

The gateway can emit a live, structured stream of its own leg, ingress, and validation events over a loopback-only SSE endpoint — a window onto what this gateway is doing, for local tooling (for example, the SHN Kit's flow inspector) rather than another participant. It is off unless configured, and the address it binds must be loopback: the events include the request/response payloads flowing through this gateway's edge, so enabling it exposes the contents of exchanges with your connected systems to whatever process you point it at — treat it like any other local access to your data, not a network-facing feature.

Env var Description
OBSERVER_ADDR Loopback host:port for the observer stream (SSE GET /events, GET /health): structured leg/ingress/validation events including request/response payloads as seen at this gateway's edge. Off unless set; non-loopback values are refused at startup. Intended for local tooling (the SHN Kit flow inspector); enabling it exposes payloads from your connected systems to local processes.
Connect your system of record (optional — see §7)
Env var Description
FHIR_DATA_URL FHIR R4 base URL for your system of record. Omit to use the built-in synthetic stub (carries the sandbox personas, so you can run end to end with no backend).
FHIR_TOKEN_URL SMART Backend Services token endpoint, if your FHIR server requires authenticated access. Requires the client quad below.
FHIR_CLIENT_ID SMART client id.
FHIR_CLIENT_KEY SMART client private key (PEM).
FHIR_CLIENT_ALG ES384 or RS384.
FHIR_CLIENT_SCOPE Requested scope. Default system/*.read.
FHIR_CLIENT_KID Key id for the client assertion JWK, if your server requires it.
SHN_STORE_DATABASE_URL Postgres DSN for durable claim-state storage. Omit for in-memory (non-durable across restarts).
Accept Da Vinci requests from a provider EHR (provider, optional)

Set PROVIDER_DAVINCI_INGRESS=1 to mount the provider-side Da Vinci ingress: the gateway accepts a provider EHR / reference-implementation's native Da Vinci requests — CDS Hooks order-select (Coverage Requirements Discovery), Questionnaire/$questionnaire-package (DTR), and Claim/$submit (PAS) — resolves and inlines the payer's prefetch from your own system of record (non-aggregating; no callback to the provider), and forwards conformant requests through the substrate.

Inbound requests authenticate via SMART Backend Services: the gateway hosts its own POST /oauth/token and GET /.well-known/smart-configuration, verifies a registered client's signed JWT assertion (private_key_jwt, ES384/RS384), issues a short-lived bearer, and verifies it on every ingress call.

Env var Description
PROVIDER_DAVINCI_INGRESS Set to 1 to mount the ingress on the provider gateway.
PROVIDER_DAVINCI_INGRESS_BASE_URL The gateway's public base URL — the SMART audience the gateway pins and the token endpoint it advertises. Required when the ingress is enabled.
INGRESS_CLIENTS_FILE Path to a JSON array of registered inbound clients: [{"client_id":"…","alg":"ES384","public_key_pem":"-----BEGIN PUBLIC KEY-----…","scopes":["system/Davinci.write"]}]. Required (≥1 client) when the ingress is enabled.

Enabling the ingress without a base URL or at least one valid registered client is a hard startup error. (Exposing this ingress on a public edge and full UDAP dynamic client registration are planned future enhancements.)

Advanced overrides (rarely needed)

Each substrate endpoint and trust-anchor key URL is resolved from discovery by default; set the matching variable only to override (e.g. when the gateway runs inside the substrate's own network): AUTHZ_URL, HUB_URL, CONSENT_URL, AUDIT_URL, PHG_URL, REGISTRAR_URL, FHIR_VALIDATE_URL, AUTHZ_PUBKEY_URL, HUB_TRANSPORT_KEY_URL. Explicit env always wins over discovery. NPI overrides the organization NPI stamped into a provider's originated requests (defaults to a synthetic sandbox placeholder).


5. Run

Each organization launches its own gateway with the bundle from §3. Both examples below use the dev validator (SHN_FAKE_VALIDATOR=1) and the built-in synthetic stub for a first run — no validator host, no backend. For production, drop SHN_FAKE_VALIDATOR=1, set FHIR_VALIDATE_URL, and set FHIR_DATA_URL (§7).

Provider — boots ready to originate to its counterpart:

docker run --rm \
  -e SHN_DISCOVERY_URL=https://accounts.shn-preview.org/discovery \
  -e ROLE=provider \
  -e PAYER_DIRECTORY=/etc/shn/payer-directory.json \
  -e SHN_FAKE_VALIDATOR=1 \
  -e SHN_SECRETS=/etc/shn/bundle \
  -v "$PWD/provider-bundle:/etc/shn/bundle:ro" \
  -v "$PWD/payer-directory.json:/etc/shn/payer-directory.json:ro" \
  -p 8080:8080 \
  shn-gateway

# payer-directory.json maps your members' Coverage payor identity to the payer holder:
#   [{"system":"urn:oid:2.16.840.1.113883.6.300","value":"00001","holderId":"acme-health-2b9c"}]

Payer — boots as a responder listening for Hub deliveries:

docker run --rm \
  -e SHN_DISCOVERY_URL=https://accounts.shn-preview.org/discovery \
  -e ROLE=payer \
  -e SHN_FAKE_VALIDATOR=1 \
  -e SHN_SECRETS=/etc/shn/bundle \
  -v "$PWD/payer-bundle:/etc/shn/bundle:ro" \
  -p 8080:8080 \
  shn-gateway

Each gateway boots, loads its bundle, resolves the Hub/authz/registrar and trust anchors from discovery, and listens on :8080. Running the binary instead of the image is the same env, e.g. ROLE=payer SHN_SECRETS=./payer-bundle … gateway.

Run as non-root

The Docker image runs as uid/gid 65532 (distroless nonroot). A self-mounted bundle directory and its key files must be readable by gid 65532: use 0640 for files and 0750 for the directory, with group ownership set to 65532. SHN's provisioning tooling applies these automatically; if you mount your own bundle, set them yourself:

chgrp -R 65532 ./payer-bundle && chmod 750 ./payer-bundle && chmod 640 ./payer-bundle/*

6. Exchange a payload through the Hub (end to end)

Two gateways let two organizations exchange directly. Here a payer stands up its gateway as a responder, and a provider stands up its gateway and originates a prior authorization to that payer — the request travels provider → Hub → payer, and the decision comes back the same way. Neither party sees the other's keys or network; the Hub routes sealed envelopes between them.

The payer side — receive and adjudicate

A payer-role gateway serves POST /substrate/inbound: the Hub delivers sealed exchanges there, and the gateway authenticates the Hub, decrypts, adjudicates, and returns a sealed response. Out of the box it uses the built-in sandbox decision logic — it approves the lumbar-MRI prior auth (UC-03), pends the amend scenario (UC-04), denies UC-08, and answers eligibility for the seeded members. Plug in your own decisioning via §7.

Because the Hub must reach the payer, the payer's registered --base-url has to be a publicly resolvable https endpoint that fronts the gateway's :8080. For a real deployment that's your service's address behind TLS; to test from a laptop, expose the local gateway with a tunnel and register that https URL:

# e.g. with ngrok — then register --base-url with the https URL it prints
ngrok http 8080

Start the payer gateway (§5). The Hub will now POST deliveries to {your-base-url}/substrate/inbound.

The provider side — originate

Start the provider gateway with a PAYER_DIRECTORY that maps your members' Coverage payor identity to the payer's holder id from §3 (acme-health-2b9c in this guide), as in §5. Routing is coverage-derived (FR-G40): there is no default payer — the recipient is resolved per-exchange from the patient's own Coverage. Then originate a coverage-eligibility exchange (UC-01) for a seeded persona:

# Covered persona → routes provider → Hub → your payer → back.
curl -s -X POST localhost:8080/scenario/uc01 \
  -H 'Content-Type: application/json' \
  -d '{"branch":"covered"}'
# → {"covered":true,"reason":""}

Originate the full prior-authorization chain (CRD → DTR → PAS, UC-03):

curl -s -X POST localhost:8080/scenario/uc03 \
  -H 'Content-Type: application/json' -d '{}'
# → {"outcome":"approved","preAuthRef":"…"}

A covered:true / approved response means the full loop worked: your provider gateway built and validated the request, authorized the leg with the Authorization Framework, sealed it, sent it to the Hub, the Hub routed it to your payer gateway, the payer adjudicated and sealed a response, and your provider gateway verified and decrypted it. Two independent organizations just completed a prior authorization through the substrate.

The provider serves the POST /scenario/uc01/scenario/uc08 originate endpoints (UC-06/UC-07 add /start, /complete, /cancel sub-routes for their multi-step flows). For the full persona matrix — including how the clinical answers drive approve vs. pend vs. deny — drive the exchange from the shn CLI or the SDK; see the SANDBOX guide in shn-sdk.

Quick single-machine smoke (built-in sandbox payer)

To confirm a provider gateway works without standing up a payer, point it at the sandbox's built-in payer responder — provide a PAYER_DIRECTORY mapping the seeded personas' Coverage payor identity (urn:oid:2.16.840.1.113883.6.300|00001) to the built-in payer holder — and originate. No payer deployment, no public endpoint required:

# provider gateway running with PAYER_DIRECTORY mapping 00001 → payer
curl -s -X POST localhost:8080/scenario/uc01 \
  -H 'Content-Type: application/json' -d '{"branch":"covered"}'
# → {"covered":true,"reason":""}

7. Integrate your internal systems

By default the gateway uses a built-in synthetic stub for its system of record (the sandbox personas) — which is why §6 works with no backend. To carry your data, point the gateway at your systems through connectors. This applies to both sides: a provider reads the clinical and coverage data it originates from; a payer reads the records its decisioning evaluates.

The common case — point at your FHIR server (no code)

If your backend exposes FHIR R4 (Epic, and increasingly Availity / Surescripts — the CMS-0057 direction), you write no code. Set FHIR_DATA_URL to your US Core FHIR base URL and, if it requires authenticated access, the SMART Backend Services quad:

docker run --rm \
  -e SHN_DISCOVERY_URL=https://accounts.shn-preview.org/discovery \
  -e ROLE=provider -e PAYER_DIRECTORY=/etc/shn/payer-directory.json \
  -e FHIR_VALIDATE_URL=https://your-hapi.example.com/fhir \
  -e FHIR_DATA_URL=https://fhir.your-org.example.com/r4 \
  -e FHIR_TOKEN_URL=https://fhir.your-org.example.com/oauth2/token \
  -e FHIR_CLIENT_ID=shn-gateway \
  -e FHIR_CLIENT_KEY="$(cat client-key.pem)" \
  -e FHIR_CLIENT_ALG=ES384 \
  -e SHN_SECRETS=/etc/shn/bundle \
  -v "$PWD/provider-bundle:/etc/shn/bundle:ro" \
  -p 8080:8080 \
  shn-gateway

Trust anchors, the Hub/authz/registrar, and the consent/audit/PHG planes are all resolved from SHN_DISCOVERY_URL — nothing else to wire.

Payer decisioning

A payer gateway decides eligibility and prior-auth via an Adjudicator. The default reads coverage and clinical facts from its system of record (the stub, or your FHIR_DATA_URL) and applies the sandbox criteria. To run your own coverage and medical-necessity policy, inject a custom shnsdk.Adjudicator through the engine.Config.Adjudicator seam — the same interface the SDK responder uses, so one implementation works in both. See STABILITY.md for the supported engine seam.

Note: The engine.LegResponder interface is an internal, unstable 0.x seam — it may change in any minor version. Do not depend on it directly. engine.Config.Adjudicator is the supported partner injection point and will remain stable across minor versions.

Native-forward payer mode (PAYER_DAVINCI_*)

Setting PAYER_DAVINCI_BASE_URL switches the payer gateway into native-forward mode: the three read-only Da Vinci legs (coverage eligibility, CRD order-select, and DTR questionnaire fetch) are forwarded to a real partner Da Vinci endpoint instead of being adjudicated by the sandbox fallback. PAS (the claim submission legs) also forward when PAYER_DAVINCI_PAS_NATIVE=true is set; without it, PAS falls back to the sandbox adjudicator. A gateway with PAYER_DAVINCI_BASE_URL set and PAYER_DAVINCI_PAS_NATIVE=true is fully native: all five payer legs forward to your partner. A payer Store (SHN_STORE_DATABASE_URL or holdersim) is required when PAYER_DAVINCI_PAS_NATIVE=truebuild() fails at startup otherwise.

Env var Description
PAYER_DAVINCI_BASE_URL Base URL of the partner Da Vinci payer (e.g. https://api.payer.example/davinci). Setting this enables native-forward mode.
PAYER_DAVINCI_TOKEN_URL SMART Backend Services token endpoint for the partner. Required if the partner requires authentication.
PAYER_DAVINCI_CLIENT_ID SMART client id for the partner. Required when PAYER_DAVINCI_TOKEN_URL is set.
PAYER_DAVINCI_CLIENT_KEY SMART client private key (PEM path or inline PEM). Required when PAYER_DAVINCI_TOKEN_URL is set.
PAYER_DAVINCI_CLIENT_ALG ES384 or RS384. Required when PAYER_DAVINCI_TOKEN_URL is set.
PAYER_DAVINCI_SCOPE Requested scope. Default system/*.read.
PAYER_DAVINCI_CLIENT_KID Key id for the client assertion JWK, if the partner requires it.
PAYER_DAVINCI_PAS_NATIVE true to forward PAS submit/update legs to the partner's /Claim/$submit. Default false (sandbox PAS fallback). Requires a payer Store.

All-or-nothing rule: if PAYER_DAVINCI_TOKEN_URL is set, then PAYER_DAVINCI_CLIENT_ID, PAYER_DAVINCI_CLIENT_KEY, and PAYER_DAVINCI_CLIENT_ALG must also be set — a partial credential block is a hard startup error (a likely misconfig). Setting PAYER_DAVINCI_BASE_URL alone (no token URL) is valid and forwards to the partner unauthenticated — the gateway logs a warning on startup to make this mode visible.

The engine continues to own authority enforcement: every forwarded leg is still independently authorized, sealed, and audited through the substrate. The (C) outbound subject fence (fenceResponseSubject) applies to all native-forwarded responses — including PAS submit/update when PAYER_DAVINCI_PAS_NATIVE=true. If the partner returns a response about a different patient than the request, the engine rejects it before sealing (a 403, not a sealed foreign-patient leg).

Provider DTR population (PROVIDER_DTR_*)

On the provider side, the DTR leg fills the payer's questionnaire from the member's clinical data. By default the gateway uses a managed populator that fills the sandbox lumbar-MRI questionnaire from the system of record (the stub, or your FHIR_DATA_URL). To populate arbitrary DTR questionnaires — the real Da Vinci DTR case, where questionnaires carry CQL expressions the gateway does not itself evaluate — forward population to an SDC Questionnaire/$populate engine:

Env var Description
PROVIDER_DTR_NATIVE true to forward DTR population to an SDC $populate engine instead of the managed sandbox populator. Default false.
PROVIDER_DTR_POPULATE_URL The SDC Questionnaire/$populate endpoint. Required when PROVIDER_DTR_NATIVE=true.

A CMS-0057-conformant provider runs its own DTR client and points PROVIDER_DTR_POPULATE_URL at it. A provider without a DTR client yet can point it at a $populate CQL engine you operate (for example a HAPI FHIR Clinical Reasoning server) — the same SDC contract, populated centrally (DTR-as-a-service). Either way the engine keeps authority: the populated QuestionnaireResponse is fenced to the member it was populated for — a response about a different patient is rejected before it can reach PAS — then sealed and audited like any other leg.

Durable claim state

Set SHN_STORE_DATABASE_URL to a Postgres DSN to persist in-flight (pended/resumable) claim state across restarts and replicas, instead of the default in-memory store.

A non-FHIR backend (custom connector)

For a legacy or non-FHIR system of record (HL7v2, X12, SQL, SOAP), implement the engine.SystemOfRecord interface starting from the runnable scaffold. See connectors/scaffold/README.md for the step-by-step: copy scaffold.go, fill the read methods against your backend (deriving the patient identifier via shnsdk.ResolvePCI), and wire your connector through the already-public engine.Config.SoR seam.


8. Roles and endpoints reference

Your --role (at registration) and ROLE (at run) must match. Each role serves a different surface:

Role What it does HTTP surface it serves
provider Originates exchanges (eligibility, prior auth) POST /scenario/uc01…uc08 (+ UC-06/07 sub-routes)
payer Responds to inbound exchanges; serves the CMS-0057 Patient Access API POST /substrate/inbound; GET /metadata, GET /ExplanationOfBenefit[/{id}]
facility Responds to inbound federated-query exchanges POST /substrate/inbound
phg Patient Health Gateway — responds to inbound patient-directed exchanges POST /substrate/inbound

The provider scenario endpoints are an operator surface, not a public one — keep them off the public internet (the Hub never calls them; only your own tooling does). The responder /substrate/inbound is the one the Hub calls, and it authenticates every delivery via the X-Hub-Assertion header before reading the body.


9. Troubleshooting

Symptom Cause and fix
refusing to run without per-message validation (FR-36) at startup No validator configured and discovery advertises none. Set FHIR_VALIDATE_URL, or SHN_FAKE_VALIDATOR=1 for a dev smoke.
fetch discovery: … / fetch hub transport key: … at startup SHN_DISCOVERY_URL unreachable, or the gateway can't reach the substrate hosts it resolves. Confirm curl $SHN_DISCOVERY_URL works from the gateway's network.
Provider originate returns recipient "…" not in registry The payer holder resolved from the member's Coverage (feed payerIds, or your PAYER_DIRECTORY override) isn't a registered holder, or the registrar feed hasn't propagated yet. Confirm the counterpart appears in shn clients / the /holders feed.
Provider originate returns 422 no payer identifier on member coverage / no registered payer for identifier … Coverage-derived routing found no route (FR-G41; no default): the member's Coverage carries no parseable payor identity, or no role=payer holder in the feed claims that identity (and no PAYER_DIRECTORY override maps it). Ensure the target payer holder published that {system,value} in its feed payerIds (payer-onboarding path), or set a PAYER_DIRECTORY override row. An identity claimed by two holders also fails closed (ambiguous — AI-G12).
Provider originate returns 502 (routing / response sender mismatch) The counterpart isn't reachable or didn't respond as itself. Check the payer gateway is running and its registered --base-url resolves publicly to it.
Payer never receives a delivery The Hub can't reach the payer's --base-url. It must be a public https endpoint fronting :8080 and must not redirect on /substrate/inbound. Re-check the tunnel/load balancer.
Payer returns 403 missing or invalid hub assertion The request didn't come from the Hub (e.g. a direct curl to /substrate/inbound). Only the Hub can deliver; originate from a provider instead.
Scenario call returns 400 unknown branch / unknown member The originate body must be {"branch":"covered"} or {"branch":"notcovered"}; the member must exist in the active system of record (the stub has the sandbox personas).
Permission-denied reading the bundle (Docker) The mounted bundle isn't readable by gid 65532. Apply chgrp -R 65532 + 0750/0640 (see §5).
Registration rejected with invalid baseURL: … --base-url must be a publicly resolvable https URL — not private/loopback/link-local. See §3.
egress validation failed (422) on originate The resource your connector produced doesn't conform to its IG profile. Check the issues array in the response; fix the data your SystemOfRecord returns.

10. Scenario tooling and Kit seed packages

Two additional packages ship in this module for driving and seeding a gateway, rather than being the gateway itself:

  • scenariodriver — drives the eight Prior Authorization scenarios (UC-01…08) against a Smart Gateway's edges: the Da Vinci ingress (CRD order-select, DTR $questionnaire-package, PAS $submit) over a UDAP B2B direct-bearer transport, a provider BFF's origination + SDC populate chain, PAS-golden builders, amend/CDex evidence builders, and clients for the provider-data /scenario/* routes, the ops console, and the patient surface. It is consumed by the live conformance gate that exercises a gateway end to end against real Da Vinci reference implementations, and it is the scenario engine the SHN Kit's desktop daemon drives to run the same eight scenarios with no Docker and no CLI.
  • fhirseed — a thin partner/Kit seed loader: waits for a FHIR server's readiness, creates tenant partitions, installs the operated-CQL prepopulation Library fixtures, warms the $populate engine, loads provider-data persona bundles, and writes/polls the seed-complete marker. It carries only the wire-level steps that are safe to publish; the heavy sandbox persona builders it seeds from stay behind that boundary. It also ships a baked provider-tenant persona fixture (fixtures/sandbox-provider-personas.json) so a partner or the Kit can seed the conformant lane's ingress member set with no builder dependency — the fixture is generated from the canonical seed source and drift-guarded there, and the same resources are $validated live wherever a full stack is seeded.

Both packages are evolving surfaces — see STABILITY.md. Pin an exact gateway version if you depend on them; their shapes may change in minor releases until the Kit stabilizes.


11. Further reading

  • STABILITY.md — versioning policy and the supported seam contract.
  • connectors/scaffold/README.md — the custom SystemOfRecord connector walkthrough.
  • shn-sdk — the public participant SDK and shn CLI. Its docs/SANDBOX.md (Discover → Register → Build → Run → Validate) and docs/PARTICIPANT_PROTOCOL.md (the language-neutral wire contract) are the canonical participant references; this gateway implements that same contract.

Documentation

Overview

Package shngateway is the root of the shn-gateway module — the public, partner-deployable Smart Gateway engine. The engine lives in ./engine, its reference FHIR connector in ./connectors/fhirsor, and its private FHIR client in ./internal/fhirclient. It depends only on github.com/SmartHealthNetwork/shn-sdk.

Directories

Path Synopsis
Package app is the federation-capable PUBLIC gateway runtime.
Package app is the federation-capable PUBLIC gateway runtime.
cmd
gateway command
Binary gateway is the PUBLIC, federation-capable Smart Gateway: it boots config-only, loads its `shn register` bundle, federates against the live /discovery + the registrar feed, and runs prior auth for its ROLE.
Binary gateway is the PUBLIC, federation-capable Smart Gateway: it boots config-only, loads its `shn register` bundle, federates against the live /discovery + the registrar feed, and runs prior auth for its ROLE.
connectors
fhirsor
Package fhirsor implements engine.SystemOfRecord by reading a US Core FHIR server (via internal/fhirclient).
Package fhirsor implements engine.SystemOfRecord by reading a US Core FHIR server (via internal/fhirclient).
pgstore
Package pgstore is the durable (Postgres) implementation of the gateway's Store seam — the holder's OWN business state: issued authorization numbers, the pended-claim ledger, and PA-decision EOBs (AI-1: metadata/decision only, never a cross-holder clinical record).
Package pgstore is the durable (Postgres) implementation of the gateway's Store seam — the holder's OWN business state: issued authorization numbers, the pended-claim ledger, and PA-decision EOBs (AI-1: metadata/decision only, never a cross-holder clinical record).
scaffold
Package scaffold is a runnable Tier-3 starter for partners whose backend is NOT a FHIR server (HL7v2, X12, SQL, SOAP).
Package scaffold is a runnable Tier-3 starter for partners whose backend is NOT a FHIR server (HL7v2, X12, SQL, SOAP).
smartauth
Package smartauth lets a gateway authenticate to its FHIR data server via SMART Backend Services (RFC 7523 signed-JWT client-credentials).
Package smartauth lets a gateway authenticate to its FHIR data server via SMART Backend Services (RFC 7523 signed-JWT client-credentials).
composite.go — the compositeResponder routes a CONFIG-DERIVED set of legs to a native LegResponder and every other leg to a fallback.
composite.go — the compositeResponder routes a CONFIG-DERIVED set of legs to a native LegResponder and every other leg to a fallback.
Package fhirseed is the partner/Kit FHIR seed LOADER: a small HTTP client that drives a HAPI FHIR server through the seed sequence a partner (or the desktop Kit's shnkitd) needs before running scenarios — wait for readiness, create tenant partitions, install the operated-CQL prepop Library fixtures, warm the $populate engine, load provider-data persona bundles, and write/poll the seed-complete marker.
Package fhirseed is the partner/Kit FHIR seed LOADER: a small HTTP client that drives a HAPI FHIR server through the seed sequence a partner (or the desktop Kit's shnkitd) needs before running scenarios — wait for readiness, create tenant partitions, install the operated-CQL prepop Library fixtures, warm the $populate engine, load provider-data persona bundles, and write/poll the seed-complete marker.
internal
fhirclient
Package fhirclient is a small read-only FHIR R4 HTTP client (Search and Read).
Package fhirclient is a small read-only FHIR R4 HTTP client (Search and Read).
Package observer turns the engine's ObserverEvent callback into a loopback-served SSE stream — the SHN Kit flow inspector's data source (Kit spec §6.1).
Package observer turns the engine's ObserverEvent callback into a loopback-served SSE stream — the SHN Kit flow inspector's data source (Kit spec §6.1).
Package scenariodriver drives the eight Prior Authorization scenarios (UC-01…08) against a Smart Gateway's edges: the Da Vinci ingress (CRD / DTR $questionnaire-package / PAS $submit, UDAP B2B direct bearer), a real br-provider BFF (origination + SDC populate), the provider-data /scenario/* origination routes, the ops console, and the patient surface.
Package scenariodriver drives the eight Prior Authorization scenarios (UC-01…08) against a Smart Gateway's edges: the Da Vinci ingress (CRD / DTR $questionnaire-package / PAS $submit, UDAP B2B direct bearer), a real br-provider BFF (origination + SDC populate), the provider-data /scenario/* origination routes, the ops console, and the patient surface.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL