Compounding Room Admixture System
Configurable sterile/IV compounding (USP 797/800) where every pillar is independently optional via a capability matrix.
Status: Design (2026-05-27). Not yet implemented. Builds on the worklist port in
web/src/containers/compounding-room/worklist.tsx (route /pharmacy/compounding-room,
kit @medical-kit/pharmacy-ipd/components/work-list with type="compounding-room").
One-liner: Upgrade the compounding-room from a generic single-product dispense dialog into a global-standard IV admixture / sterile compounding workspace — where every pillar is independently optional, driven by config, reusing the CDS engine, acknowledgement system, dose-pattern visualizers, and policy gates already in the repo.
1. Why — the gap vs. global standard
The ported worklist gives us the pipeline (screen → prepare → verify → dispense →
return + stock linkage via createTransactionsWithStockAdjustment / createStockReturn).
It is not at the standard of IV workflow management systems (BD Cato, Omnicell IVX,
Baxter DoseEdge, PharmacyKeeper) or USP <797>/<800> / ASHP guidance. Those rest on
four pillars the generic modal lacks, plus safety/documentation layers:
| Pillar | Global standard | Current state |
|---|---|---|
| Recipe / formula | Ingredient-level base + additives, dose/volume/concentration math | Single product line |
| Barcode (BCMA) | Scan every component before use | None |
| Verification | Gravimetric (weight) or volumetric, tolerance gated | Manual screen/verify only |
| Photo documentation | Step-by-step image capture for remote pharmacist check | None |
| Beyond-use dating | Auto BUD per USP category + stability | None |
| Hazardous (USP 800) | Hood/PPE/CSTD checklist, chemo segregation | None |
| Independent double-check | Forced for high-alert/chemo | None |
| Bulk / batch | Batch prep, cart batching, bulk verify | One row at a time |
Design stance: none of this is forced on. The default config reproduces today’s behavior exactly; each capability is an opt-in toggle so a small clinic, a teaching hospital, and a chemo center each get a different shape from the same code.
2. Configuration-first — compounding_config capability matrix
A single per-scope config object gates everything. Resolution order (most specific wins):
dept → facility → global → built-in default. Stored in Supabase compounding_configs
(read-model / live-editable), authored at /admin/compounding-config (new admin page,
same shape as /admin/cds-rules and /admin/policy-gates).
interface CompoundingConfig {
scope: 'global' | 'facility' | 'dept';
scopeId?: string;
// Each pillar independently optional. All default false except `recipe`.
capabilities: {
recipe: boolean; // ingredient breakdown + calc (default true)
barcodeVerify: boolean; // BCMA scan of each component
gravimetric: boolean; // weight-based verification
volumetric: boolean; // volume-based verification
photoCapture: boolean; // per-step image documentation
beyondUseDate: boolean; // auto BUD + stability
hazardous: boolean; // USP 800 checklist (hood/PPE/CSTD)
independentCheck: boolean; // forced double-check (see policy gate)
bulk: boolean; // batch/cart bulk processing
aiAssist: boolean; // Ollama recommender (per-tenant flag, off by default)
};
workMode: 'standard' | 'supervised' | 'throughput' | 'simulation';
// Tunables (all optional, sane fallbacks)
gravimetricTolerancePct?: number; // default 5
budRules?: BudRulePack; // USP category → hours; per-region overridable
requiredSteps?: CompoundingStep[]; // ordered; subset of the pillar steps
doubleCheckDrugClasses?: string[]; // e.g. ['chemo','tpn','high-alert']
icons?: Record<CompoundingStep, string>; // override step icons per tenant
}
The worklist reads this config once (new hook useCompoundingConfig) and turns each
modal step on/off. A step that is false is not rendered and not required — the
completeness ring and save-guard ignore it.
3. Data model — canonical vs. read-model
Follows the repo’s two-layer rule (see ipd-dispense-cycles.md): the medication domain
service (NestJS, Mongo) is canonical truth; Supabase is the realtime/collab projection.
Canonical (MongoDB, medication service) — extend the existing dispense flow, do not
fork it. New subdocument compounding on ProductDispense / MedicationRequest:
compounding?: {
formula: { base: IngredientLine; additives: IngredientLine[] }; // qty, unit, concentration, lot, expiry
verification: { method: 'gravimetric'|'volumetric'|'visual'; steps: VerificationStep[] };
photos: { step: CompoundingStep; fileId: string }[]; // filestore/IPFS ids
beyondUseAt?: string;
hazardClass?: 'none'|'usp800'|'chemo';
preparedBy?: Ref; verifiedBy?: Ref; checkedBy?: Ref; // independent check
batchId?: string; // bulk grouping
}
Read-model (Supabase, projected by inventory-orchestrator / encounter-orchestrator):
| Table | Purpose |
|---|---|
compounding_configs |
the capability matrix above (authored, not projected) |
compounding_worksheets |
realtime worklist projection + presence (cursors / per-row edit-lock, dynamic-sheet PresenceProvider pattern) |
compounding_verification_log |
append-only audit (who/what/when per step + photo + scan + weight) |
No frontend writes to read-model tables — writes go through the medication service; the
orchestrator projects to Supabase; the worklist subscribes via department_queues
(dept_type='compounding-room') for realtime refresh.
4. Hooks (mirrors periops-kit perioperative-status-kit)
Pure calculation + a `` each, sharing one 30 s tick via useNow (reuse
packages/periops-kit/.../perioperative-status/useNow.ts). New module
packages/medical-kit/src/compounding/hooks/:
| Hook | Computes | Drives |
|---|---|---|
useCompoundingConfig(scope) |
resolved capability matrix | which steps render at all |
useCompoundingFormula(order) |
ingredient lines from order + drug master | recipe table |
useDoseCalc(formula) |
dose ↔ volume ↔ concentration (final conc, total volume, overfill) | calc panel + warnings |
useGravimetricCheck(expected, measured, tol) |
within/over/under tolerance | tolerance dial badge |
useBeyondUseDate(formula, budRules) |
BUD timestamp + countdown | expiry badge / ticker |
useCompoundingCompleteness(steps, config) |
% of required (config-gated) steps done | `` + save-guard |
useBulkSelection(rows) |
multi-select + per-action eligibility | bulk toolbar |
useHazardChecklist(config) |
USP 800 checklist state | hood/PPE/CSTD badges |
Calc lives in a framework-agnostic packages/medical-kit/src/compounding/calc/ (unit
conversion, concentration math, overfill, BUD tables) so it is unit-testable and reusable
backend-side. Hooks are thin wrappers over the calc module + useNow.
5. Visualization + icons
Reuse, don’t reinvent:
- Dose patterns — reuse
ui-kit/.../dynamic-scheduler-2/summary-container/TimePerDay.tsxdosePatternKind.tsfor infusion/bolus/continuous visualization in the modal summary.
- New compounding visuals (small additions in the compounding module):
- bag/syringe fill gauge (volume vs. container capacity),
- concentration bar (mg/mL with safe-range band),
- gravimetric tolerance dial (green / amber over / red under),
- step-progress tracker + photo gallery per step,
- `` (reuse periops-kit) in the worklist row and modal footer.
Icons — MUI v7 icon set, with per-step overrides via config.icons:
ScienceOutlined (formula), QrCodeScannerOutlined (barcode), ScaleOutlined
(gravimetric), PhotoCameraOutlined (photo), ScheduleOutlined (BUD),
MasksOutlined/WarningAmberOutlined (hazardous), HowToRegOutlined (double-check),
ChecklistOutlined (bulk). Steps that are config-off don’t render their icon.
6. Work modes (the teaching-hospital vs. real-hospital answer)
config.workMode reshapes the same modal — no fork:
| Mode | For | Behavior |
|---|---|---|
standard |
small clinic | minimal: recipe + verify + dispense, double-check off |
supervised |
teaching hospital | technician/resident prepares → preceptor independent verify+sign always on; teaching annotations; competency capture to compounding_verification_log |
throughput |
real / high-volume hospital | cart/batch prep, barcode-first fast path, double-check forced only for doubleCheckDrugClasses, BUD enforced |
simulation |
teaching / onboarding | full UI, no real stock decrement, no canonical write — practice runs logged separately |
Mode is a config field, so a teaching hospital can run supervised on the chemo dept and
throughput on general IV from the same deployment.
7. Roles + independent double-check (policy gate, not hardcoded)
Map to existing RBAC: technician (prepare), pharmacist (verify/release), preceptor
(teaching co-sign), nurse (receipt). The independent-check rule is a policy gate row
(policy_gates, /admin/policy-gates) keyed on drug risk class — not an if in the
component. New gate triggers: compounding_release, compounding_independent_check,
compounding_simulation_to_live. Gate hard_stop + policy_gate_overrides (override
protocol, 20260527d) handle the rare break-glass with audit.
8. Allergy + CDS — no bespoke subsystem
Do not rebuild the old mockup’s allergy subsystem. Allergies are clinical → MongoDB
canonical, projected to Supabase for realtime surfacing. Run ingredient allergy /
incompatibility / dose-range checks through the existing CDS engine
(src/services/cds/cdsEngineDb.ts + cdsDispatcher.ts, edge fn
infrastructure/medbase/functions/evaluate-cds-rules, admin /admin/cds-rules). Each
additive’s drug code is fed to CDS on formula change. Close the noted “allergy-vs-order
conflict” CDS gap and add seed rules: incompatibility pairs, max-concentration,
hazardous-handling-required. Config-editable, consistent with the rest of the system.
9. Alerting — reuse the alert surface + ack
Critical hits (allergy match, dose over-range, incompatibility, expired component) surface:
- inline in the modal via
InlineCdsAlertBadge(src/common/components/medical/surface/cds-alert/InlineCdsAlertBadge.tsx), - globally via
CdsAlertSurface(FAB/drawer/toast), - as an
AcknowledgementRequest(src/services/ever-foundation/acknowledgement-request/) with the existing escalation chain for criticals.
No new alerting plumbing.
10. Bulk processing
Gated by capabilities.bulk. useBulkSelection adds a worklist toolbar:
- bulk screen / claim (assign self as screener across selected rows),
- batch prepare — group selected orders sharing a base into one
batchId, one prep session, shared photo set, - cart batching — reuse the dispense-cycle cart-batch model (
ipd-dispense-cycles.md), - bulk verify/release — one independent-check action over a batch (policy gate still evaluated per row; any blocked row drops out of the batch with a reason).
Stock for a batch is reserved/decremented through the inventory-orchestrator edge fn
(bulk path) rather than N single calls. Bulk actions are append-only logged per row in
compounding_verification_log so a batch never hides an individual failure.
11. AI / Ollama — recommender-only, hard safety line
Gated by capabilities.aiAssist + the per-tenant LLM feature flag. Follows the repo’s
established AI stance (Revenue Platform Master Plan + e-Kardex): recommender-only, async,
never blocking, no PHI/free-text in prompts (structured codes only), per-inference audit,
named clinical owner. Allowed:
- draft the compounding worksheet from the order (human edits/accepts),
- suggest the dose-pattern kind + a second-opinion incompatibility note shown alongside (never replacing) the deterministic CDS rule,
- optional advisory vision-OCR on the prep photo (“looks like the wrong vial”) — hint only.
Hard rules: AI cannot release, cannot gate, cannot hide/demote a CDS alert. Gravimetric / barcode / dose verification stay 100% deterministic — AI never closes a verification step or a release.
12. Phased rollout (every phase independently shippable + optional)
| Phase | Scope | Demo-ready? |
|---|---|---|
| P0 | compounding_configs table + useCompoundingConfig + /admin/compounding-config (everything defaults to today’s behavior) |
infra |
| P1 | Recipe modal: useCompoundingFormula + useDoseCalc + calc module + recipe table/visuals (gated by capabilities.recipe) |
✅ |
| P2 | CDS allergy/incompatibility wiring + alert surface + ack (seed rules) | ✅ |
| P3 | BUD (useBeyondUseDate) + completeness ring + save-guard |
✅ |
| P4 | Verification: gravimetric/volumetric + barcode (BCMA) + tolerance dial + compounding_verification_log |
✅ |
| P5 | Photo capture per step (filestore/IPFS) + gallery | ✅ |
| P6 | Work modes (supervised/throughput/simulation) + independent-check policy gates |
— |
| P7 | Bulk: useBulkSelection + batch prepare + cart batching + inventory-orchestrator bulk path |
— |
| P8 | USP 800 hazardous checklist (useHazardChecklist) + chemo segregation |
— |
| P9 | AI/Ollama recommender (worksheet draft, second-opinion, vision hint) — behind flag + compliance preconditions | — |
P1+P2+P3 = the high-value, demo-ready first slice. Each phase reads its own
capabilities.* flag, so partial deployment is always valid.
13. File plan (new module — additive, nothing existing rewritten)
web/packages/medical-kit/src/compounding/
├── calc/ # framework-agnostic: unit conv, concentration, overfill, BUD tables
│ └── index.ts
├── hooks/ # useCompoundingConfig / Formula / DoseCalc / GravimetricCheck / BeyondUseDate / Completeness / BulkSelection / HazardChecklist
├── components/
│ ├── CompoundingModal.tsx # config-driven step host (renders only enabled steps)
│ ├── RecipeTable.tsx
│ ├── visuals/ # FillGauge / ConcentrationBar / GravimetricDial / StepTracker / PhotoGallery
│ └── BulkToolbar.tsx
└── index.ts
# wiring (small edits, not rewrites):
web/.../pharmacy-ipd/components/work-list/index.tsx # mount CompoundingModal when usingPage==='pharmacy' && config gates it
infrastructure/medbase/migrations/2026XXXX_compounding.sql # compounding_configs + worksheets + verification_log
infrastructure/medbase/functions/inventory-orchestrator # bulk stock path (extend)
infrastructure/medbase/functions/evaluate-cds-rules # compounding seed rules (extend)
services/medication/.../ # ProductDispense.compounding subdoc + release action
web/src/common/components/super-admin/compounding-config/CompoundingConfigPage.tsx
The current dispense modal remains the fallback when all capabilities.* are off — i.e.
zero behavior change unless a tenant opts in.
14. Open questions
- Gravimetric verification needs a hardware scale integration (serial/HID/network) — in scope now, or manual weight entry first and device adapter later?
- Barcode source — GS1 DataMatrix on vials vs. internal SKU barcodes; which master?
- BUD rule pack: ship USP
<797>default categories, or require per-tenant authoring from day one (per-region like the kaigo/philhealth packs)? - Photo storage: filestore/IPFS (existing) vs. a dedicated WORM bucket for compounding evidence retention.
- Does
simulationmode need a separate competency/grading model, or is thecompounding_verification_logenough for teaching sign-off?