Case Study — The Whole Assure PAT System
This is the capstone. Six pages of theory, now spent on one real system you have access to every day: Assure PAT. We draw the entire platform using the C4 zoom-levels method — Context → Container → Component — then trace the real business flow end to end. By the end you should be able to sit at a whiteboard and redraw all of it from memory. That single skill is what separates “I work on the codebase” from “I understand the system.”
⏱ ~25 min · 🔴 Advanced · Prerequisites: pages 1–6 (especially the C4 model on the Start Here page)
🎯 What you’ll be able to do
- Draw Assure PAT at three zoom levels and pick the right one for any conversation
- Narrate the full request → approve → tap → review flow without notes
- Explain why the boundaries are where they are (multi-tenancy, three token types, serverless)
- Use this page as a map back into the real code when you get lost
1. What is Assure PAT, and what problem does it solve?
Payment cards that use Discover D-PAS contactless (tap-to-pay) have to behave exactly right when they touch a real terminal — the cryptogram, the counters, the EMV tags all have to be correct, or the card is rejected in the field. You cannot find that out after you ship a million cards. Assure PAT is a multi-tenant platform that lets card issuers test that behaviour before production. Ayris Global onboards each customer (an organization), who sets up their issuers, BINs and card profiles; a card request flows through approval; a tester then loads the profile onto an Android phone that emulates the card over NFC (Host Card Emulation), taps a real terminal, and the phone captures the full EMV/APDU transaction. Everything captured comes back to the dashboard where it is reviewed and any failures are tracked as issues. One platform, many organizations, all isolated from each other — that “many tenants, one system” property drives almost every design decision below.
2. Level 1 — Context (🛰 satellite view)
The Context diagram answers one question: who and what touches the system, from the outside? No internals — just Assure PAT as a single black box, the people who use it, and the external things it depends on.
flowchart TB
subgraph ext["External systems"]
Terminal["Real payment terminal<br/>(POS / contactless reader)"]
AWS["AWS Cloud<br/>(Lambda · API GW · RDS · S3 · KMS)"]
end
subgraph people["People"]
SysAdmin(["Ayris Global<br/>System Admin"])
OrgUser(["Org users<br/>SME/Admin · Approver · Manager · Profile Editor"])
Tester(["Mobile tester<br/>(field)"])
end
PAT["Assure PAT<br/>Payment-card testing platform"]
SysAdmin -->|"onboards orgs, manages keys & cards"| PAT
OrgUser -->|"sets up issuers, profiles, requests; reviews results"| PAT
Tester -->|"loads a card profile, taps terminals"| PAT
Tester -->|"NFC tap (HCE emulates the card)"| Terminal
PAT -->|"runs on"| AWS
How to read it. The three pill shapes are actors (people), the box is the system, the right-hand cluster is external systems. Narrate it aloud: “Ayris admins onboard organizations and manage crypto keys; org users set up the test data and review results; a mobile tester loads a profile and taps a real terminal, and the whole platform runs on AWS.” Notice that the terminal is not part of Assure PAT — it’s a real-world device the mobile app interacts with. That boundary is the entire point of the product: it tests how the emulated card behaves against real hardware.
Why so many kinds of user?
Assure PAT has two completely separate worlds of people: Ayris Global staff (system admins) who run the platform, and the customers’ own staff (org users), who are further split into six roles. That split is enforced all the way down with three different JWT token types — we’ll see it again at the component level.
3. Level 2 — Container (✈️ airplane view)
The Container diagram answers: what are the separately deployable/running pieces, and how do they talk? “Container” here means a runnable thing (an app, an API, a database) — not a Docker container specifically. Now we open the black box from level 1.
flowchart TB
Tester(["Mobile tester"])
OrgUser(["Org user"])
SysAdmin(["Ayris admin"])
subgraph clients["Clients"]
Web["Web Dashboard<br/>React 18 + Vite"]
Admin["System Admin Panel<br/>(same React app, /system/*)"]
Mobile["Mobile App<br/>Android / Kotlin · NFC HCE"]
end
subgraph aws["AWS Cloud"]
APIGW["API Gateway"]
API["Express 5 API<br/>(Lambda via serverless-http)"]
DB[("MySQL 8 'assurepat'<br/>on RDS")]
S3[("S3<br/>files / artifacts")]
KMS[("KMS<br/>crypto keys")]
end
Terminal["Real payment terminal"]
OrgUser --> Web
SysAdmin --> Admin
Tester --> Mobile
Web -->|"HTTPS / REST"| APIGW
Admin -->|"HTTPS / REST"| APIGW
Mobile -->|"HTTPS / REST (uploads taps)"| APIGW
Mobile -.->|"NFC tap"| Terminal
APIGW --> API
API -->|"Knex / SQL"| DB
API -->|"presigned URLs"| S3
API -->|"per-user keys"| KMS
How to read it. Three clients on top, the cloud in the middle, the terminal off to the side. Every client speaks HTTPS/REST through API Gateway — there is one front door. Behind it, the Express API does all the thinking and is the only thing that touches the data stores. That “API is the single gatekeeper of data” rule is what keeps a multi-tenant system safe.
| Container | Tech | Responsibility |
|---|---|---|
| Web Dashboard | React 18 + Vite + Bootstrap | Org users (roles 1–6) manage issuers, profiles, requests, cards, issues; review captured transactions. |
| System Admin Panel | Same React app, /system/* routes | Ayris staff onboard orgs, manage partners, generate cards, manage crypto keys. |
| Mobile App | Android / Kotlin, NFC HCE | Emulates a Discover contactless card from a profile, taps terminals, captures EMV/APDU, uploads results. |
| API Gateway | AWS API Gateway | Single HTTPS front door; routes requests to the Lambda. |
| Express API | Express 5 + Knex, on Lambda via serverless-http | All business logic, auth, tenant isolation, validation; the only thing that reads/writes data. |
| MySQL 8 | RDS, schema assurepat | System of record. Every table carries org_id. |
| S3 | AWS S3 | Files and artifacts — issue attachments, receipts, APDU log files. |
| KMS | AWS KMS | Manages the per-user / per-issuer encryption keys for card data. |
The serverless detail that trips people up
The Express app doesn’t run on a server that’s “always on.”
serverless-httpwraps the whole Express app so it runs inside a Lambda function, invoked per-request by API Gateway. To you as a developer it’s still just Express with routes and controllers — but there’s no long-lived process, which matters for things like in-memory caches and DB connection pooling.
4. Level 3 — Component (🚗 street view)
The Component diagram answers: inside one container, what are the modules and how does a request move through them? We zoom into the Express API — the most interesting container. This is the layered architecture from page 2, made concrete.
flowchart TB
APIGW["API Gateway"] --> Routes["Routes<br/>(/api/v2/...)"]
subgraph mw["Middleware chain (runs in order)"]
direction TB
VT["verifyToken<br/>(JWT: org / mobile / system)"]
ET["extractTenant<br/>(org_id + org must be active)"]
VR["validateRequest<br/>(schema check)"]
RA["checkRoleAccess<br/>(186 role mappings)"]
VT --> ET --> VR --> RA
end
Routes --> mw
RA --> Ctrl["Controllers"]
Ctrl --> Svc["Services<br/>(business logic)"]
Svc --> Models["Models"]
Models --> Knex["Knex query builder<br/>(scopedQuery adds WHERE org_id = ?)"]
Knex --> DB[("MySQL 8")]
Svc -.->|"files"| S3[("S3")]
Svc -.->|"keys"| KMS[("KMS")]
How to read it. A request enters at a route, then runs the middleware chain in order: is the token valid (verifyToken)? which tenant is this and is it active (extractTenant)? does the body match the schema (validateRequest)? is this role allowed on this route (checkRoleAccess)? Only if all four pass does it reach a controller (HTTP shape), which calls a service (the actual logic), which calls a model, which builds SQL with Knex — and scopedQuery quietly bolts WHERE org_id = ? onto every query so one tenant can never see another’s data.
The major functional modules inside this same skeleton:
| Module group | What it owns |
|---|---|
| Auth | Login, OTP, password reset, the three token types, T&C acceptance. |
| Users | Org user CRUD, roles via user_org_roles, login history, mobile debug logs. |
| Organizations | Tenant records, status, allowed email domains, config. |
| Cards / Issuers / BINs | Issuers (35+ encrypted crypto columns), BINs, encrypted cards, assignment. |
| Profiles & Templates | D-PAS EMV profiles (the core), system + org templates, versioning, clone. |
| Card Requests | The approval workflow — the wizard and its status lifecycle. |
| Test Cases | What to test (terminal type, scope, PIN capability), V2 validation rules. |
| Issues | Bug tracking with comments, attachments (S3), tags, history, timeline. |
| Notifications | Web / mobile / inbox announcements with a draft→approve flow. |
| Audit Trails | Every INSERT/UPDATE/DELETE across all tables, with before/after diffs. |
The three tokens, one chain
verifyTokenis really three checks: an org token (bhtoken, web users), a mobile token (the app), and a system token (systemToken, Ayris admins). Org and system tokens are mutually exclusive — logging in as one clears the other. Same middleware chain, three identities.
Before you move on, prove you can actually read this architecture — not just nod at it. Each question below points at one layer, boundary, or design choice you just saw.
5. The business flow, end to end
Now stitch the levels together with the real workflow. Watch the card_requests status lifecycle drive it: draft → submitted → approved → assign_card → shipped → completed.
flowchart TB
A["Ayris onboards an org<br/>(issuers seeded, cards generated)"] --> B["Org sets up<br/>issuers · BINs · profiles"]
B --> C{"Card Request wizard"}
subgraph wiz["Wizard steps"]
direction TB
C1["1 · Requester details"]
C2["2 · Terminal info<br/>(terminal_type + brands)"]
C3["3 · Profile selection"]
C4["4 · Test case selection"]
C5["5 · Card assignment"]
C6["6 · Submit"]
C1 --> C2 --> C3 --> C4 --> C5 --> C6
end
C --> wiz
C6 -->|"status: submitted"| D["SME / Approver reviews"]
D -->|"approve → status: approved"| E["Cards assigned<br/>(status: assign_card)"]
D -->|"reject (with comments)"| B
E -->|"status: shipped"| F["Mobile tester loads profile<br/>taps real terminal (NFC HCE)"]
F --> G["Phone captures EMV / APDU<br/>+ debug logs"]
G --> H["Uploaded via API Gateway"]
H --> I["Reviewed in dashboard<br/>(Transactions module)"]
I -->|"failures"| J["Issues tracked<br/>→ resolved"]
I -->|"all good → status: completed"| K(["Done"])
Read it as a story. Ayris onboards an organization and seeds it with issuers and generated cards. The org’s profile editors build the D-PAS profiles. An approver opens the Card Request wizard — requester details, then terminal info (terminal_type plus the card brands), then which profile to test, which test cases to run, which card to assign, then submit. An SME/Approver reviews; on approve, cards are assigned and the request moves toward shipped. A mobile tester loads the assigned card’s profile, the phone emulates it over NFC and taps a real terminal, capturing the EMV/APDU data and debug logs. That upload flows back through API Gateway, lands in the Transactions view for review, and anything that failed becomes a tracked issue. Clean run → completed.
Where a profile gates the request
A request can’t even be created for a feature unless an active profile exists for that feature/product/issuer (
isActiveProfileAvailable()). The profile must be ready before the request — the workflow enforces it up front, not at assignment time.
6. Data flow — the “tap capture” path
The single most distinctive path in the whole system: a physical NFC tap turning into a reviewable row. This is a focused data-flow view of step F→I above.
sequenceDiagram
participant App as Mobile App (HCE)
participant Term as Real terminal
participant GW as API Gateway
participant API as Express API (Lambda)
participant DB as MySQL 8
participant S3 as S3
App->>App: load card profile (JSON) → emulate card
App->>Term: NFC tap (APDU exchange)
Term-->>App: terminal APDUs (amount, TTQ, ...)
App->>App: capture EMV (AC, CID, ATC, IAD, Track2 ...) + debug logs
App->>GW: HTTPS POST transaction JSON
GW->>API: invoke Lambda
API->>API: verifyMobileUserToken + extractTenant
API->>DB: INSERT user_card_usage (+ link user_card, card)
API->>S3: store artifacts (receipt / APDU log file)
API-->>GW: 200 OK
GW-->>App: 200 OK
Note over DB,S3: Dashboard Transactions module reads<br/>these rows + artifacts for review
How to read it. Time runs downward; solid arrows are calls, dashed are responses. The interesting part is the APDU round-trip with the terminal — the phone, acting as the card, answers the terminal’s commands and records everything. The structured transaction (cryptograms, counters, Track 2) goes to MySQL; the bulky artifacts (receipt images, raw APDU log files) go to S3. The dashboard later reads both to let an SME review the tap.
7. The dashboard modules at a glance
The org dashboard is ~14 modules sharing the same table/sort/search/role skeleton. One line each:
| # | Module | Purpose |
|---|---|---|
| 1 | Request History | The card-request lifecycle list (draft → completed) with status tabs. |
| 2 | Test Card Issuer | Manage issuers, their BINs and EMV crypto keys. |
| 3 | Test Card Fulfilment | Manager’s view of requests needing approval / assignment / shipping. |
| 4 | Testing Partner | Global & internal partners and the testers that belong to them. |
| 5 | Manage Cards | Search/view/update encrypted test cards; assign, block, delete. |
| 6 | Maintain Card Inventory | Read-only stock counts by issuer + BIN (available / assigned / total). |
| 7 | Transactions | Captured taps — amount, status, APDU logs, receipts, map. |
| 8 | Manage Users | User CRUD, login history, mobile debug logs, passcode reset. |
| 9 | Manage Announcement | Web/mobile announcements with a draft → submit → approve flow. |
| 10 | Audit Trail | Every change across all tables with before/after diffs. |
| 11 | Card Profile | D-PAS EMV profiles — the core; create/edit/clone/archive, assign card. |
| 12 | Test Cases | Define what to test (terminal type, scope, PIN capability); V2 rules. |
| 13 | Manage Issues | Full bug lifecycle: new → assigned → in progress → resolved → closed. |
| 14 | Report | Analytics — request status, terminal mix, success rate, trends. |
The two cross-cutting modules
Audit Trail (10) and Report (14) don’t own a workflow — they sit across every other module. Audit Trail records every write everywhere; Report aggregates reads from everywhere. When you see a module that touches “ALL modules,” that’s your hint it’s cross-cutting, not a peer.
8. This is a living document
Everything above is a snapshot. The real system changes every week — new migrations, new test-case columns, new modules. The always-current source of truth lives in the repo: Assure PAT/system-understanding.md (the HLD + LLD), with Assure PAT/module-map.md for the module detail. When something here disagrees with those files, those files win — go read them, then update your mental model.
Do this, don’t just read it
Close this page and, on paper, redraw the three C4 levels from memory — Context, then Container, then Component. Then redraw the tap-capture sequence. Where you stall is exactly where your understanding is thin; open
system-understanding.mdor the real code at that spot and patch the gap. Do that twice and you’ll own this system, not just work in it.
Check Yourself
No peeking at the diagrams. The goal is to retrieve and redraw — where you stall is where your understanding is thin.
Name the three C4 zoom levels used on this page, in order, and what question each one answers.
Context (who/what touches the system from outside) → Container (what are the separately deployable pieces and how do they talk) → Component (inside one container, what modules does a request move through). Satellite → airplane → street view.
Draw the Container diagram from memory: the three clients, the single front door, and what sits behind it.
Three clients (Web Dashboard, System Admin Panel, Mobile App) all speak HTTPS/REST to one API Gateway. Behind it: the Express API on Lambda — the only thing that touches data — talking to MySQL 8 (RDS), S3, and KMS. The mobile app also taps the real terminal over NFC off to the side.
List the four middleware in the chain, in order, with the one question each answers.
verifyToken— is the token valid (org / mobile / system)? 2.extractTenant— which tenant, and is the org active? 3.validateRequest— does the body match the schema? 4.checkRoleAccess— is this role allowed on this route? Only if all four pass does it reach a controller.
What are the three token types, who uses each, and which two are mutually exclusive?
Org token (bhtoken, web users), mobile token (the app), system token (systemToken, Ayris admins). Org and system are mutually exclusive — logging in as one clears the other.
Recite the card_requests status lifecycle that drives the business flow.
draft → submitted → approved → assign_card → shipped → completed. A reject sends it back to the org to fix and resubmit.
Trace the layered path inside the Express API from route to database, in one line.
Route → middleware chain → Controller (HTTP shape) → Service (business logic) → Model → Knex (scopedQuery adds WHERE org_id = ?) → MySQL. Services also reach out to S3 (files) and KMS (keys).
In the tap-capture path, which store gets the structured transaction and which gets the artifacts, and why split them?
Structured EMV data (cryptograms, counters, Track 2) → MySQL (queryable). Bulky artifacts (receipts, raw APDU logs) → S3 (cheap blob storage). The dashboard’s Transactions module reads both to review the tap. Split because relational stores are for queryable rows, object storage for large opaque blobs.
Which two dashboard modules are cross-cutting rather than peers, and how do you spot one?
Audit Trail (records every write across all tables) and Report (aggregates reads from everywhere). The tell: a module that touches “ALL modules” sits across the others, it doesn’t own a workflow of its own.
Still Unclear?
Paste any of these into Claude to go deeper:
- “Walk me through what happens when an org user with the wrong role calls an Assure PAT endpoint — at which of the four middleware does it get rejected, and what does the response look like?”
- “Explain why a multi-tenant system enforces
org_idscoping at the data-access layer (scopedQuery) instead of trusting the API routes or the UI to filter. What goes wrong if it doesn’t?” - “Why run the Express API on Lambda via serverless-http instead of an always-on server? Walk me through the trade-offs for connection pooling, cold starts, and cost for a system like this.”
Why AI Can’t Do This For You
AI can describe what a C4 diagram is, but it cannot hold your system in its head — it has never seen the private assurepat schema, the 186 role mappings, or why the terminal sits outside the box. The skill that matters is sitting at a whiteboard and redrawing all three levels from memory while a senior engineer pokes holes in it. That comes from tracing real requests through real code until the boundaries are yours, not from a prompt. When production breaks at 2 a.m., you won’t have time to ask an AI where org_id scoping lives — you’ll already need to know.
One-line summary
Assure PAT is a multi-tenant D-PAS card-testing platform: org users build EMV profiles and route a card request through approval, a tester emulates the card over NFC and taps a real terminal, and the captured transaction flows back through API Gateway → Express-on-Lambda → MySQL/S3 for review — all isolated per tenant by
org_id, guarded by a four-step middleware chain and three token types.
Module done? Add it to today’s tracker