Resources & Tech · eRx Solution

The vendor is a channel. The chart is the record.

e-Prescribing in eKlotho is built as a gateway: a seven-method vendor protocol implemented by a DoseSpot REST client and a Photon Health GraphQL client, dispatched per organization from an encrypted credential row. Everything that matters clinically — the prescription state machine, the drug-safety chain enforced at signature, the append-only controlled-substance log, the chart med-list write-back, the refill and change-request queues — is platform code that behaves identically whichever vendor carries the message, and degrades honestly when none is configured.

Overview

The engineering record behind a prescribing engine that treats e-prescribing vendors as swappable transmission channels behind one gateway.

Technology stack by layer
LayerWhat runs thereWhy
Vendor protocolA Python structural Protocol with seven methods and vendor-neutral dataclassesauthenticate, send, cancel, search pharmacies, check formulary, initiate identity proofing, respond to refills — every payload crossing the boundary is a typed dataclass, so neither vendor's wire format leaks into platform code.
DoseSpot clienthttpx REST client with static header authentication and sandbox/production hostsCredentials are a three-part clinic:user:key string validated at parse time; a malformed credential is reported as an unconfigured vendor rather than a mysterious downstream authentication failure.
Photon Health clientGraphQL over httpx with OAuth2 client-credentialsAccess tokens are cached in Redis keyed per organization with a TTL shaved under the vendor's expiry; a 401 evicts the token and retries exactly once with refresh disabled, so an invalid credential fails fast instead of looping.
Gateway dispatchAn async context manager that resolves the organization's vendor row and instantiates the right clientOne lookup, one decryption, one client. Send returns the vendor name alongside the result so the prescription records which channel carried it — later cancels and refill responses target the originating vendor even after a practice switches.
APIFastAPI routers on the shared backend — prescriptions, queues, config, pharmacy, formulary, webhooksThe prescribing surface is ten routers on the same async backend the EMR and the network portal use; there is no separate prescribing service to keep consistent with the chart.
DatabaseMySQL 8.0 via SQLAlchemy 2.0 async, with Alembic migrations for every eRx tableThe prescribing event (rx_orders), the vendor config, the refill queue, the controlled-substance log, the transmission log and the drug catalogs are each their own migrated table with explicit unique constraints doing the idempotency work.
Credential storageFernet symmetric encryption with a platform key from the environmentVendor API keys are encrypted before they touch a row and are never returned by any endpoint — the admin config API answers with booleans about what is on file, not values.
TerminologyLive NLM RxNav proxy for RxNorm search; NDC catalog and NCPDP pharmacy directory cached in MySQLRxNorm's full catalog is hundreds of megabytes updated monthly, so drug search proxies the public API with a hard timeout and an empty-result fallback that lets the composer accept free-text names; NDC and pharmacy rows the platform owns are local tables.
EMR frontendNext.js App Router pages for the queue hub, refills, change requests, transmissions and vendor config, with a 546-line composer dialogThe composer drives the per-check safety endpoints live — interaction, allergy, controlled, dispense-limit — and the queue pages are thin views over the same list endpoints the tests pin.
Architecture

How the engine actually works

Each section describes a mechanism that exists in the codebase today, not a pattern we admire.

Swappable channels

One gateway, two vendors

The gateway is an async context manager: it selects the organization's single vendor-config row, Fernet-decrypts the credentials, and instantiates either the DoseSpot REST client or the Photon GraphQL client — both implementing the same seven-method protocol. A missing row, a missing platform encryption key or a malformed credential all raise the same typed unconfigured error, which callers translate into honest degradation rather than a 500.

Truthful states

The prescription state machine

A prescription lives in rx_orders and moves DRAFT → SIGNED → TRANSMITTED → FILLED, with CANCELED, VOIDED, QUEUED, TRANSMIT_FAILED and EXPIRED as the honest side-exits. QUEUED is the deliberate one: a signed prescription whose organization has no vendor configured is held there with its transmit timestamp empty — TRANSMITTED is set only when a vendor actually accepted the handoff, so the record never claims more than what happened.

The chart owns the checks

Drug safety enforced at signature

The composer calls per-check endpoints live — interaction, allergy, controlled-substance, dispense-limit — but the enforcement that counts runs server-side at signing, in a fixed order: allergy match, contraindicated-interaction block, quantity ceiling parsed from the sig, then a pediatric age floor. Each block can be overridden only with force plus a recorded reason, and each override is audited. Signing then updates the chart med list and, for a controlled drug, writes the append-only audit row.

An unedited trail

Controlled substances

Every prescription is classified against the schedule catalog at check time and again at signing. Schedule II forces zero refills; Schedules III through V cap at five; and a controlled drug prescribed by someone with no active DEA registration is a hard block with no override path — the one check that force cannot bypass.

