A cart that won't check out until the prescription is real
An online pharmacy where a prescription is a first-class record with its own lifecycle, and prescription-only items simply can't leave the cart without one.
How the work was scoped
- Industry
- Healthcare & Dental Practices
- Duration
- 20 weeks
- Cooperation model
- Fixed price, phased
Client name withheld under NDA. Engagement details are shown to the extent our agreement permits.
The hard problem
Most pharmacy storefronts treat a prescription as a file upload at checkout. The order exists before anyone has looked at the prescription, and canceling it later means a refund and an apology. The regulated item and the shampoo sit in the same cart, and only one of them can be sold on demand.
Prescriptions were a file uploaded at checkout, so the order was created and paid for before a pharmacist had looked at anything. When a prescription was rejected, the fix was a refund, a cancellation and an apology, and the customer had already been charged. The counter's most frequent call was people asking whether theirs had been approved yet.
A prescription modeled as its own record with a state (submitted, under review, approved, rejected) that the cart reads. A prescription-only line simply can't be checked out until its prescription is approved, so the order never exists in an invalid state.
How the pieces fit
- 01
Modeled the prescription as an entity with a lifecycle of its own
The lifecycle was agreed with the pharmacist-in-charge before any storefront work, because everything downstream is a consequence of that model.
- 02
Made the cart read prescription state per line
The cart reads prescription state per line, which is what makes partial checkout expressible at all.
- 03
Blocked checkout at the line level, so the rest of the cart still moves
Gating at the line means the shampoo checks out while the regulated item waits, and the waiting line converts to fulfillment when approval lands.
- 04
Gave the customer a prescriptions area where state is visible without contacting the pharmacy
Customers see the state of their own prescriptions and the reason for any rejection, which was the counter's most-asked question.
- 05
Used GraphQL so the storefront asks for exactly the prescription state it needs
GraphQL lets the cart ask for prescription state without loading the order graph, which is what removed the slow-cart problem at its cause.
The system we were asked to build
The pharmacy sells ordinary health products and prescription-only medicines through one storefront. We built the catalog, the cart, and the prescription workflow that has to sit between them.
A pharmacy selling ordinary health products and prescription-only medicines through one storefront, where the two can't be sold the same way and the customer puts them in the same cart. The engagement began after canceling improperly created orders had become routine enough to need its own process.
Full-Stack Engineering
A prescription is an object
It has an identity and a lifecycle, so it can be reviewed and referred to long after the order.
A prescription is a record with an identity and a lifecycle: submitted, under review, approved, rejected. That means it can be referred to, reused across orders, reviewed independently of any cart, and audited afterward. The old attachment model made every one of those impossible, which is why it was replaced first.
- A record with an identity and a lifecycle
- Reviewable and reusable independently of any order
- Auditable after the fact
A prescription as a record in the pharmacy console: its own identity, the four-state lifecycle it moved through, the two orders that reused it, and an audit trail naming who did what.
Gating at the line: two lines are ready for $14.40 while the third waits on its prescription, uncharged, with the cart rules evaluated line by line.
Line-level gating
The shampoo checks out while the regulated item waits. The order never exists in an invalid state.
Gating happens per line: the shampoo and the vitamins check out immediately while the regulated item waits on its prescription. The order simply can't be created with an unapproved prescription-only line on it, so the invalid state never has to be handled. It doesn't exist. A waiting line converts into its own fulfillment the moment approval lands.
- Per-line gating; unregulated items check out immediately
- The invalid order state is unrepresentable
- Approval converts the waiting line into fulfillment automatically
State without a phone call
Customers see whether a prescription is under review or approved, which is the counter's most-asked question.
The customer sees exactly which state their prescription is in and what happens next, because "is it approved yet?" was the counter's most-asked question and every instance of it was a phone call. Rejections carry the reason and the route to fix it, so nobody is left wondering whether to resubmit or call.
- Prescription state visible to the customer at all times
- Rejections carry a reason and a route to resolve
- Removed the counter's single most-asked question
The customer's own prescriptions area, where submitted, under review, approved and rejected are visible without calling the counter, and the rejection carries its reason and a route to fix it.
A real review queue
The pharmacist works a queue: nine waiting, longest first, each against its held cart line, with the photo, item and history open and decisions made from the keyboard and stamped by name.
Pharmacists work a dedicated queue, with no digging through order records.
Pharmacists work a queue ordered by wait time and urgency, with the prescription image, the item and the customer's history on one screen. Nobody opens order records one at a time to find the prescriptions inside. Approve and reject are keyboard actions, because this work happens in sessions, and every decision is stamped with the pharmacist who made it.
- A queue ordered by wait and urgency
- Image, item and history on one screen
- Keyboard decisions, each stamped with the pharmacist
Precise queries
GraphQL means a screen asks for the prescription state it needs instead of over-fetching an order graph.
GraphQL lets each surface ask for exactly what it needs: the cart asks for prescription state and nothing else, while the review queue asks for the image and the history. Over-fetching an entire order graph to read one field was what had been slowing carts down, and changing the query shape fixed it at the cause. No caching layer papering over the same problem.
- Each surface requests exactly the fields it renders
- The cart reads prescription state without loading the order graph
- Fixed by query shape, with no cache papering over it
The cart's GraphQL query asking for prescription state and nothing else, the response each line is gated from, and the order graph it no longer loads.
Phase by phase
Phase 1: The Prescription Object
Not An Upload Field
Modeled the prescription with its own identity and lifecycle, so it can be reviewed, approved, rejected and referred to later. It no longer lives as a file stapled to an order.
- Prescription Entity
- State Machine
- Review Queue
Phase 2: The Cart
Per Line, Not Per Order
Made the cart evaluate prescription requirements line by line, so a customer buying shampoo and a prescription medicine can complete the part that's ready.
- Line-Level Gating
- Split Checkout
- Cart Rules
Phase 3: The Customer's View
State Without A Phone Call
Built the prescriptions area so a customer can see whether theirs is under review or approved, which is the question the pharmacy counter otherwise answers all day.
- My Prescriptions
- Status Surface
- Notifications
Phase 4: The API
Ask For What You Need
Used GraphQL so the storefront requests exactly the prescription state a screen needs, without over-fetching an order graph to find one status field.
- GraphQL Schema
- Prescription Queries
- Client Cache
Delivery is chosen only for the lines that can actually ship; the waiting line is explicitly excluded, never silently delayed, and the order is previewed as it will be placed: two lines shipping, one held, nothing charged for it.
What it carries now
0
Orders created in an invalid state
−64%
Counter calls asking for status
4
Prescription states modeled
Never
Cart blocked entirely
Orders created in an invalid state is zero by construction: the state is unrepresentable, so there's nothing to measure. Counter calls asking for status compares the pharmacy's own call log before and after. Prescription states modeled is a count. Cart blocked entirely is a statement about the line-level gate.
Client name withheld under NDA. Figures are approximate, drawn from the engagement’s own reporting.
What the architecture settled
A prescription as a file upload means the order exists before anyone has checked it.
Uploading at checkout inverts the order of operations: the money moves before the check, and every rejection becomes a refund where it should have been a decline.
Gate at the line. Blocking a whole cart over one item loses the rest of the sale.
Blocking the whole cart is the obvious gate and loses the unregulated half of the sale, which at this pharmacy was most of the cart value.
Most counter calls are status questions, and a status surface answers them without staff.
The status calls weren't a communication failure. They were a missing surface: customers had no way to see a state that already existed in the system.
GraphQL earns its place when screens need a few fields from a large graph.
Asking for a few fields from a large graph is exactly the case GraphQL is for, and here it fixed a real performance problem, not just a tidy-up.
How the work was run
The decision that shaped everything was made in the first two weeks: gate at the line, not the order. It's a small modeling choice that turns a blocked cart into a partial checkout, and it's why the storefront stopped losing the unregulated half of every mixed order.
Phased over twenty weeks, with the regulated half settled before the storefront half. The line-level gate was the decision that made the commercial case work. Blocking the whole cart was the obvious answer, and it would have lost the unregulated items with it, which at this pharmacy is most of the cart value. Pharmacists worked the review queue in a pilot before it replaced the old process.
One held line, four states
The prescription decides one line. The rest of the cart has already gone.
One order: shampoo and vitamins checked out for $14.40, and a blood pressure medicine waiting on its prescription. Put that prescription in each of its four states and see what the order’s lines, the charge and the customer’s own status view do. Switch tabs, or use the arrow keys once one is focused.
Prescription RX-2026-04817 · times illustrative
- Submitted (current)
- Under review
- Approved
The prescription exists as its own record before anyone has looked at it. The order was created with the regulated line held outside the charge and the delivery, so the shampoo and the vitamins are already shipping.
The order’s linesML-2026-30914
- Anti-dandruff shampooNo prescription neededShipping
- Vitamin D3 1,000 IU tabletsNo prescription neededShipping
- Amlodipine 5mg tabletsRX-2026-04817 · No delivery chosen, not chargedHeld · waiting on prescription
The other two lines checked out in every state.
The charge
- Two ready lines, at placement
- $14.40
- The held line
- $0.00
- Charged so far
- $14.40
Refunds needed: none
What the customer sees
Received
Waiting for a pharmacist. Your other two items are already on their way.
Submitted 9:12 AM
Visible in My prescriptions, so the answer doesn’t need a call to the counter.
Why the order is never invalid: the cart reads the prescription’s state line by line, so a prescription-only line that isn’t approved can only ever be on an order as held: not charged, not delivered. Approval is the only thing that converts it.
From a prescription arriving to the line it releases
The lifecycle was agreed with the pharmacist-in-charge before any storefront work, because everything downstream reads it: the queue writes it, the cart gates on it, and the customer sees it.
- 01 · SourcePrescription submittedCreated as a record with its own identity, separate from any order, so it can be reviewed independently of any cart.
- 02 · QueuePharmacist review queueOrdered by wait and urgency, with image, item and history on one screen. Every keyboard decision is stamped with the pharmacist.
- 03 · StateLifecycle in PostgreSQLFour states: submitted, under review, approved, rejected. The record can be reused across orders and audited afterward.
- 04 · EngineCart rules, per lineA prescription-only line can't check out unapproved, so the invalid order is unrepresentable. Approval converts the line into its own fulfillment.
- 05 · DeliveryGraphQL to the storefrontEach surface asks for exactly the fields it renders: the cart reads prescription state without loading the order graph.
What can’t happen to a prescription-only line
Dispensing, charging & status
Nothing charged or sent before approval
Gating is per cart line. A prescription-only line that isn't approved stays held: no charge, no delivery. The order never exists in an invalid state, so a rejection is a simple decline, with no refund or cancellation.
Every decision has a name on it
Pharmacists work a queue with the image, the item and the customer's history on one screen, and each approve or reject is stamped with the pharmacist who made it. The prescription record can be audited afterward.
State visible without a phone call
Customers see submitted, under review, approved or rejected in their own prescriptions area, and a rejection carries its reason and the route to fix it. Counter calls asking for status fell 64% in the pharmacy's own call log.
Selling prescription-only medicines in the same cart as everything else? Scope your build in 3 minutes.
Scope your buildNearby engagements
Web PlatformsThe ransomware attack that became a four-hour non-event
A ransomware attack encrypted the primary patient records server. Drilled offsite backups and a written runbook turned what could have been a scramble into a full restore inside the recovery time objective.
Healthcare & Dental Practices · Ongoing retainer
Web PlatformsThe right blood type isn't enough: it has to be someone who can get there
A donor register that matches each request on blood-group compatibility and on whether the donor can actually reach the hospital. One platform serves a web admin and a mobile client from the same records.
Healthcare & Dental Practices · 16 weeks
Web PlatformsA medicine isn't a SKU, so the catalog is generic, strength and company before it's a product
A pharmacy counter system whose catalog is modeled on how medicines actually differ (generic, strength and manufacturer as separate axes), so when a brand is out of stock the counter can find an equivalent instead of turning the customer away.
Healthcare & Dental Practices · 16 weeks
Let's talk
Running a large platform, shaping a first MVP, or getting a product ready for a funding round? Tell us where you are. We'll shape the process around it, and stay with you after launch.














