@photon-rx/core sits underneath Prescribe. It handles the prescriber’s sign-in and tokens, calls the API, and enforces the workflow rules: which drug was meant, when a draft is created, and when an order is ready to send. Use it when you’re building your own UI in another framework, or when your backend wants typed calls.
It has no dependencies and runs in browsers and Node.
Create a client
Try it
The live objects in /integrate, drawn with no framework: sync, sign and send, beside their code.
clientId, ph.session handles sign-in: start() on load, signIn() from a click, and signOut(). Its status and user show who is signed in. Create one client per signed-in person. On a server, that means one per request, never one shared module-wide.
With
clientId, the access token stays in memory and the refresh token is kept in the page’s localStorage, so a reload signs back in. Any script on the page can read it, so treat script injection there as a token leak, and set a Content-Security-Policy. Auth0 rotates the refresh token on every use and revokes it as soon as an old one is reused. How Prescribe is built has the details.Get, sync, update
ph.patient, ph.prescription and ph.order each have the same three calls:
sync and update return the live object, any candidates when Photon isn’t sure which record you meant, and the changes it reported. The client never picks a candidate for you. Pick one with patient.pick(id).
Live objects
Each record is a live object.state is the record, and status is idle, loading, saving or error. subscribe calls you on every change, and every method waits for the call it makes. There’s one object per record per client, so a patient opened twice is the same object.
- A new prescription without a patient only looks the drug up and never creates anything.
- The newest response always wins, so a slow early reply can’t overwrite a later one.
rx.sign()waits for saves in flight, then signs only what’s stored.order.readyToSendis true once the patient is found, every prescription is signed, and nothing is still saving.order.submit()refuses until it’s ready, and never sets a pharmacy.
Choices
When Photon offers options, such as which of several drugs a name meant,choicesIn(obj.changes) lists them. obj.choose(option) applies one.
treatment in parts, so you can group drugs by brandName, then form, then strength without parsing displayName. It also has genericName, route, schedule (the DEA schedule) and recommendedDispenseUnits, the values dispense.unit takes. Once picked, the same parts are rx.treatment:
Errors
Anything Photon answers about your data comes back as a result. Only failures that different data can’t fix throw aPhotonError, and its kind says which:
Links
url() builds a deep link and frame() builds an iframe URL. Each record’s calls have both, and so does every live object:
- Links carry Photon ids only (
pat_,rx_,ord_,med_,tmp_,org_), in the clear.url()andframe()throwinvalidfor anything else, such as patient details or a drug’s name. Sync those first, then link by id. See deep links for every parameter. - A live object’s link is its record’s id. Before it has a record, the link uses the ids the object has: an order’s patient and lines, or a prescription’s patient and its template or drug. A patient with no record has no link, so
url()throws. - Links point to the environment’s Prescribe app:
rx.neutron.healthin the sandbox, orrx.newin production. PassappUrltophoton()to point them somewhere else. url()is for sharing, so it never carries aclient_id. The Prescribe app signs the prescriber in on its own.frame()puts your Applicationclient_idin the URL, fromauth: { clientId }, and throws without one. Withauth: { getToken }, pass it in the link:ph.frame({ flow, …, auth: { clientId } }).- Pass
organizationtophoton(), and every link carries it, so the prescriber signs straight in to that organization.
Secrets
A client secret never belongs in a browser. If you pass aclientSecret to photon() in a page, it throws before doing anything else. Treat that secret as leaked and rotate it. Links never carry a secret.