Inbound without duplication

Queues, webhooks and one-shot decisions

Refill requests arrive as vendor webhooks that are HMAC-verified against the organization's own decrypted webhook secret using constant-time comparison, and are replay-safe: the vendor's request id is a unique column, and a redelivered event is answered with an acknowledged-duplicate response instead of creating a second work item. The refill list joins through the patient with the organization id inside the join condition, so cross-organization rows drop out at the SQL level, and the approve and deny actions re-check panel access before acting.

One med list, by rule

Chart write-back and reconciliation

Signing runs a three-rule sync against the chart med list: a row already linked to this prescription is refreshed in place; otherwise an active row with the same drug and strength is adopted and re-linked, so a renewal stays one entry while a different strength gets its own; otherwise a new linked row is inserted. The sync is deliberately best-effort — a failure logs and does not undo the signature — and cancel or void stops the linked medication rather than deleting history.

Same 404 for every stranger

The transmission record and its scoping

prescription_transmission is the message log: direction, message type — new prescription, cancel, refill request and response, change request and response, fill, medication history — plus payload, acknowledgment code and error text. The transmissions dashboard buckets prescriptions by state (pending, submitted, accepted, failed, cancelled) and the detail view counts outbound attempts and surfaces the last acknowledgment and error from the event list.

Data model

The records underneath, and the rules that hold them true

Core entities and their invariants
EntityWhat it holdsRule that always holds
rx_ordersThe prescribing event itself — drug, RxNorm code, strength, sig, dispense quantity, refills, prescriber, pharmacy and stateTRANSMITTED requires a vendor to have accepted the handoff; transmitted_via names the carrying vendor for the life of the record
erx_vendor_configOne row per organization naming the vendor and holding Fernet-encrypted credentialsentity_id is unique — an organization has exactly one active vendor configuration at a time
erx_refill_requestsPharmacy-originated refill requests with status, decider, decision reason and timestampsvendor_request_id is unique, so a replayed webhook can never create a second work item
controlled_rx_logAppend-only DEA audit trail for controlled prescriptions with schedule and DEA snapshotOne row per prescription, no update timestamp — written at signing and never edited afterwards
prescription_transmissionThe message conversation per prescription — direction, message type, payload, acknowledgment code, error textEvents are only appended; the dashboard derives attempts and last-acknowledgment by reading, never by mutating
drug_interaction_rulesCurated interaction pairs with severities, from contraindicated down to minorGlobal rows carry a null organization id; an organization's own row wins for the same drug pair
controlled_drug_catalogIngredient-to-schedule classification used by the controlled-substance checksSchedule II classification forces zero refills allowed on any prescription that matches it
patient_medicationsThe chart med list the EMR displays — linked back to the prescription that produced each rowA renewal of the same drug and strength updates the linked row; a different strength is a new row
pharmacy_directory / pharmacy_catalogNCPDP-keyed pharmacy records — a global directory plus the per-organization catalog tenants can maintainLookups resolve entity rows first, then global, deduplicated by NCPDP id — never an N+1 query
medication_history_importImported outside-pharmacy history held for clinician reconciliation before it can touch the chartA row reaches the chart only through an explicit merge that records the resulting prescription id
formulary_cachePayer-and-NDC keyed coverage rows with tier, prior-auth, step-therapy and quantity-limit flagsUnique per payer and NDC pair; a patient without resolvable plan data answers unknown rather than guessing
med_adherence_scorePer-patient, per-drug-class proportion-of-days-covered over 90, 180 and 365 daysOne row per patient and drug class, recomputed from fill data rather than accumulated
Interfaces

What it exchanges, and in which direction

Optional integrations degrade gracefully: with no key configured the product still runs, it just does less.

DoseSpot

Bidirectional

REST client behind the gateway protocol: send, cancel, pharmacy search, formulary, identity-proofing initiation and refill responses, with webhook intake HMAC-verified per organization.

Photon Health

Bidirectional

GraphQL client with OAuth2 client-credentials, Redis-cached tokens per organization and single-retry refresh on 401; webhook events map vendor prescription statuses onto the platform state machine and carry fill dates.

NLM RxNav (RxNorm)

Inbound

Live drug search proxy with a six-second timeout and a maximum of ten results; a numeric query resolves the exact concept first, and any upstream error returns an empty list so the composer can fall back to free-text drug names.

EMR chart

Bidirectional

The composer reads the chart's medication and allergy lists for safety checks, and signing writes the med list back through the three-rule sync — one patient record shared with the rest of the platform.

NDC catalog & pharmacy directory

Inbound

