Rainforest Cheng
Back to portfolio

Case study

OpenCGT

Senior Frontend Developer · CodeGreen · 2024 — Present

An admin platform that orchestrates cell & gene therapy treatments end to end — patient Chain-of-Identity, material and shipment tracking, and the hospital and manufacturer orgs that hand a therapy between them.

The front door: a token that already knows your role

One "Log in" hands off to Auth0 and the app comes back already knowing the org role — no separate permissions call.

I owned auth — the NextAuth + Auth0 config, and the decision to read roles out of the access token in the jwt/session callbacks so components read the role synchronously, with no extra round-trip.

Auth0's roles claim key isn't stable, so getRolesFromJwt() base64url-decodes the payload, scans keys for one containing "roles", then narrows the match to RoleEnum — hospital_admin, manufacturer_admin, root.

Choose who signs in
app → Auth0 /authorize
Auth0 → /api/auth/callback
NextAuth jwt() mints session
access_token (opaque)
eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6Im9jZ3RfMjAyNF8wOSJ9.eyJpc3MiOiJodHRwczovL29wZW5jZ3QudXMuYXV0aDAuY29tLyIsInN1YiI6ImF1dGgwfGhvc3BfN2YzMSIsImF1ZCI6WyJodHRwczovL2FwaS5vcGVuY2d0LmFwcCJdLCJpYXQiOjE3MjE1MDAwMDAsImV4cCI6MTcyMTU4NjQwMCwib3JnX2lkIjoic3QtbWFyeXMiLCJodHRwczovL29wZW5jZ3QuYXBwL3JvbGVzIjpbImhvc3BpdGFsX2FkbWluIl19.sHMa8xQ2v_Rk1p0d9F3nZ7wLtY6bC4eK2gJ0uWqN8s

Mirrors auth.ts · lib/getRolesFromJwt.ts

Mirrors auth.ts · lib/getRolesFromJwt.ts

One deployment, two products

Hospital and manufacturer staff sign into the same deployment but see materially different apps — and a forbidden route returns a real 404, not a leak.

I built the role-aware shell: every sidebar item wrapped in Refine's <CanAccess> so the nav is computed from policy, plus middleware that rewrites a disallowed route to /not-found server-side.

Two gates that must agree — client <CanAccess> and the middleware.ts server rewrite. The app even splits @synopsis/@hospital vs @synopsis/@manufacturer so each product is its own segment.

Signed in as
Navigation
Dashboard
Program overview across every active therapy.
Enroll patient
Dashboard workspace
Try a direct link — middleware still guards it

Mirrors app/(protected)/layout.tsx · middleware.ts · providers/accessControl.ts

Mirrors app/(protected)/layout.tsx · middleware.ts · providers/accessControl.ts

The rule behind the gate: a Casbin playground

Authorization is one Casbin model, evaluated identically on the client and the server — so every decision is explainable and auditable.

I designed the dual-enforcement strategy: a local casbin-core enforcer answers the common cases instantly, and denials fall through to the remote authorizer holding the full policy set.

The matcher is g(r.sub, p.sub) && keyMatch(r.obj, p.obj) && (p.act == '*' || regexMatch(r.act, p.act)) — objects matched by path prefix, actions by regex, * a wildcard.

p.subp.objp.actmatcher
root/**
hospital_admin/patients/*
hospital_admin/patients/*
manufacturer_admin/materials/*
manufacturer_admin/shipments/*

Mirrors lib/casbin/model.conf · lib/casbin/policy.csv · enforcer.ts

Mirrors lib/casbin/model.conf · lib/casbin/policy.csv · enforcer.ts

Encrypted before it leaves the browser

During enrollment, a patient's PHI is encrypted in the browser; each org is granted "phi" or "non-phi", and only the right private key can read it. The server only ever stores ciphertext.

I implemented the WebCrypto E2EE utilities and the enrollment access-control step — the useFieldArray UI that decides who can decrypt what.

Hybrid, not plain RSA: the record body is AES-CBC encrypted, and the AES key + IV are RSA-OAEP-2048 wrapped with each recipient's public key — big records stay fast while read access is bound to the private key.

Patient record
Orgs that can access
Pipeline
generate AES-256 key + IV
AES-CBC encrypt record body
RSA-OAEP wrap key · per recipient

Mirrors lib/crypto/e2ee.ts · app/enroll/AccessControlFields.tsx

Mirrors lib/crypto/e2ee.ts · app/enroll/AccessControlFields.tsx

Build only what changed

One Nx pipeline ships only the projects a change touches — affected tests and builds on a PR, then a signed image push and Helm rollout on main, guarded by role-scoped Playwright e2e and a k6 load test.

I focused on the parts closest to the app — wiring nx affected with tag exclusions, the container build configs, and both suites: Playwright logging in as each role, and k6 ramping real VUs through the actual Auth0 login.

It's driven by the Nx project graph, not a job list — nx-set-shas computes base/head and nx affected walks the graph. The k6 browser test drives the real Auth0 form under 100 ramping VUs, asserting a 1.00 checks rate.

Changed files
nx project graph
@opencgt/ui@opencgt/api-client@opencgt/auth@opencgt/webweb-e2eweb-load
idle — tick files, pick PR / push, run.
Playwright · role matrix
rolelogingateflow
hospital_admin
manufacturer_admin
root
k6 · VU ramp
0 → 100 VUschecks 1.00

Mirrors nx.json · .github/workflows/ci.yml · apps/web-e2e · apps/web-load

Mirrors nx.json · .github/workflows/ci.yml · apps/web-e2e · apps/web-load