PropelRx is McKesson’s pharmacy management software — the system Canadian pharmacists use to fill prescriptions, track inventory of controlled substances, and meet provincial dispensing requirements. Lightspeed X is a retail POS — the system the cashier rings vitamins, supplements, and over-the-counter products through.
At Greenbrook, the same store sells both. A customer walks in to pick up a script, and while they’re there, they grab a bottle of magnesium. The pharmacist fills the script in PropelRx. The cashier rings the whole order in Lightspeed X. The cashier should not see PropelRx. The auditor should never have to ask whether the cashier could have.
The integration is two characters wide: UPC and price. Everything else stays where it belongs.
The problem with a naive sync
The obvious shape of this integration — pull prescriptions from PropelRx into Lightspeed, mark them as products, ring them through — is also the wrong shape. PropelRx prescriptions are not products. They are patient records with prices attached. Treating them like SKUs in your retail POS means:
- The patient’s name flows into a retail loyalty profile.
- The drug name appears on the cashier’s screen.
- The prescription number, fill date, refill count, and dispensing pharmacist sit in a queryable table that the POS vendor (and its hosting vendor, and its analytics vendor) can read.
- You’ve just exported PHI to four systems that have no business holding it.
Under PIPEDA — and under the provincial pharmacy acts that govern dispensing in Canada — most of those are reportable breaches. Under HIPAA, if you operate cross-border at all, every one of them is.
The right shape: a one-way label bridge
The bridge we ship does exactly one thing. When PropelRx prints a prescription label, we intercept the print event and push two fields to Lightspeed: the label’s UPC and its price. That’s it. No drug name. No patient identifier. No prescription number. No refill metadata. Nothing the POS needs to fulfill a sale that isn’t already on the printed barcode the customer is carrying.
// PropelRx-side event, simplified
onLabelPrint(label) {
bridge.send({
upc: label.upc, // sticker barcode
price: label.totalPrice, // patient pays at till
// Everything below intentionally absent.
// patientName ↠never sent
// drugName ↠never sent
// rxNumber ↠never sent
// pharmacistId ↠never sent
});
} On the Lightspeed side, this lands as a transient SKU scoped to a single transaction window — call it a “line voucherâ€. The cashier scans the customer’s label barcode, the line voucher resolves to a price, the sale rings through, the voucher expires. The POS never knows what was on the label.
Architecture, end to end
Four moving parts. None of them touch the PropelRx patient database.
- PropelRx local agent. A small daemon on the pharmacy workstation that subscribes to the label-printer queue. PropelRx writes labels via the OS print spooler; we tail the spooler, parse the label payload, extract the two fields, and emit a bridge event. No reads against the PropelRx database itself — we never have a credential for it.
- Outbound queue. Events are written to a local SQLite WAL queue first, then drained to our cloud bridge over mTLS. If the network drops, the local queue holds. When it comes back, the queue drains in order. Every event has a UUID, a timestamp, and a signed digest of the two fields.
- Bridge service (Cloudflare Workers). Receives the event, validates the signature, persists the event to an append-only audit log, and forwards to Lightspeed via its inventory API. The bridge has no read access to anything in the pharmacy.
- Lightspeed voucher API call. Creates
a one-shot line item against a hidden product called
RX-VOUCHER-PASSTHROUGH, with the UPC and price as variant attributes. Scoped to expire in 24 hours regardless of whether it’s scanned.
The boundary the auditor sees
Every external review of a pharmacy integration starts with the same question: what data crosses the line, and where is the line drawn? Our answer is a one-page diagram and a queryable audit log. Both are written down before the bridge ships, and both stay true for the life of the integration.
The boundary contract reads, verbatim, on the architecture doc that ships with the bridge:
PROPELRX ↔ LIGHTSPEED X BRIDGE — BOUNDARY CONTRACT v1.2
CROSSING THE LINE (PHI → RETAIL):
- UPC string, ≤ 13 chars, from label barcode
- price decimal, ≤ 8 digits, from label total
NEVER CROSSING THE LINE:
- patient name, DOB, address, phone, email
- drug name, dosage, NDC, DIN
- prescription number, refill count, fill date
- pharmacist ID, prescriber ID, insurer ID
- any free-text field from the PropelRx record
DIRECTION:
PropelRx → Bridge → Lightspeed. No reverse path.
AUDIT:
Every event signed (HMAC-SHA256), logged append-only
in two regions, retained 7 years, queryable by UUID. If a pharmacist asks “where does the patient’s drug name go,†the answer is one word: nowhere. We can prove it by running a regex over our audit log for drug-name patterns and getting zero hits over 18 months.
Gotchas we hit (and what we did)
1. The label printer hiccup
Pharmacy label printers reprint. A pharmacist will reprint a label because the first one smudged, was applied to the wrong bottle, or the customer changed their mind. Each reprint is a new print event in the spooler. Without protection, the bridge fires a new Lightspeed voucher for every reprint — and the customer’s till total doubles.
Fix: deterministic UUID per label content (UPC + price + a high-resolution timestamp bucketed to 10 minutes). Reprints within 10 minutes collapse to the same UUID and are deduplicated at the bridge. Reprints 10+ minutes later are treated as a new label — because at that point a pharmacist did something intentional.
2. Cancelled scripts
A pharmacist cancels a prescription after the label prints. The voucher exists in Lightspeed; the customer walks out without paying. The voucher should expire, not linger as a stale line item.
We can’t pull cancellation events from PropelRx without crossing the boundary, so we don’t. We rely on Lightspeed’s built-in voucher expiration (24h) and a daily reconciliation job that nukes any voucher older than the till’s rolling window. In 18 months, this has caught a handful of orphans — zero customer-facing incidents.
3. Offline cashier
Greenbrook’s in-store Lightspeed has a local offline mode for when the POS network blips. During offline mode, the voucher hasn’t synced down to the till yet, so a scan returns “unknown SKU.â€
Fix: the local Lightspeed agent pre-pulls voucher registry on a 30-second loop. When the network drops, the till has the last 30 seconds of vouchers cached and can ring them through. Worst case, a customer waits 30 seconds. We’ve never had a real failure here.
4. Audit-log queryability
The audit log is append-only and signed, but auditors also need to read it. We expose a small read-only API that takes a date range and returns event UUIDs, timestamps, and the two fields — and nothing else. The read API is on a separate Cloudflare Worker, has its own role, and is logged independently. The auditor reviews their own log, not ours.
The strongest compliance posture is the smallest surface area. Two fields is a smaller surface than twenty.
What we don’t do
We don’t:
- Pull prescriptions from PropelRx as a database query. We never have read credentials to the patient database — we only read the print spooler.
- Store patient identifiers anywhere, including hashed. Hashed patient IDs are still patient IDs to a regulator.
- Push retail data back to PropelRx. The bridge is one-way by construction.
- Use a third-party iPaaS (Workato, Zapier, Mulesoft). Every iPaaS hop is another vendor sitting on the wire — another HIPAA business associate that needs to be papered, audited, and trusted. We host the bridge ourselves on Cloudflare Workers and own every byte of it.
Why this shape, and not the other shapes
We’ve been pitched the “just sync everything, Lightspeed has a customer record anyway†version of this integration more than once. The pitch is faster to ship: full bidirectional sync, patient records as Lightspeed customers, prescriptions as products. It also fails the first PIPEDA review, and any HIPAA-aware client looks at it once and walks away.
The narrow bridge takes longer to design and produces fewer features. It also produces a system that the pharmacy lawyer signs off on without a meeting. That trade is the entire point.
Building something on top of a regulated pharmacy stack and want the boundary right the first time? — hello@trace-ai.ca.