Locally cached MySQL tables serving NDC lookups and the pharmacy picker's fallback path, so drug detail and pharmacy selection keep working when the vendor is unreachable.

Security

How access is decided and recorded

Credential storage
Vendor API keys, secrets and webhook secrets are Fernet-encrypted with a platform key before persistence; plaintext is never stored and never returned.
Config read-back
The admin configuration endpoint answers with booleans — whether a key, secret or webhook secret is on file — never the values; partial updates keep stored values unless explicitly cleared.
Permission ordering
The admin-write requirement on the config endpoint is a dependency evaluated before the request body, so an unauthorized caller gets a 403 without the server ever echoing or validating what they posted.
Webhook authentication
Per-organization webhook secrets verify vendor signatures with constant-time comparison; a signature mismatch is a 403 and an organization without a secret configured rejects rather than accepts.
Entity and panel scoping
Queue and dashboard queries carry the organization id inside the join and apply the provider's patient panel; detail endpoints return an identical not-found answer for missing, foreign and off-panel ids.
Controlled-substance audit
The controlled prescription log is append-only with the prescriber's DEA number snapshotted at signing; the unique prescription key makes double-signing a no-op instead of a duplicate audit row.
Transmit audit
Every transmit and queue action emits an audit event recording the actor, the prescription, the resulting status and the destination pharmacy id — the platform is designed for HIPAA obligations under a BAA.

Infrastructure is designed for HIPAA obligations under a BAA. We document the controls and hand over the runbooks; we do not claim a certification that does not exist for software.

Reliability

What keeps it correct under load

Webhook replay dedupe
The vendor's request id is a unique column; a pre-check answers redelivered events with an acknowledged-duplicate response, and the Photon path additionally catches the constraint violation as a fallback.
Honest QUEUED state
Transmit with no configured vendor logs a warning and holds the prescription in QUEUED with an empty transmit timestamp — TRANSMITTED is set only when a vendor name came back from the gateway.
One-shot decisions
Refill approve and deny reject a request that is no longer pending with a 409, so two staff members cannot silently overwrite each other's decision; the recorded decision keeps its reason and decider.
Token refresh without loops
A 401 from Photon evicts the cached token and retries exactly once with refresh disabled, so an invalid credential fails fast instead of hammering the vendor's auth endpoint.
Pharmacy search fallback
A vendor error or unconfigured gateway falls back to the local directory, merging the organization's catalog, the global catalog and the directory ranked in that order and deduplicated by NCPDP id.
Pharmacy-name resolution
One canonical resolver turns NCPDP ids into names in exactly two queries across the catalog and directory — never per-row lookups — and omits unknown ids so callers cannot leak raw identifiers into the UI.
Med-list sync by rule
Three ordered rules — refresh the linked row, adopt the active same-drug-same-strength row, else insert — keep renewals as one med-list entry while a strength change gets its own row; failures log without undoing the signature.
Sig-derived quantity ceiling
The dispense limit is parsed from the sig on the server and returned to the composer as a maximum, so the enforced ceiling and the instructions the pharmacy reads can never disagree.
Development record

The documents this was built from

Analysis, design discussion, implementation notes and QA written while the work happened. These are engineering artifacts in the product repository, not published pages — listed here so you can see what exists and ask for any of it.

Development-time documentation for eRx Solution
DocumentKindDateWhat it covers
Wedge B — e-Prescribing Integrationdocs/e-Prescribing/eKlotho EMR — Wedge B_ e-Prescribing Integration.htmlDesign2026-05-26The founding architecture record: why prescribing is an EMR-core function, the three-layer integration model, the DoseSpot-versus-Photon comparison, the end-to-end prescription workflow and the phased scope.
Bringing e-Prescribing In-Housedocs/eKlotho Nexus — Bringing e-Prescribing In-House.htmlAnalysisA strategic build-versus-rent memo on becoming a network-certified prescribing application directly — the certification, regulatory and audit obligations involved — concluding it is a 12-to-18-month undertaking kept on the long-term roadmap.
DoseSpot API engineering guidedocs/erx-dosespot-api-guide.htmlGuideEngineering onboarding for the gateway: architecture, sandbox and production application tracks, credential wiring through the admin config API, a seven-step smoke test, the California regulatory checklist and a current-state gap table.
EMR module — e-Prescribingdocs/docs-module-emr-erx.htmlReferenceThe module reference for the P3 prescribing build: the five screens, requirements around network connectivity, RxNorm and NDC data, formulary sources and which organization types the module serves.
Photon Health integration guidedocs/docs-module-emr-erx-photon.htmlGuideA ground-up guide to the Photon vendor path: system components, credential and environment setup, entity setup, the write-a-prescription and patient-sync workflows, webhooks, refills and the status mapping.
Medications module referencedocs/docs-module-medications.htmlReferenceThe network-side medications module the prescribing engine feeds: the longitudinal RxNorm-coded medication list, formulary lookups, adherence measures and interaction checking as seen from the HMO platform.
Client QA report — eRx worklistsdocs/client-report-2026-07-29.mdQA2026-07-29Client-reported findings on the refill queue and worklists, each fixed and verified: an inert refresh control made honest with a spinner, and search added across the eRx queue pages — with the fix widened to a sibling page the report missed.
Client improvement items — Labs, eRx, Imagingdocs/client-report-2026-07-28-emr-dev-4005.mdQA2026-07-28Seven client improvement items including linking the patient-profile prescription history to the central eRx queue, each documented with current state, decision and disposition.
QA verification — vendor config behaviordocs/qa-robert-chen-0617-verify.mdQA2026-06-17Verification pass covering the vendor-config endpoint's empty state: an unconfigured organization now answers a clean not-configured payload instead of a console-noise 404, pinned by tests.
Glossary

