squinch · git:20261031.887e53f · 2026-10-31 · sha256 32ff13269d2fd7d9
squinch git:20261031.887e53fA
Immutable. This exact content never changes and is served at /api/v1/blob/32ff13269d2fd7d9.
---
name: squinch
description: Author architecture diagrams as code with the Squinch DSL. Use this whenever the user wants an architecture or system diagram — creating one from prose, editing or reviewing a .squinch file, drawing cloud infrastructure (AWS, Azure, Kubernetes, hybrid estates), documenting services for a README or a design review, or any picture of systems, services and the connections between them — even when they never say "squinch". Write the .squinch model, validate it with `squinch check`, render it with `squinch render` (SVG in both themes plus the interactive HTML by default; PNG only when asked), and fix what you see with the layout cookbook.
compatibility: Requires Node.js 22+ and the squinch CLI on PATH, or npx squinch with no install
---
# Squinch — architecture diagrams as code
Squinch renders `.squinch` files into deterministic SVG diagrams. You write the
*model* (systems, components, connections); the engine lays it out. When the
auto-layout isn't what you want, you steer it with **relative** hints — there are
no pixel coordinates anywhere in the language.
## The loop
Work like a compiler user, not an artist:
```bash
squinch check diagram.squinch --format json # parse + lint; machine-readable
squinch render diagram.squinch -o out.svg # deterministic SVG; theme: the view's, else dark
squinch render diagram.squinch --view NAME --theme light -o out.svg # themes: light | dark
squinch render diagram.squinch --sync # every view × both themes, next to the source
squinch render diagram.squinch -o out.html # one interactive file: every view, both themes
squinch render diagram.squinch -o out.png --scale 2 # PNG, only when asked (--width also works)
squinch icons search "<term>, <term>, …" # find icon ids — batch every unknown in one call
squinch diff --format json # what changed in the architecture
```
If `squinch` is not on PATH, prefix every command with `npx` — `npx squinch
check …` — or install it once with `npm i -g squinch`.
### What to hand over
Unless the user names a format or a theme, a finished diagram is **all** of:
```bash
squinch render diagrams/ --sync # <name>.<view>.light.svg + .dark.svg per view
squinch render diagrams/ -o diagram.html # every view, both palettes, click-to-zoom, presentation mode
```
- **Both themes, always.** Dark is designed, not inverted, and the reader's
environment picks — never choose one for them. For a README or a docs site
that should follow the reader's colour scheme, `--adaptive -o x.svg` folds
the pair into one SVG behind `prefers-color-scheme`.
- **The HTML is the thing to open.** It is one self-contained file with every
view and a theme switch, and it needs no server — point the user at it
first. A project with several views is unreadable as a pile of SVGs.
- **PNG is the exception**, not the default: render one only when the user
asks for an image, slides, or a surface that cannot show SVG. A PNG you
render to check your own work is a different thing (quality bar 3) and
stays out of the hand-over.
- Every render carries the tool version that drew it (`data-squinch` on the
root `<svg>`), and `render --check` re-renders and compares, so committed
SVGs can gate CI. For a throwaway diagram that lives nowhere, two explicit
`--theme` renders plus the HTML are the lighter recipe.
- **Write the files; leave committing to the user.** Don't commit, push or
make a branch unless they ask — they will want to look first.
Then end your reply with three to five bullets, one or two plain sentences
each, the way you would say it to a colleague — and nothing before them that
says the same thing:
- Open `diagram.html` — it has the landscape and orders views, light and dark.
- I guessed that the services run on Lambda and that Postgres means RDS; the
request named neither.
- I looked at both views. The one rough spot is the Kafka wire running round
the right edge; moving it made the services row harder to read, so I left it.
- If this is more than you need, I can take out the load generator and the
tracing collector, or show less detail by grouping the services into three
areas.
The first three are never skipped: where to open it; what you guessed — every
product, cloud or runtime the request did not name — or "Nothing guessed" when
that is true; and what you saw when you looked, or that you could not look.
The last — an offer to cut it down, naming your own most peripheral boxes, not
a category — ends every hand-over of an open-ended request or a big system,
and is left off a small diagram that drew what was asked. One more bullet may
say how you grouped a big system, or a warning you kept. Don't describe the
components or the layout: the diagram shows them.
0. If the system is big — a whole repo, a platform — decide its *areas* before
you write a node, because no layout hint rescues everything on one page: see
"When the system is big".
1. Write the model first, with **no `layout` block at all** — most diagrams
never need one. Your edges already say what the tiers are, and the engine
ranks from them: a node sits below everything that points at it. Render it
and look. Add hints only to fix something you can see is wrong, one at a
time, naming only the nodes you actually care about. Icon ids outside the
AWS basics — anything `azure/`, `gcp/`, `k8s/`, `logos/` or `sys/` — are
looked up before the first `check`, never written from memory: open
`references/icons.md`, which lists them per pack, or run `squinch icons
search` with every unknown in one call. An id guessed from the word you
would say — a `sys/` or `azure/` name that reads right and does not exist —
is the most common way a first `check` fails.
2. `check` after every edit. Diagnostics tell you the location, the problem, and
usually the fix (`did you mean ...?`). Trust them. When one is not
self-explanatory, open `references/cookbook.md` — symptom → fix, keyed on
the diagnostic's own words.
3. Exit code 0 and **no diagnostics at all** = clean. Errors block the render;
warnings do not, which is exactly why they matter — a warning means the file
is valid but probably not the diagram you were asked for. Fix warnings
before you stop.
4. A stderr line starting `update:` (a newer squinch is on npm) or `skill:`
(this SKILL.md was installed by a different squinch) — or the `update` /
`skill` fields in `check --format json` — is information, not a diagnostic:
exit 0 with no diagnostics is still clean. Mention it to the user once and
carry on. Never upgrade squinch or re-run `squinch skill` unasked: an
upgrade can change render bytes, and that is the user's call.
## Quality bar before you call it done
1. `squinch check` exits 0 with no diagnostics.
2. **Every view** you declared is rendered in both `--theme light` and
`--theme dark`, plus the interactive `.html` — and all of it is handed
over, not just checked (see "What to hand over").
3. **Look at the picture, not the markup.** SVG source cannot show you a
detour. Rasterise each view beside the source and open the image —
`squinch render diagram.squinch --view NAME -o NAME.check.png` — then
delete the `.check.png` files when you are done. Keep them inside your
working directory: a path like `/tmp` is often one you may not read. One
theme is enough, both share one layout. Tiers read top-to-bottom (or
left-to-right), no edge takes a baffling detour, async flows (`~>`) are
dashed, related things sit together. If you could not view the image, say
so in the hand-over and make no claim about how the layout looks.
4. Labels are short noun phrases. A `subtitle:` of a few words says what a
leaf runs on or who owns it; anything longer goes in `description`, never
the label.
5. Model semantics honestly: request/response is `->`; anything that queues,
buffers or fans out is `~>`. If the prose says stream, queue, topic, event,
publishes, emits, feeds, notifies or subscribes — Kinesis, Kafka, SQS, SNS,
EventBridge, Service Bus — that edge is `~>`, and a pipeline drawn entirely
with `->` is almost always wrong.
6. **Every actor the prose names is in the diagram.** People are easy to drop:
"customers browse", "developers push", "an analyst queries" each name a
`person`, and a stack diagram that draws only the technology has left out
who uses it. Re-read the request and check each noun appears — a human is a
`person`, not a box and not omitted.
7. **Nothing is in the diagram that the request does not support.** A reader
takes every box as a fact, so a CDN, WAF, cache or queue that nothing in
the request implies is a guess presented as truth — leave it out. The same
goes for glue: an API or worker you put between two things the request
names, and a product you picked where it named only a kind ("Postgres" is
not "RDS"). A `link:` is the same: use a URL the request or the code gives
you, never one you composed. When the picture cannot be drawn without
filling a gap, draw the plainest reading and say what you guessed in the
hand-over.
## Language
```squinch
// comments are // only (# belongs to tags)
pack aws // icon packs this file draws from (aws |
pack azure // azure | gcp | logos | k8s). Optional — any
// installed pack resolves without it — but
// declaring documents intent and catches a
// misspelled pack name at check time.
person customer "Customer" // human actor. This form is top-level
// only — inside a system write it as
// `who = person "Operator"`. Either
// takes a block: `{ description: "…" }`
gw = aws/api-gateway "Edge Gateway" // components may sit at the top level too —
// that's how things stay individually
// visible at landscape altitude (see
// "Grouping vs. nesting" under Views)
system shop "Order Service" { // systems/containers nest arbitrarily
description: "Checkout and orders" // optional; shows on the collapsed card
icon: aws/api-gateway // the card's own mark; without it the
// first child's icon stands in
glyph: sys/code // kind mark, in a chip at the card's
// top right; a bad ref is a check error
domain: "orders" // optional owner/team tag on the card's
// shelf, beside the child icons
tags: #core // tags inherit to everything inside
api = aws/api-gateway "API Gateway" // id = pack/icon "Label"
create = aws/lambda "Create Handler" {
description: "Validates and persists"
subtitle: "Lambda · Node 20" // a few words under the label, always
// drawn: runtime, owner, region
link: "https://runbooks.acme.dev/create" // makes the title a hyperlink, in
// quotes, http(s) only, leaves only: a
// card's click zooms. Only a URL you were
// given (quality bar 7)
tags: #pci
}
db = aws/dynamodb "Orders Table" datastore #pci
// a tag can sit in kind position like that, or in the block — same thing:
idx = aws/opensearch "Search Index" datastore {
tags: #pci // kind and attr block go in either order
}
wh = sys/database "SQL Warehouse" datastore {
badge: logos/databricks // small vendor mark on the icon plate —
// see "Platforms with no icon pack" below
}
legacy = box "Old Billing" external // `box` = no icon
// kinds: `external` (not ours — draws a hatched surface, and also goes on a
// whole system: `system stripe "Stripe" external { … }`) and `datastore`
// (holds state — a note to the reader and to `squinch diff`; your icon
// choice is what actually shows it). For a human, use the `person` forms
// above rather than a kind.
api -> create // sync edge (solid)
api -> create, get, search // fan-out
db ~> sync "DynamoDB stream" // async edge (dashed); label optional
api -> create { tags: #hot-path } // edges take tags and attrs too
api -> db { color: amber } // a hue on the stroke + head — see Colour
a <-> b // bidirectional; a -- b undirected
}
customer -> shop.api "places order" // cross-system edges use dotted paths
zone prod_vpc "VPC prod" vpc { // deployment boundary — cross-cuts the
contains shop // ownership tree; renders as the classic
} // dashed frame around its members
```
Rules that matter:
- **Ids are unique within their container**; refer to nested things as `shop.api`
from outside, bare `api` from inside.
- Statements end at newline (or `;`). Labels are quoted strings.
- **Commas are optional wherever whitespace already separates** — `rows [a, b]`,
`align a, b`, `highlight #a, #b`, `{ style: dashed, animate: slow }` all parse,
as does a trailing comma. They stay *required* in a path list (`a -> b, c`,
`contains`, `channel`, `only`, `expand`), and stay wrong inside a tag value: write
`tags: #a #b`, never `tags: #a, #b`.
- Parallel edges between the same pair are fine — give each a label.
- **A system you are not breaking down is a node, not an empty system.**
`system partner "Partner System" external { }` gives you a card with nothing
behind it and a zoom that goes nowhere; `check` warns that the system is
empty. Write `partner = box "Partner System" external` instead.
- A `layout { }` block goes inside a `view` (arranging the view) **or inside a
`system`** (arranging that system's own interior — `rows`, `cols`, `place`
and `direction`, written by short name; see Layout hints). Everything else
— `channel`, `align`, `wrap`, `density`, `highlight`, `note`, `expand` —
belongs to a view alone.
Tables — a database is one node until the reader asks what is *in* it. Then
write it as a `container` holding one `sys/table` per table: it still draws as
one card where it sits, a dive opens it onto the tables, and a foreign key is
an ordinary labelled edge. A caller points at the container (`svc -> db`) or
at the table it touches (`svc -> db.orders`). `datastore` moves to the tables
— on a container it is a check error — and `icon:` keeps the database's own
mark on the card. Tables are as deep as it goes: nothing draws columns, so
leave them out. A database nobody asked to open stays a node, and so does a
table with no database above it. On a serverless or managed store where the
table is itself the resource you deploy, write one node per table inside the
service that owns it, with no container invented to hold them.
```squinch
system shop "Order Service" {
svc = aws/lambda "Order Handler"
container db "Orders DB" {
icon: aws/rds
description: "Postgres 16 · 3 tables"
customers = sys/table "customers" datastore
orders = sys/table "orders" datastore { subtitle: "partitioned monthly" }
payments = sys/table "payments" datastore
orders -> customers "customer_id" // a foreign key, child to parent
payments -> orders "order_id"
}
svc -> db.orders "writes"
}
```
Edge motion — `~>` edges animate on their own (dashes drift toward the target,
off under `prefers-reduced-motion`). Opt out with `{ animate: false }`, or pick
a variant: `reverse` (acks flowing back), `slow`/`fast` (cadence), `packets`
(discrete messages), `pulse` (a heartbeat — works on solid sync edges too),
`comet` (a dot rides the route — the only motion a plain solid edge can take).
Sync edges take `style: dotted` (`dashed` warns next to `~>` edges — dashes
are the async convention), and a dotted sync edge may also animate. One `animate:` value per edge, and don't decorate every edge — motion
is for the hops where cadence or direction *means* something.
```squinch
probe -> legacy "healthcheck" { animate: pulse }
sensor ~> ingest "telemetry" { animate: packets }
cart -> pay "checkout" { animate: comet } // no `style:` needed
mirror -> replica "sync" {
style: dashed
animate: slow
}
```
Colour — `color:` takes one of nine hues, `red | amber | green | teal | blue |
violet | pink | gray | accent`, never hex (each is a designed light/dark pair,
and a literal cannot be right on both). It goes on anything: a leaf, a person
or a `system` gets a spine down its left edge, an edge its stroke and head, a
zone its outline. Use it to say *these belong together* or *this is the path*
without dimming everything else the way `highlight` does. The stronger form is
the view statement `color #tag red`, which colours every visible thing carrying
the tag, one tag per line — it wins over an element's own `color:`, and with
`legend auto` each coloured tag earns a legend entry. Colour is annotation:
async stays dashed, context stays muted, so a greyscale print loses only the
emphasis.
Zones mark deployment boundaries: `zone id "Label" kind { contains a, b.c }`,
kinds `account | region | vpc | subnet | network | cloud | onprem | custom`.
Optional attrs: `icon:` — **any** pack icon (`azure/vnet`, `logos/docker`; AWS
ships purpose-made group marks like `aws/vpc`, `aws/region`,
`aws/private-subnet`, `aws/corporate-data-center`) — `label: top-right`
(corners: top-left default, top-right, bottom-left, bottom-right), and
`color: teal` (the nine hues above; never hex). A zone's `kind` already picks
its tint — `account` red, the network kinds blue, `cloud` violet, the rest
gray — so two nested zones of related kinds come out nearly the same shade:
set `color:` on the inner one to tell them apart. `detail: "10.0.0.0/16"` adds a second, monospaced
segment to the chip for the boundary's hard fact — a CIDR, an account id, a
region. It is dropped rather than truncated on a boundary too narrow for both,
since a clipped CIDR is a different network, not a shortened label.
**Zones nest by sharing members, never by naming each other.** `contains` takes
nodes, so an outer boundary repeats the inner one's members:
```
zone account "Azure Subscription" account { contains gw, aks, sql }
zone vnet "Virtual Network" network { contains aks, sql }
```
`aks` and `sql` are in both, so `vnet` draws inside `account`. Naming the inner
zone — `contains gw, vnet` — is accepted as shorthand for exactly that
expansion. Everywhere *else* a zone id is an error: you cannot draw an edge to
a boundary. Zones must nest cleanly or stay disjoint in any one view, may not
cut through an expanded container, and only appear where their members are
visible.
## Views (altitudes)
Every system automatically gets a zoomable view. Declare views to customize or to
add lenses — and declare one for any part the ask singles out ("I care most
about orders"): `render --sync` writes only declared views, so an auto view the
reader was promised never reaches them as SVGs. `view orders { title "…" }` is
enough.
```squinch
view landscape { // views take no positional label — the title is a
title "System Landscape" // statement. It draws top-left as the diagram's
// name, with or without a `titleblock`.
include * // all TOP-LEVEL entities, as collapsed cards
}
view shop { // name matching a system = that system's view
scope shop // implied by the name here; explicit for clarity
only #pci // KEEP only these — the view's filter. `scope` says
// where you stand, `only` says which of it you keep.
// Takes ids too: `only api, vault`
exclude legacy // trim noise (removes the subtree)
expand workers // inline one child container in a frame — one
// level; nesting two explicit expands is an error.
// Several: `expand workers, ledger`, or one per line
detail ledger.post // draw an outside node itself, not its system card
highlight #pci // spotlight matches, dim the rest — this still
// shows EVERYTHING; "only the PCI parts" is `only`
color #pci red // colour everything tagged #pci, dim nothing —
// one tag per line; wins over an element's own color:
note right-of db "Single-table design; see ADR-42"
note top-right "Audit scope: Q3" { style: warning }
context off // drop the muted neighbour cards this view earned
// (default is `context auto`)
legend auto // footer key of the styles actually used
titleblock { // a meta chip under the title. version/commit/
subtitle: "Landscape view" // date are reserved — their values stand alone,
version: "2026-07" // and `commit` sets in mono. Any other key
commit: "a41f0c2" // keeps its key beside its value.
owner: team-orders // Nothing here is derived: no git, no clock.
}
}
```
When the reader wants **everything on one page** — every container open to
leaf depth, no clicking around — declare a full-detail view with `expand *`
(declare it after the landscape so the breadcrumb keeps pointing home). It
composes with the other verbs: `scope orders` + `expand *` opens one subtree.
```squinch
view full {
title "Full detail"
expand * // open every container, frames nesting as they go;
// empty containers stay cards, edges de-aggregate
}
```
Between a card and an open frame there is one more altitude: **`preview
<path>`** (a comma list, or `preview *`) draws a card **detailed** — the children its
`preview:` attr names (§ containers; up to three, `auto` takes the first three
declared) as readable rows under the head, `+N more` on the shelf for the
rest. It is still a card: every wire lands on it, nothing attaches to a row,
and a dive opens the container. Reach for it on the *landscape*, when the
reader should see what a system is made of without opening it — a domain and
its capabilities, a platform and its services — and only where those children
carry a `description:` or `subtitle:`; three bare names say little more than
the chips. Do not put it on every view: a dense view has no room for a card
twice its height, and `preview` beside `expand` on the same container is an
error.
```squinch
system orders "Orders" {
description: "Order lifecycle, cart to refund"
preview: [checkout fulfillment returns] // what the card calls out; pricing folds into +1
system checkout "Checkout" {
description: "Cart to confirmed order"
api = aws/lambda "Checkout API"
}
system fulfillment "Fulfillment" {
description: "Pick, pack, dispatch"
worker = aws/lambda "Dispatcher"
}
system returns "Returns" {
description: "Refunds and RMAs"
api = aws/lambda "Returns API"
}
system pricing "Pricing" {
description: "Prices, promotions, tax"
api = aws/lambda "Pricing API"
}
}
view landscape {
include *
preview orders // the Orders card, with its three parts named
}
```
Numbered flows badge a request's path over **edges that already exist** — a
flow annotates the model, it never creates connections. A step with no edge
behind it is a check error telling you to declare the edge first (steps count
in declaration order; bare ids bind when unambiguous, otherwise use full
paths):
```squinch
system shop "Shop" {
api = aws/api-gateway "API"; create = aws/lambda "Create"
db = aws/dynamodb "Orders"; files = aws/s3 "Files"
api -> create // these edges…
create -> db
create ~> files
}
flow checkout "Checkout" {
api -> create -> db // …are what the flow numbers: steps 1, 2
create ~> files // step 3 — branches keep counting
}
view shop { show flow checkout }
```
`flow` blocks live at the **top level**, beside your systems — not inside a
`system` and not inside a `view` (the view only says `show flow <id>`). From out
there, write steps as full paths (`shop.api -> shop.create`). A step may cross a
view's `scope`. A flow is also a story: in the playground's **Present** mode the
arrow keys walk a `show flow` view one hop at a time — nothing extra to author.
Grouping vs. nesting: `include *` shows only *top-level* entities, so wrapping
several services in a parent `system` purely to group them collapses them into
one card at landscape altitude. If they should stay individually visible but
share a boundary, keep them top-level and group them with a `zone`.
### When the system is big
A reader can only take in so much at once, so on a large system — a whole
repo, a platform — size is a modelling decision, made before layout. A small
system needs none of this: keep it flat.
- **Group before you draw.** Sort the components into the areas the system
already has — its top-level folders, its teams, its domains — and make each
a `system`. Use the names the code uses. If nothing hands you the areas,
find them: things that call each other and change together. The landscape
then draws one card per area with the calls between them merged (`×4`), and
every area gets its own zoomable view for free.
- **It holds at every altitude.** Opening an area should read as easily as the
landscape does. Every service inside one `backend` system has only moved
the problem down a click — group by what things are *for* (identity,
catalogue, buying, fulfilment), and nest a system inside a system when an
area is itself big.
- **What a thing owns lives inside it.** A service's own database, cache or
worker belongs in that service's area, not beside it on the landscape. What
everyone shares — a gateway, an event bus — stays top-level.
- **One caller with many targets is the shape that runs off the page**: a UI
or gateway calling every service draws them all in one row. Grouping the
targets is the fix. `wrap N` folds a fan-out only when the targets do not
call each other, and a real system's usually do. Never answer it by listing
every target in one `rows` band — that forces exactly the strip you are
trying to avoid. Leave the targets unhinted and let the engine rank them.
- **Hand over altitudes, not one page.** Declare the landscape first, then a
view for each area worth opening; the interactive HTML lets the reader
click down. `view full { expand * }` is for when they ask for everything at
once. Say in your hand-over how you grouped it, so the reader can ask you
to open an area up rather than start over.
- **Let the landscape say what each area is made of.** Give every area a
`description:`, and when its parts are things the reader would recognise —
the services of a platform, the capabilities of a domain — name the three
that matter in `preview: [a b c]` and put `preview *` on the landscape view.
Each card then lists those three under its name, still as one card, and the
reader knows what is inside before clicking. Only there: a zoomed view has no
room for cards twice the height.
Zoomed views automatically show outside neighbours as muted **context** cards —
don't add them yourself; if one appears that you don't want, `context off`.
## Layout hints (in a `layout { }` block — a view's, or a system's own)
Hint conflicts are the single biggest source of failed `check` runs in this
project's history — almost always a `rows` that pins every node, colliding with
one feedback edge. The engine ranks a clean pipeline correctly on its own;
every hint you add is a constraint you have to keep true.
```squinch
view shop {
layout {
direction down // down (default) | right
density comfortable // compact | comfortable | spacious
lines orthogonal // orthogonal (default) | curved | straight
rows [api] [create get search] [db files idx] // horizontal bands, top to bottom
cols [create db] [get files] // vertical bands, left to right (shared axis)
place sync right-of db // right-of | left-of | above | below
align gw db // exact shared axis; first one is the anchor
channel create, get, search -> db // one trunk into a shared target, not N lines
// (the edges stay declared in the model;
// this only merges how they are drawn)
route api -> db from south to north // which side an edge exits/enters —
// only for edges that SPAN rows; a
// same-rank edge is routed for you
route api -> db "write" from south // label disambiguates parallels
}
}
view steps { include *
layout { wrap 5 } } // a long chain or a wide fan-out, folded
```
- `rows` is the workhorse: one bracket group per horizontal band, listed top to
bottom; order inside a bracket is left to right. Unlisted nodes place
themselves — only list a node when you care where it lands. Do **not** use
it to fold a long chain or a wide fan-out into several bands by hand: the
edges that skip a band detour around it and the fold reads worse than the
wide version. That is what `wrap N` is for — it folds the chain as a
serpentine, or the fan-out onto a bus, and routes the fold cleanly.
- **`wrap N` puts N in each band, and N sets the shape** — for a chain and a
fan-out alike. For a slide, roughly 16:9, use N ≈ √(nodes being folded),
rounded up: a 14-step chain is `wrap 4`, and so are twelve workers under
one dispatcher. Half per band (`wrap 7` on fourteen) is a ribbon, not a
slide. Leave `direction right` out of a wide fold: a band is a rank, and
under `direction right` ranks are columns, so `wrap 7` becomes two tall
columns of seven.
- **Every edge must point down your bands** — check each band against your
arrows before you write it. The bands are a claim about direction, and a node
pointing back *up* the list is a check error, not a nudge. This bites on
monitoring, feedback and retry paths: `mon -> api` under
`rows [api] [svc] [db] [mon]` is refused. Two fixes, both fine: put the
observer in the **same** band as what it watches (`rows [api mon] [svc] [db]`
— equal ranks are legal and route side to side), or leave it out of `rows`
and let the engine rank it.
- `cols` is its transpose: one bracket group per vertical band, left to right.
Members of a column share an exact axis, so a service and its database line
up. `rows` and `cols` compose — they pin different axes, so using both gives
you a full grid, and a cell is empty when nobody is placed in it.
- **`cols` and `align` work across bands, never within one.** Six collectors
that all feed one normaliser are siblings on a single rank — they are already
drawn side by side, and no two of them can share an axis, so `cols [c1 … c6]`
is refused. If you want them beside each other, that is `rows [c1 … c6]`. The
trap is worst under `direction right`, where a rank *looks* like a column on
screen: the words name the model, not the picture.
- **A system's interior is arranged by the system itself.** Give the system
its own `layout { }` with `rows`/`cols`/`place` in short names, and that
arrangement follows it into every view that opens it — expanded beside its
siblings, or standing inside it in the system's own view. Direct members
only: a nested container gets its own block. The view then only ranks
systems against each other, and never restates an interior:
```squinch
system checkout "Checkout" {
api = aws/api-gateway "API"; svc = aws/lambda "Orders"
bus = aws/sqs "Events"; cache = aws/elasticache "Sessions" datastore
db = aws/dynamodb "Orders" datastore
api -> svc; svc ~> bus; svc -> cache; svc -> db
layout { rows [api] [svc] [bus cache] [db] } // short names, inside
}
system pipeline "Pipeline" {
ingest = aws/lambda "Ingest"; enrich = aws/lambda "Enrich"; publish = aws/lambda "Publish"
ingest -> enrich; enrich -> publish
layout { direction right } // this interior reads left to right
}
view detail {
expand checkout
expand shipping
layout { rows [cdn] [checkout shipping] } // the view ranks the systems
}
```
`direction right` in a system's block lays that system's interior out left
to right wherever it is opened, inside a view that still flows down — the
way to draw a pipeline as a row. It is a property of the system, not of the
view: there is no per-view override for an expanded frame.
A view may still name interior paths (`rows [gw] [checkout.api]`); when its
hints relate two or more of a system's members they replace that system's
block in that view, whole — bands are never merged. Naming one member only
ranks the whole system from outside. Two members of one system asked to
share a row with an edge between them is a warning: same-rank edges route
between systems, not inside one, so put one below the other or collapse the
system in that view.
- **Rank hints don't reach inside a zone.** A zone lays out as a single block —
`rows`/`cols`/`place` order zones *relative to each other*, and the engine
arranges the members within. To rank a zone, name **one** member of it (or
the zone's own id): `rows [gw] [prod_vpc]` puts the whole boundary below the
gateway. Listing every member does nothing but earn a warning. If the ranking
matters more than the boundary, drop the zone. Two zones *may* share a band
with edges running between them — `rows [batch orders]` on two namespaces
holds, and the edge routes through the gutter like one between expanded
containers.
- A node may be in a band **and** carry a `place`, so long as the two agree —
`rows [db bus]` with `place bus right-of db` is fine; a `place` that puts the
node somewhere the band does not is refused.
- `place x right-of y` is the whole side-car idiom (stream processors, caches,
DLQs). Do **not** add `route y ~> x from east to west` to it: `place` puts
the two on the same row, and same-rank edges route side to side
automatically — a `from`/`to` on one is ignored, and says so.
The cookbook in `references/cookbook.md` is the next step when a diagnostic is
not self-explanatory or the render shows a shape you cannot name the hint for —
symptom → fix, keyed on the diagnostic's own words.
## Icons
**Read `references/icons.md` when you need an icon id outside the AWS basics**:
it lists the ids and aliases per pack, the Google Cloud category rule in full,
the Kubernetes short names, the `sys/` rows by theme, and the Databricks recipe
worked out component by component. Open it before you write the node, not after
`check` refuses the id.
When the request names a **specific product**, search for it — don't write the id
from memory. `check` only tells you an id exists, never that it's the one the
reader asked for, so a plausible-but-wrong mark passes silently and ships. The
confusable pairs are the ones to watch: CloudFront (`aws/cloudfront`, a CDN) is
not Cloudflare (`logos/cloudflare`, a different company); `aws/aurora` is not
`aws/rds`. One `squinch icons search cloudfront` settles it.
The AWS ids you'll use constantly: `lambda` · `dynamodb` · `s3` · `sqs` · `sns` ·
`api-gateway` · `opensearch` · `aurora` · `rds` · `elasticache` · `cloudfront` ·
`eventbridge` · `kinesis` · `step-functions` · `ecs` · `eks` · `fargate` · `ecr` ·
`athena` · `glue` · `redshift` · `sagemaker` · `bedrock` · `rekognition` ·
`cognito` · `secrets-manager` · `route-53` · `waf` · `elastic-load-balancing`
(alias `elb`) · `batch` · `efs` · `app-runner`.
Six packs, and the ids you'll reach for in each:
- **`aws/`** — 316 icons; the basics above.
- **`azure/`** — 636 icons, named like the portal (`azure/app-services`,
`azure/storage-accounts`, `azure/monitor`), with short forms for the ones
everybody abbreviates: `azure/aks` · `azure/vm` · `azure/vnet` ·
`azure/cosmos` · `azure/functions` · `azure/sql` · `azure/blob` ·
`azure/service-bus` · `azure/event-hub` · `azure/key-vault` ·
`azure/front-door` · `azure/app-gateway` · `azure/load-balancer` ·
`azure/aci` · `azure/acr` · `azure/api-management` · `azure/log-analytics` ·
`azure/redis`.
- **`gcp/`** — 45 icons, because Google's own system works that way: nineteen
products have a unique mark (`gcp/cloud-run`, `gcp/gke`, `gcp/bigquery`,
`gcp/cloud-sql`, `gcp/spanner`, `gcp/vertex-ai`…), and *every other product
draws its category's glyph* under the name you know it by (`gcp/pubsub`
draws Data Analytics). Put the product name in the label and let the icon
say the category.
- **`k8s/`** — 39 icons under kubectl's short names, for what runs *inside* a
cluster: `k8s/pod` · `k8s/deploy` · `k8s/svc` · `k8s/sts` · `k8s/ds` ·
`k8s/cm` · `k8s/secret` · `k8s/ing` · `k8s/ns` · `k8s/sa` · `k8s/pv` ·
`k8s/pvc` · `k8s/hpa` · `k8s/job` · `k8s/cronjob` · `k8s/crd` · `k8s/node` ·
`k8s/etcd` · `k8s/control-plane` · `k8s/api` · `k8s/sched` · `k8s/kubelet`.
`logos/kubernetes` when the cluster is one box in a wider estate.
- **`logos/`** — 147 product marks plated in their brand colour, the non-cloud
half of a stack: `logos/postgres` · `logos/mysql` · `logos/mongodb` ·
`logos/redis` · `logos/kafka` · `logos/rabbitmq` · `logos/elasticsearch` ·
`logos/docker` · `logos/terraform` · `logos/nginx` · `logos/github` ·
`logos/gitlab` · `logos/grafana` · `logos/prometheus` · `logos/datadog` ·
`logos/sentry` · `logos/stripe` · `logos/snowflake` · `logos/cloudflare` ·
`logos/vercel` · `logos/react` · `logos/python` · `logos/go` · `logos/rust`.
Slack, Twilio, Salesforce, Heroku, gRPC and dbt have no mark upstream — use
`box` or the label.
- **`sys/`** — 195 generic Lucide icons for what no vendor draws, and it needs
no `pack` statement. Ids are Lucide's own names, so a word you would say
(`process`, `analytics`, `device`, `web`) is usually *not* one — pick from
these: compute `server` `container` `cpu` `code` `app-window` `terminal`
`cog` `webhook` `workflow`; hardware `laptop` `monitor` `smartphone`
`hard-drive` `printer`; network `network` `router` `wifi` `radio-tower`
`globe` `share-2`; security `lock` `key-round` `shield` `shield-check`;
data `database` `folder` `search` `archive` `table` `file`; places `factory`
`warehouse` `building-2` `house` `earth`; process and observability `clock`
`timer` `repeat` `activity` `gauge` `chart-line` `siren` `bug`; commerce
`shopping-cart` `credit-card` `wallet` `receipt-text` `truck`
`package-check` `barcode`; shapes `box` `circle` `square` `hexagon`
`diamond` `star`. Aliases: `gear`→`cog`, `db`→`database`,
`rack`/`vm`/`host`→`server`, `firewall`→`shield`, `vault`→`lock-keyhole`,
`cron`→`clock`, `lb`→`share-2`, `cart`, `payment`, `order`, `shipping`.
Pick one cloud's pack and stay with it — don't draw the same concept as
`aws/…` in one box and `azure/…` in the next. Composing a cloud pack with
`logos` and `sys` is normal; it's how you draw a hybrid estate. A platform with
no pack at all — Databricks, Snowflake, dbt, Confluent publish no icons anyone
may redistribute — is drawn as the concept from `sys/*` with the vendor's mark as
a `badge:` where `logos/*` has one; never a lookalike from another vendor, never
an invented id.