Diagrams
ArchitectureFigure
A component map: services, datastores, trust boundaries, and what talks to what.
Usage
When to reach for it
Ships from@misoto22/design/diagrams
import { ArchitectureFigure } from '@misoto22/design/diagrams'Notes
A component map: services, datastores, boundaries, and what talks to what.
Takes the same JSON an archify architecture specification carries — components, boundaries, connections — and draws it in this system's own terms: paper plates on a hairline, one reversed plate for the component the diagram is about, and seven drawn sigils where archify uses seven hues.
IT RENDERS ON A SERVER because every position is already in the specification. A component gives a row and a col into a grid whose cell size is either declared or defaulted, or it gives an absolute pos — nothing here is solved for, relaxed, or measured. So the markup is a pure function of the input, it is identical on the server and in the browser, and there is no layout shift on hydration because there is no layout to do.
A component that declares NEITHER flows: it takes the next free cell in declaration order, wrapping at layout.cols. That is still arithmetic on numbers the specification carries rather than a solver — and the alternative was every unplaced component defaulting to row 0, column 0 and stacking into one plate.
BOUNDARIES ARE DRAWN FIRST, AND DRAWN DIFFERENTLY. A region is where something RUNS — a VPC, a zone, a cluster — and is a solid frame. A security-group is what may REACH it, and is dashed. That is not decoration: an infrastructure diagram is very often read for exactly one of those two questions, and a reader should be able to tell which line answers which without reading either label.
Anatomy
| Element | Description |
|---|---|
| Figure shellrequired | The frame all five figures sit in — a serif title, the scrolling paper surface, the role="img" svg. What a map hands it is the relationship list: every connection published as "CloudFront → API: HTTPS", which is the sentence this diagram type exists to make. |
| Gridrequired | What row and col index into: a 184 × 72 cell with 64 and 76 unit gutters, overridable per figure through spec.layout — where cellW is the pitch AND the width of the plates drawn at it. A component declaring neither row nor col flows into the next free cell, wrapping at layout.cols; pos replaces the grid entirely and puts the box at an absolute coordinate. |
| Component platerequired | One box per component: a sigil and an eyebrow on the top line, the name at reading size, the sublabel in mono under it. The eyebrow prints the component’s own tag when it has one and the kind word otherwise, and the box grows past a declared height rather than printing through its own bottom rule. |
| Boundary frame | A labelled rule around the union of the boxes its wraps names, inflated by pad — 28 units by default. Solid for a region, dashed for a security-group, so where a thing runs and what may reach it are two different lines before either label is read. |
| Connections | Lines that leave and arrive perpendicular to a face, spread across that face when several share it, each with its wording on a mask that punches the line out from under itself. |
| Key | The kinds actually drawn, each shown with the same sigil the plates carry. legend="all" names all seven instead; legend="hidden" prints none. |
Best practices
Do
- Build a new spec object when something changes. The model is memoised on the spec’s identity, so mutating spec.components in place leaves the same reference and the figure goes on drawing the picture it was first given — development prints DIAGRAM_SPEC_MUTATED when it catches that, and a production build prints nothing and still draws the old picture.
- Place both ends with pos when a connection carries a hand-tuned via, channelX or channelY. Those are honoured only when both endpoints were placed absolutely — on the grid they are coordinates in a space this renderer did not choose, so a route between two row/col components is dropped and re-routed.
- Drive a guided reading yourself. meta.views typechecks and no renderer reads it — a chapter is a control rather than a layout — so the chapter’s focus ids have to arrive as activeIds, which is what dims everything else and adds the "n highlighted" status line.
- Leave row and col off the components you have no opinion about. They flow into the next free cell in declaration order, wrapping at layout.cols and stepping around whatever the placed ones claimed — a component declaring neither used to mean row 0, column 0, which is one plate with the rest of them underneath it.
Don’t
- Two components on one cell are still two plates at one coordinate, one drawn over the other: there is no second place to put the second plate. Development prints DIAGRAM_CELL_COLLISION naming both, which is the only signal — the picture is identical either way.
- A boundary silently shrinks to the components it can find. A wraps id no component declares is skipped, and a boundary whose ids are all missing draws no frame at all — a trust boundary can leave the picture while the specification still claims it.
Examples
default
A request path on the default grid: every component names a row and a column, so nothing here is solved for and the markup a server sends is the markup the browser keeps. The region boundary is what makes ap-southeast-2 a place rather than a sixth box, and it is what puts the browser visibly outside it. One connection carries emphasis — without it four arrows of equal weight leave a reader to work out unaided which one the diagram is about.
One request, end to end
5 elements and 4 relationships. The edge, the service, the row it reads.
- Browser (external) — Safari / Chrome
- CloudFront (cloud) — CDN
- API (backend) — FastAPI
- Redis (database) — read-through
- Postgres (database) — primary
- Browser → CloudFront: HTTPS
- CloudFront → API
- API → Redis: read-through
- API → Postgres: SQL
- Region ap-southeast-2 encloses API, Redis, Postgres.
- Every request is fronted by the CDN.
- Reads go through Redis first.
- Postgres is the source of truth.
two kinds of boundary
The two boundary kinds, one nested inside the other. A region is a solid frame around where something runs and a security-group is dashed around what may reach it, so a reader who came for one of those two questions can tell which frame answers it before reading either caption. The security variant marks the single hop that crosses the dashed line, and the queue-to-worker arrow inside it carries no wording at all — affordable only because exactly one arrow leaves that box, and a guess the moment a second one does.
An inbound webhook
4 elements and 3 relationships. Where it runs, and what is allowed to reach it.
- Partner (external) — webhook sender
- Gateway (security) — signature check
- Ingest queue (messagebus) — SQS
- Worker (backend) — consumer
- Partner → Gateway: signed POST
- Gateway → Ingest queue: verified
- Ingest queue → Worker
- Region ap-southeast-2 encloses Gateway, Ingest queue, Worker.
- Security group Reachable only from the gateway encloses Ingest queue, Worker.
a denser map
Eight boxes and eight lines on a grid tightened through layout, which is what a real system looks like and also what density costs: at this count nobody traces a path, they scan for one. The two emphasised connections and the cards underneath are the repair — the heavier run says where to start, and the cards state the conclusions so the reader is not asked to derive them. A map this size with every line at one weight is a wall the reader is invited to solve.
The platform
8 elements and 8 relationships. Eight services, and one path through them.
- Web (frontend) — Next.js
- Mobile (frontend) — iOS
- Gateway (backend) — routing
- Auth (security) — OIDC
- Orders (backend) — service
- Search (backend) — service
- Postgres (database) — primary
- OpenSearch (database) — index
- Web → Gateway
- Mobile → Gateway
- Gateway → Auth: verify
- Gateway → Orders: REST
- Gateway → Search: REST
- Orders → Postgres: SQL
- Search → OpenSearch: query
- Orders → OpenSearch: reindex
- Gateway to Orders to Postgres carries every paid order.
- Orders writes the index Search reads.
- Only Orders and Search reach a datastore at all.
placed by hand
pos instead of row and col, which is what unlocks a hand-tuned route: via, channelX and channelY are honoured only when BOTH ends of the connection were placed absolutely. Between two grid components they are coordinates in a space this renderer chose rather than the author, so they are dropped and the line is re-routed without a word. Nothing detects a collision either — two components given the same coordinate are drawn one plate over the other and the figure renders, with the summary list beside it still reporting both.
Orders, with the retry drawn under the row
4 elements and 4 relationships. Every box placed by hand, so the loop can be too.
- Checkout API (backend) — Fastify
- SQS orders (messagebus) — standard queue
- Fulfilment worker (backend) — ECS service
- orders-dlq (messagebus) — paged on depth > 0
- Checkout API → SQS orders: publish
- SQS orders → Fulfilment worker: consume
- Fulfilment worker → orders-dlq: after 5 attempts
- Fulfilment worker → SQS orders: retry in 30s
Accessibility
- The <svg> is role="img" with a name, so a screen reader announces a picture instead of walking two hundred <text> nodes in drawing order.
- The diagram’s content is published beside it as an ordinary list — every node with its kind, every relationship as "A → B: over HTTPS". That list is where the meaning lives for anyone not looking at the picture.
- Passing onSelectNode turns that list into real buttons, which is the keyboard’s only route to a selection: the plates inside the picture are presentational by construction.