Terms used on this page

eRx
Electronic prescribing — composing, signing and transmitting prescriptions to pharmacies electronically.
Surescripts
The national e-prescribing network. The configured vendor holds the network certification and carries the transmission; the platform keeps the clinical record.
NCPDP ID
The national pharmacy identifier used to route a prescription and to key the pharmacy directory and catalog tables.
RxNorm
The NLM's normalized drug vocabulary; the composer's drug search resolves names to RxNorm concepts via the public RxNav API.
RXCUI
An RxNorm concept unique identifier — the code stored on the prescription for the chosen drug concept.
NDC
National Drug Code — the package-level drug identifier cached in the local catalog and used for formulary lookups.
Sig
The patient instructions on a prescription; the server parses it to derive the enforced dispense ceiling.
DAW
Dispense as written — the prescriber's instruction forbidding generic substitution.
EPCS
Electronic prescribing of controlled substances — identity proofing and audit obligations, initiated through the configured vendor.
DEA schedule
The federal controlled-substance classification (II–V) driving refill caps and the hard DEA-registration requirement.
PDMP
Prescription drug monitoring program — a state-run controlled-substance history database (CURES in California).
Formulary
A payer's covered-drug list with tiers; the platform surfaces tier, prior-auth and step-therapy flags as advisory context.
Prior authorization
A payer's requirement that a drug be approved before it is covered — surfaced as a flag on formulary results.
Step therapy
A payer rule requiring cheaper alternatives be tried first — likewise surfaced as an advisory flag.
PDC
Proportion of days covered — the adherence measure computed per patient and drug class from fill data.
Refill request
A pharmacy-originated request for more refills, arriving by webhook into the one-shot decision queue.
Change request
A pharmacy's proposed modification to a prescription, approved or denied with a recorded reason.
ACK code
The acknowledgment code on a transmission event — accept or error — stored with each message in the transmission log.
Fernet
The symmetric encryption scheme protecting vendor credentials at rest under a platform key.
HMAC
The keyed-hash signature scheme used to verify that a webhook genuinely came from the configured vendor.
Questions

Asked by the people who evaluate this

Why a gateway instead of integrating one vendor deeply?

Because the vendor is the most replaceable part of the system. The gateway keeps every clinical behavior — safety checks, state machine, queues, audit — in platform code, and reduces a vendor switch to a credentials change. The transmitted_via column keeps history correct across the switch.

What happens when no vendor is configured?

Typed, honest degradation: transmit holds the prescription in QUEUED with an empty timestamp, pharmacy search falls back to the local directory, and vendor-dependent lookups answer unavailable. The same pattern the platform uses for every optional integration — a missing key is a warning, never a crash and never a false success.

Where does the drug knowledge come from?

Curated platform tables — interaction pairs and controlled-substance schedules — with per-organization overrides, plus live RxNorm search from the NLM. There is deliberately no licensed commercial drug database in the loop, and the procedure vocabulary that requires a commercial license is never bundled.

How is a webhook kept from double-applying?

The vendor's request id is a unique column. A replayed delivery is answered as an acknowledged duplicate, and the constraint catches anything that slips past the pre-check. Signatures are verified with constant-time comparison against the organization's own webhook secret.

What keeps the chart med list from filling with duplicates?

The three-rule sync at signing: refresh the row already linked to this prescription, otherwise adopt the active row with the same drug and strength, otherwise insert. Renewals collapse to one entry; a strength change is deliberately a new row because it is a different order.

How is access to prescribing data scoped?

Organization id inside the join conditions, provider panels applied where they exist, and identical not-found responses for missing, foreign and off-panel ids — the error shape never confirms a record exists. Mutations re-check panel access at the action, not just the list.