cd ../projects

Project 4 // _qcode

QCode

A prescription as a QR code — issued by one person, carried by another, redeemed by a third, and provable afterwards.

client

private client — Swiss healthcare · confirm before publishing

timeline

25 months · Aug 2021 → Sep 2023 · shipped as medicodeapp.com, since retired

role

full-stack developer

scope

role model & authorizationprescription lifecycledrug catalogue integrationweb app + Expo clientoperator console

## the-problem

A paper prescription is a bearer token with no memory. It doesn't know how much has already been collected, it can't tell you who dispensed it, and once it leaves the issuer's hands the issuer is blind. Digitising it naively just moves that blindness to a screen.

The hard part isn't the QR code — it's that four different people touch one record and none of them may see the same thing. The issuer needs their own patients and nobody else's. The patient needs the code and the count. The dispenser needs today's entitlement, not the medical history. The operator needs all of it, to audit. Get that wrong once and it's a data-protection incident, not a bug report.

On top of that, the products aren't free text. Swiss dispensing runs on a national article catalogue, so a code that says "painkillers" is worthless — it has to resolve to the exact article number the counter will scan.

## the-approach

  • five roles — Superadmin, Admin, Adminclient, Reader, Client — declared per endpoint, so the role list sits next to the handler rather than in a middleware table nobody reads
  • made the authorization check hit the database, not the token: the JWT carries an id, the role is re-read from the user record on every request, so a demotion takes effect immediately
  • modelled the code as a lifecycle, not a flag — issued, sent, given — with a date range, a per-day limit, a renewable flag and the issuer's signature on the record
  • consent by scan: a patient's own QR is a signed token, and an issuer scanning it adds that patient to their allowed list — the issuer's client list is that list, so access is granted by the patient, in person
  • every collection appends to a log with a per-day counter, so "three times a day" is an enforceable number rather than a note
  • products resolve against the Swiss Documedis article catalogue by article number, cached locally on first use — searched live, stored once, referenced by id from then on
  • delivery on two channels, email and in-app notification, with an Expo client for the patient that carries the code, the count and the alerts
  • an operator console on top: account oversight, catalogue import, an AVS registry import and a value-per-patient rollup over everything dispensed

// authorization — the token proves who, the row decides what

type Role =
| "Superadmin" | "Admin" | "Adminclient" | "Reader" | "Client";
const VerifyToken = (roles: Role[], next: Handler) =>
async (req: Request, params: any) => {
const token = req.headers.get("authorization")?.split(" ")[1];
if (!token) return json({ msg: "authorization denied" }, 401);
const { id } = jwt.verify(token, process.env.JWT_SECRET);
req.user = id;
// The token says who you are. What you may do is re-read from
// the row every request, so a demotion lands on the next call
// instead of whenever the last-issued token happens to expire.
if (roles.length) {
const q = { _id: id, role: { $in: roles } };
if (!(await User.findOne(q))) return json({ msg: "denied" }, 401);
}
return next(req, params);
};
// Declared at the endpoint, in view of the handler it guards.
export const POST = VerifyToken(["Reader"], markCodeGiven);

## the-architecture

// architecture — one code, four roles, one audit trail

client

issuer & operator

Next.js dashboards

  • issue, send, revise a code
  • catalogue + account oversight
client

patient app

Expo · React Native

  • carries the code
  • push alerts, print/share
client

dispenser

in-browser QR scan

  • scan, read entitlement
  • mark as given
backend

role gate

per-route, DB-backed

  • 5 roles declared per endpoint
  • role re-read from the row
backend

API

Next.js route handlers

  • 41 endpoints
  • codes, products, users, AVS
backend

code lifecycle

issued → sent → given

  • date range + per-day limit
  • collection log with counter
data

MongoDB

codes · products · users

  • signature + revision history
  • allowed-clients consent list
external

Documedis

Swiss article catalogue

  • search by article number
  • cached locally on first use
external

email

code delivery

  • the QR, sent to the patient
clientbackenddataexternal

## the-results

5

roles, re-checked at every endpoint

41

API routes in one Next.js app

25 mo

from first screen to retirement

  • one record serves four roles without any of them seeing the others' view of it
  • the prescription carries its own history — issued by, sent, dispensed by, collected when and how often
  • codes reference real catalogue articles, so what's issued is what the counter dispenses
  • shipped web, mobile and operator console off one Next.js app
nextreactreact-nativeexpressmongodbawstypescriptdataviz

// want something like this built for you?

start-a-project