diff --git a/.claude/skills/datum-diagram/SKILL.md b/.claude/skills/datum-diagram/SKILL.md
index 6028f7f7..e680af34 100644
--- a/.claude/skills/datum-diagram/SKILL.md
+++ b/.claude/skills/datum-diagram/SKILL.md
@@ -49,16 +49,30 @@ Exported from the Figma "Components - Diagram" file (PNG inside SVG). Complex se
In the Strapi body, write the designed image, then the Mermaid fence directly after it:
````
-
+
```mermaid
flowchart TB
- …
+ a["first"]:::datum -.-> b["second"]:::ext
+```
````
-```
+`src/plugins/rehypeMermaid.ts` turns that pair into an "Illustrated | Mermaid" tab set (`src/components/DiagramTabs.astro`, `components-diagram-tabs.css`). Illustrated shows on landing; the Mermaid panel renders off-screen and appears when its tab is chosen, with a "Copy Mermaid" button. The caption sits under both. A mermaid fence with no image right before it renders alone, as before.
+
+## Mermaid layout rules
-`src/plugins/rehypeMermaid.ts` turns that pair into an "Illustrated | Mermaid" tab set (`src/components/DiagramTabs.astro`, `components-diagram-tabs.css`). Illustrated shows on landing; the Mermaid panel renders off-screen and appears when its tab is chosen, with a "Copy Mermaid" button. The caption sits under both. A mermaid fence with no image right before it renders alone, as before. Stack panels vertically in Mermaid (root `flowchart TB` plus an invisible `a ~~~ b` link); side by side makes everything small.
+- Aim for a natural width under about 650px (it scales to the article column; narrower reads better on a phone). Stack panels and steps top to bottom.
+- Stack panels with a root `flowchart TB` plus invisible `a ~~~ b` links.
+- A subgraph's own `direction` is ignored as soon as an edge crosses its boundary. To get a tall column of nodes that all link to one hub, use a root `flowchart LR` and link each node individually (see `examples/mermaid/landAnything.mmd`). Do not link the subgraph itself.
+- Request flows read best as a real `sequenceDiagram` (see `examples/mermaid/requestFlow.mmd`); the theme styles them.
+- Do not put a diagram's grey outer panel in Mermaid as a coloured node: panels are subgraphs (dashed grey), moss is for Datum nodes only.
+
+## Examples
+
+- `examples/diagrams.mjs`: the seven IPv6 article diagrams (designed).
+- `examples/desktop-article.mjs`: the two Datum Desktop article diagrams (direct vs relay, request flow).
+- `examples/five-articles.mjs`: four-flavors, Galactic VPC (land anything, private by default), every-agent-needs-an-edge, network-vs-compute.
+- `examples/mermaid/`: the matching Mermaid versions.
## Gotchas learned
@@ -67,8 +81,9 @@ flowchart TB
- Mermaid `look: handDrawn` does not apply to sequence diagrams and was dropped.
- Zero-length path segments (identical consecutive points) draw nothing: `line()` de-duplicates them.
- Strapi bodies: publishing is outward-facing. Draft first, ask before publishing.
+- After publishing, check the live page on the plain URL several times. Right after a deploy the site can serve the build-time article cache; re-saving the published article in Strapi (or re-publishing in the admin) re-fires the revalidation webhook.
+- The Mermaid theme is read when the Astro server starts; restart the dev server to see theme changes.
## Facts to check before drawing
See `reference/datum-glossary.md`. It is a starting point, not a source of truth: confirm against https://www.datum.net/docs before stating a technical fact in a diagram.
-```
diff --git a/.claude/skills/datum-diagram/examples/desktop-article.mjs b/.claude/skills/datum-diagram/examples/desktop-article.mjs
new file mode 100644
index 00000000..99c1c3db
--- /dev/null
+++ b/.claude/skills/datum-diagram/examples/desktop-article.mjs
@@ -0,0 +1,114 @@
+import {
+ C,
+ SW,
+ text,
+ tag,
+ label,
+ badge,
+ ext,
+ datum,
+ tile,
+ extFrame,
+ line,
+ deco,
+ svg,
+} from '../kit.mjs';
+
+const mono = (x, y, s, o = {}) => text(x, y, s, { mono: true, size: 12.5, fill: C.muted, ...o });
+
+// direct UDP vs relay: both paths tried in parallel, fastest wins
+export const pathsDirectRelay = () => {
+ const W = 1140,
+ H = 600;
+ let b = '';
+ b += deco(6, 1000, 470, 150);
+ b += extFrame(30, 30, 1080, 520, 'Two paths between endpoints, tried in parallel');
+ // endpoints
+ b +=
+ tile(190, 150, 230, 250) +
+ datum(190, 255, 'hub', { r: 46 }) +
+ text(190, 345, 'Gateway', { size: 17, weight: 500 }) +
+ text(190, 368, 'endpoint', { size: 15, fill: C.muted });
+ b +=
+ tile(950, 150, 230, 250) +
+ datum(950, 255, 'laptop', { r: 46 }) +
+ text(950, 345, 'Desktop', { size: 17, weight: 500 }) +
+ text(950, 368, 'node', { size: 15, fill: C.muted });
+ // direct path (top)
+ b += line(
+ [
+ [310, 215],
+ [830, 215],
+ ],
+ 'ok',
+ { both: true }
+ );
+ b += tag(570, 215, 'Direct UDP · hole-punched', { fill: C.pine });
+ // relay path (bottom) through the home relay
+ b +=
+ tile(570, 340, 210, 130) +
+ datum(570, 392, 'cloud', { r: 30 }) +
+ text(570, 450, 'Home relay', { size: 15, weight: 500 });
+ b += line(
+ [
+ [310, 385],
+ [458, 385],
+ ],
+ 'neutral',
+ { both: true }
+ );
+ b += line(
+ [
+ [682, 385],
+ [830, 385],
+ ],
+ 'neutral',
+ { both: true }
+ );
+ b += tag(384, 363, 'encrypted', { fill: C.ink }) + tag(756, 363, 'encrypted', { fill: C.ink });
+ // outcome
+ b += `${text(570, 508, 'both paths tried in parallel · fastest wins, automatic failover', { size: 15, weight: 500, fill: C.navy })}`;
+ return svg(W, H, b);
+};
+
+// request flow: Envoy -> iroh-gateway -> Desktop node
+export const requestFlow = () => {
+ const W = 1140,
+ H = 900;
+ let b = '';
+ b += deco(4, 1050, -90, 150);
+ const cols = [
+ [210, 'Envoy', 'ext'],
+ [570, 'iroh-gateway', 'datum'],
+ [930, 'Desktop node', 'datum'],
+ ];
+ for (const [x, name, kind] of cols) {
+ b += ``;
+ if (kind === 'datum') b += tile(x, 40, 210, 100) + text(x, 98, name, { size: 17, weight: 500 });
+ else
+ b += `${text(x, 98, name, { size: 17, weight: 500, fill: C.slate })}`;
+ }
+ const step = (n, x1, x2, y, t, note, kind = 'neutral', fill) => {
+ const cx = (x1 + x2) / 2;
+ const tagHalf = (t.length * 11 * 0.62 + 18) / 2;
+ let s =
+ line(
+ [
+ [x1, y],
+ [x2, y],
+ ],
+ kind
+ ) +
+ tag(cx, y - 24, t, { fill: fill ?? C.ink }) +
+ badge(cx - tagHalf - 20, y - 24, n);
+ if (note) s += text(cx, y + 26, note, { size: 14, fill: C.muted });
+ return s;
+ };
+ b += step(1, 215, 565, 230, 'HTTP request', '(via Unix socket)');
+ b += step(2, 575, 925, 340, 'resolve endpoint ID', 'from request metadata');
+ b += step(3, 575, 925, 470, 'open QUIC stream', '(reuse connection or establish new one)');
+ b += step(4, 575, 925, 600, 'forward request', 'over QUIC stream', 'ok', C.pine);
+ b += step(5, 925, 575, 730, 'HTTP response', null, 'neutral');
+ b += step(6, 565, 215, 810, 'HTTP response', null, 'neutral');
+ return svg(W, H, b);
+};
diff --git a/.claude/skills/datum-diagram/examples/diagrams.mjs b/.claude/skills/datum-diagram/examples/diagrams.mjs
index a43ac1eb..3bb2febe 100644
--- a/.claude/skills/datum-diagram/examples/diagrams.mjs
+++ b/.claude/skills/datum-diagram/examples/diagrams.mjs
@@ -1,128 +1,410 @@
-import { logo, deco, tile, C, SW, text, tag, label, badge, ext, datum, rack, icon, datumFrame, extFrame, line, hatch, swoosh, squares, svg } from '../kit.mjs';
+import {
+ logo,
+ deco,
+ tile,
+ C,
+ SW,
+ text,
+ tag,
+ label,
+ badge,
+ ext,
+ datum,
+ rack,
+ icon,
+ datumFrame,
+ extFrame,
+ line,
+ hatch,
+ swoosh,
+ squares,
+ svg,
+} from '../kit.mjs';
const mono = (x, y, s, o = {}) => text(x, y, s, { mono: true, size: 12.5, fill: C.muted, ...o });
const outcome = (x, y, w, h, lines, o = {}) =>
- `${text(x + w / 2, y + h / 2 - ((lines.length - 1) * 11) + 5, lines, { size: o.size ?? 16, lh: 22, fill: o.color ?? C.navy, weight: 500 })}`;
+ `${text(x + w / 2, y + h / 2 - (lines.length - 1) * 11 + 5, lines, { size: o.size ?? 16, lh: 22, fill: o.color ?? C.navy, weight: 500 })}`;
// 1 ─ dual stack vs IPv6-native
export const image1 = () => {
- const W = 1240, H = 960;
+ const W = 1240,
+ H = 960;
let b = '';
b += deco(4, 1090, -70, 200) + deco(6, -50, 850, 140);
// panel A (everywhere else = external, grayscale)
b += extFrame(50, 40, 1140, 400, 'Everywhere else: dual stack');
b += ext(170, 270, 'laptop') + text(170, 345, 'client', { size: 16 });
- b += logo(640, 270, 'nginx') + text(640, 345, 'nginx', { size: 16 }) + mono(640, 366, 'listen 8080;', { anchor: 'middle' });
- b += line([[236, 232], [574, 232]], 'neutral') + badge(262, 232, 1) + tag(405, 232, 'try IPv6');
- b += line([[574, 270], [236, 270]], 'bad') + tag(405, 270, 'refused', { fill: C.clay });
- b += line([[236, 308], [574, 308]], 'ok') + badge(262, 308, 2) + tag(405, 308, 'quietly retry IPv4', { fill: C.pine });
- b += line([[708, 270], [880, 270]], 'ok');
+ b +=
+ logo(640, 270, 'nginx') +
+ text(640, 345, 'nginx', { size: 16 }) +
+ mono(640, 366, 'listen 8080;', { anchor: 'middle' });
+ b +=
+ line(
+ [
+ [236, 232],
+ [574, 232],
+ ],
+ 'neutral'
+ ) +
+ badge(262, 232, 1) +
+ tag(405, 232, 'try IPv6');
+ b +=
+ line(
+ [
+ [574, 270],
+ [236, 270],
+ ],
+ 'bad'
+ ) + tag(405, 270, 'refused', { fill: C.clay });
+ b +=
+ line(
+ [
+ [236, 308],
+ [574, 308],
+ ],
+ 'ok'
+ ) +
+ badge(262, 308, 2) +
+ tag(405, 308, 'quietly retry IPv4', { fill: C.pine });
+ b += line(
+ [
+ [708, 270],
+ [880, 270],
+ ],
+ 'ok'
+ );
b += outcome(890, 224, 250, 92, ['200 OK', 'nobody notices']);
// panel B (Datum Compute)
b += extFrame(50, 490, 1140, 420, 'Datum Compute: IPv6-native');
b += tile(190, 665, 160, 185);
b += datum(190, 735, 'hub') + text(190, 820, 'Datum ALB', { size: 16 });
- b += logo(640, 735, 'nginx') + text(640, 810, 'nginx', { size: 16 }) + mono(640, 831, 'listen 8080;');
- b += line([[280, 715], [574, 715]], 'neutral') + tag(415, 715, 'IPv6 only');
- b += line([[574, 758], [280, 758]], 'bad') + tag(415, 758, 'refused', { fill: C.clay });
- b += line([[190, 665], [190, 600], [880, 600]], 'bad', { r: 14 });
- b += outcome(890, 548, 250, 104, ['503', "the config's IPv4", 'assumption, surfaced'], { fill: C.clayPale, stroke: C.clay, color: C.clayDeep, size: 15 });
+ b +=
+ logo(640, 735, 'nginx') +
+ text(640, 810, 'nginx', { size: 16 }) +
+ mono(640, 831, 'listen 8080;');
+ b +=
+ line(
+ [
+ [280, 715],
+ [574, 715],
+ ],
+ 'neutral'
+ ) + tag(415, 715, 'IPv6 only');
+ b +=
+ line(
+ [
+ [574, 758],
+ [280, 758],
+ ],
+ 'bad'
+ ) + tag(415, 758, 'refused', { fill: C.clay });
+ b += line(
+ [
+ [190, 665],
+ [190, 600],
+ [880, 600],
+ ],
+ 'bad',
+ { r: 14 }
+ );
+ b += outcome(890, 548, 250, 104, ['503', "the config's IPv4", 'assumption, surfaced'], {
+ fill: C.clayPale,
+ stroke: C.clay,
+ color: C.clayDeep,
+ size: 15,
+ });
b += deco(12, 1120, 836, 52);
return svg(W, H, b);
};
// 2 ─ shared link vs one link per instance
export const image2 = () => {
- const W = 1300, H = 1000;
+ const W = 1300,
+ H = 1000;
let b = '';
b += extFrame(40, 60, 600, 860, 'Home LAN or Podman: a shared link');
b += ext(340, 230, 'hub') + text(340, 304, 'one shared link', { size: 16 });
- b += line([[340, 322], [340, 385]], 'link', { dot: true });
- b += line([[340, 385], [340, 410]], 'link', { solid: true, arrow: false });
- b += line([[140, 410], [540, 410]], 'link', { arrow: false });
- b += line([[140, 410], [140, 470]], 'link', { dotEnd: true });
- b += line([[340, 410], [340, 470]], 'link', { dotEnd: true });
- b += line([[540, 410], [540, 470]], 'link', { dotEnd: true });
+ b += line(
+ [
+ [340, 322],
+ [340, 385],
+ ],
+ 'link',
+ { dot: true }
+ );
+ b += line(
+ [
+ [340, 385],
+ [340, 410],
+ ],
+ 'link',
+ { solid: true, arrow: false }
+ );
+ b += line(
+ [
+ [140, 410],
+ [540, 410],
+ ],
+ 'link',
+ { arrow: false }
+ );
+ b += line(
+ [
+ [140, 410],
+ [140, 470],
+ ],
+ 'link',
+ { dotEnd: true }
+ );
+ b += line(
+ [
+ [340, 410],
+ [340, 470],
+ ],
+ 'link',
+ { dotEnd: true }
+ );
+ b += line(
+ [
+ [540, 410],
+ [540, 470],
+ ],
+ 'link',
+ { dotEnd: true }
+ );
b += ext(140, 540, 'router') + text(140, 614, 'router', { size: 16 });
b += ext(340, 540, 'laptop') + text(340, 614, 'a', { size: 16 });
b += ext(540, 540, 'laptop') + text(540, 614, 'b', { size: 16 });
- b += line([[540, 640], [540, 710], [340, 710], [340, 640]], 'ok', { r: 14, both: true });
+ b += line(
+ [
+ [540, 640],
+ [540, 710],
+ [340, 710],
+ [340, 640],
+ ],
+ 'ok',
+ { r: 14, both: true }
+ );
b += tag(440, 710, 'direct', { fill: C.pine });
b += text(440, 765, 'NDP finds b’s MAC', { size: 15, fill: C.slate });
// right: Datum, one link per instance
b += extFrame(700, 60, 560, 860, 'Datum: one link per instance');
b += tile(835, 140, 176, 200) + tile(1125, 140, 176, 200) + tile(980, 650, 200, 200);
- b += rack(835, 215) + text(835, 292, 'instance', { size: 16 }) + mono(835, 313, 'fd20:0:53::1:0:0');
- b += rack(1125, 215) + text(1125, 292, 'instance', { size: 16 }) + mono(1125, 313, 'fd20:0:53::2:0:0');
- b += line([[835, 345], [835, 560], [980, 560], [980, 650]], 'link', { dot: true, r: 16 });
- b += line([[1125, 345], [1125, 560], [980, 560]], 'link', { dot: true, arrow: false, r: 16 });
- b += line([[945, 215], [1015, 215]], 'bad', { both: true });
- b += tag(980, 184, 'link-local ×', { fill: C.clay }) + tag(980, 246, 'ff02::1 ×', { fill: C.clay });
+ b +=
+ rack(835, 215) + text(835, 292, 'instance', { size: 16 }) + mono(835, 313, 'fd20:0:53::1:0:0');
+ b +=
+ rack(1125, 215) +
+ text(1125, 292, 'instance', { size: 16 }) +
+ mono(1125, 313, 'fd20:0:53::2:0:0');
+ b += line(
+ [
+ [835, 345],
+ [835, 560],
+ [980, 560],
+ [980, 650],
+ ],
+ 'link',
+ { dot: true, r: 16 }
+ );
+ b += line(
+ [
+ [1125, 345],
+ [1125, 560],
+ [980, 560],
+ ],
+ 'link',
+ { dot: true, arrow: false, r: 16 }
+ );
+ b += line(
+ [
+ [945, 215],
+ [1015, 215],
+ ],
+ 'bad',
+ { both: true }
+ );
+ b +=
+ tag(980, 184, 'link-local ×', { fill: C.clay }) + tag(980, 246, 'ff02::1 ×', { fill: C.clay });
b += label(980, 470, ['same /64, still', 'via the router'], { size: 15, fill: C.slate });
- b += datum(980, 720, 'router') + text(980, 800, 'router', { size: 16 }) + mono(980, 821, 'fd20:0:53::1');
+ b +=
+ datum(980, 720, 'router') +
+ text(980, 800, 'router', { size: 16 }) +
+ mono(980, 821, 'fd20:0:53::1');
return svg(W, H, b);
};
// 3 ─ network → locations → instances
export const image3 = () => {
- const W = 1180, H = 1120;
+ const W = 1180,
+ H = 1120;
let b = '';
b += deco(3, 0, 10, 120) + deco(6, 1040, 960, 130) + deco(12, 1090, 30, 56);
b += tile(150, 480, 240, 220);
b += datum(150, 560, 'hub', { r: 56 });
- b += text(150, 650, 'network blog-net', { size: 17, weight: 500 }) + mono(150, 672, 'fd20:0:53::/48', { size: 13.5 });
+ b +=
+ text(150, 650, 'network blog-net', { size: 17, weight: 500 }) +
+ mono(150, 672, 'fd20:0:53::/48', { size: 13.5 });
const locs = [
- { y: 215, n: 'us-west-1', c: 'fd20:0:53:0::/64', kids: [[120, 'fd20:0:53::1:0:0/96'], [310, 'fd20:0:53::2:0:0/96']] },
+ {
+ y: 215,
+ n: 'us-west-1',
+ c: 'fd20:0:53:0::/64',
+ kids: [
+ [120, 'fd20:0:53::1:0:0/96'],
+ [310, 'fd20:0:53::2:0:0/96'],
+ ],
+ },
{ y: 560, n: 'us-central-1', c: 'fd20:0:53:2::/64', kids: [[560, 'fd20:0:53:2:0:1::/96']] },
{ y: 905, n: 'us-east-1', c: 'fd20:0:53:1::/64', kids: [[905, 'fd20:0:53:1:0:1::/96']] },
];
for (const L of locs) {
- b += line([[282, 560], [330, 560], [330, L.y], [420, L.y]], 'ok', { r: 18 });
- b += ext(480, L.y, 'pin') + text(480, L.y + 70, L.n, { size: 17, weight: 500 }) + mono(480, L.y + 92, L.c, { size: 13 });
+ b += line(
+ [
+ [282, 560],
+ [330, 560],
+ [330, L.y],
+ [420, L.y],
+ ],
+ 'ok',
+ { r: 18 }
+ );
+ b +=
+ ext(480, L.y, 'pin') +
+ text(480, L.y + 70, L.n, { size: 17, weight: 500 }) +
+ mono(480, L.y + 92, L.c, { size: 13 });
}
- const rows = [[130, 'fd20:0:53::1:0:0/96'], [300, 'fd20:0:53::2:0:0/96'], [560, 'fd20:0:53:2:0:1::/96'], [905, 'fd20:0:53:1:0:1::/96']];
- const src = [[215, 130], [215, 300], [560, 560], [905, 905]];
+ const rows = [
+ [130, 'fd20:0:53::1:0:0/96'],
+ [300, 'fd20:0:53::2:0:0/96'],
+ [560, 'fd20:0:53:2:0:1::/96'],
+ [905, 'fd20:0:53:1:0:1::/96'],
+ ];
+ const src = [
+ [215, 130],
+ [215, 300],
+ [560, 560],
+ [905, 905],
+ ];
src.forEach(([sy, ty], i) => {
- b += line([[545, sy], [680, sy], [680, ty], [746, ty]], 'ok', { r: 18 });
- b += tile(880, ty - 62, 250, 150) + rack(880, ty - 12) + mono(880, ty + 58, rows[i][1], { size: 12.5 });
+ b += line(
+ [
+ [545, sy],
+ [680, sy],
+ [680, ty],
+ [746, ty],
+ ],
+ 'ok',
+ { r: 18 }
+ );
+ b +=
+ tile(880, ty - 62, 250, 150) +
+ rack(880, ty - 12) +
+ mono(880, ty + 58, rows[i][1], { size: 12.5 });
});
return svg(W, H, b);
};
// 4 ─ IPv4-only client through the ALB to an IPv6-only app
export const image4 = () => {
- const W = 820, H = 1580;
+ const W = 820,
+ H = 1580;
let b = '';
b += deco(4, 600, 0, 170) + deco(6, 10, 1380, 120);
b += ext(410, 80, 'laptop') + text(410, 154, 'IPv4-only client', { size: 17, weight: 500 });
- b += line([[410, 174], [410, 322]], 'neutral') + tag(410, 250, 'HTTPS · anycast · A + AAAA');
+ b +=
+ line(
+ [
+ [410, 174],
+ [410, 322],
+ ],
+ 'neutral'
+ ) + tag(410, 250, 'HTTPS · anycast · A + AAAA');
b += tile(410, 330, 280, 215);
- b += datum(410, 400, 'hub', { r: 52 }) + text(410, 488, 'Datum ALB', { size: 17, weight: 500 }) + text(410, 510, 'Envoy at the nearest edge', { size: 14, fill: C.muted });
- b += line([[410, 553], [410, 740]], 'ok');
- b += label(410, 630, ['new connection from', 'fd42:…'], { size: 14 }) + tag(410, 690, 'X-Forwarded-For: client IPv4');
+ b +=
+ datum(410, 400, 'hub', { r: 52 }) +
+ text(410, 488, 'Datum ALB', { size: 17, weight: 500 }) +
+ text(410, 510, 'Envoy at the nearest edge', { size: 14, fill: C.muted });
+ b += line(
+ [
+ [410, 553],
+ [410, 740],
+ ],
+ 'ok'
+ );
+ b +=
+ label(410, 630, ['new connection from', 'fd42:…'], { size: 14 }) +
+ tag(410, 690, 'X-Forwarded-For: client IPv4');
b += datumFrame(190, 750, 440, 230, 'IPv6 inside');
- b += rack(410, 855) + text(410, 925, 'your app', { size: 16, weight: 500 }) + mono(410, 946, 'fd20:0:53::1:0:0 · IPv6 only', { size: 12.5 });
+ b +=
+ rack(410, 855) +
+ text(410, 925, 'your app', { size: 16, weight: 500 }) +
+ mono(410, 946, 'fd20:0:53::1:0:0 · IPv6 only', { size: 12.5 });
// left: DNS64 + NAT64
- b += line([[300, 980], [300, 1010], [240, 1010], [240, 1132]], 'neutral', { r: 14 });
+ b += line(
+ [
+ [300, 980],
+ [300, 1010],
+ [240, 1010],
+ [240, 1132],
+ ],
+ 'neutral',
+ { r: 14 }
+ );
b += label(240, 1075, ['IPv4-only name', '→ 64:ff9b::… address'], { size: 13.5 });
b += tile(240, 1140, 280, 195);
- b += datum(240, 1210, 'swap', { r: 46 }) + text(240, 1290, 'DNS64 + NAT64', { size: 16, weight: 500 }) + text(240, 1312, 'per-location egress', { size: 14, fill: C.muted });
- b += line([[240, 1343], [240, 1400], [350, 1400], [350, 1440]], 'neutral', { r: 14 });
+ b +=
+ datum(240, 1210, 'swap', { r: 46 }) +
+ text(240, 1290, 'DNS64 + NAT64', { size: 16, weight: 500 }) +
+ text(240, 1312, 'per-location egress', { size: 14, fill: C.muted });
+ b += line(
+ [
+ [240, 1343],
+ [240, 1400],
+ [350, 1400],
+ [350, 1440],
+ ],
+ 'neutral',
+ { r: 14 }
+ );
b += label(300, 1368, ['leaves from a shared', 'IPv4 per location'], { size: 13.5 });
// right: literal address fails
- b += line([[520, 980], [520, 1010], [610, 1010], [610, 1200]], 'bad', { r: 14 });
- b += line([[610, 1262], [610, 1400], [470, 1400], [470, 1440]], 'bad', { r: 14 });
+ b += line(
+ [
+ [520, 980],
+ [520, 1010],
+ [610, 1010],
+ [610, 1200],
+ ],
+ 'bad',
+ { r: 14 }
+ );
+ b += line(
+ [
+ [610, 1262],
+ [610, 1400],
+ [470, 1400],
+ [470, 1440],
+ ],
+ 'bad',
+ { r: 14 }
+ );
b += tag(610, 1235, 'a literal like 1.1.1.1 ×', { fill: C.clay });
b += label(610, 1320, ['nothing to', 'translate'], { size: 13.5, fill: C.clayDeep });
- b += ext(410, 1500, 'globe', { r: 40 }) + text(410, 1556, 'IPv4-only internet', { size: 16, weight: 500 });
+ b +=
+ ext(410, 1500, 'globe', { r: 40 }) +
+ text(410, 1556, 'IPv4-only internet', { size: 16, weight: 500 });
return svg(W, H, b);
};
// 5 ─ configs from other platforms
export const image5 = () => {
- const W = 1560, H = 720;
+ const W = 1560,
+ H = 720;
let b = '';
- b += ext(110, 360, 'case', { r: 54 }) + text(110, 442, 'configs from', { size: 16 }) + text(110, 464, 'other platforms', { size: 16 });
+ b +=
+ ext(110, 360, 'case', { r: 54 }) +
+ text(110, 442, 'configs from', { size: 16 }) +
+ text(110, 464, 'other platforms', { size: 16 });
const rows = [
['listen 8080; · 0.0.0.0', 'healthy-looking, 503'],
['8.8.8.8 · 1.1.1.1', 'no route to a literal'],
@@ -133,21 +415,48 @@ export const image5 = () => {
const ys = [90, 225, 360, 495, 630];
rows.forEach(([a, r], i) => {
const y = ys[i];
- b += line([[170, 360], [210, 360], [210, y], [270, y]], 'neutral', { r: 16, dot: false });
+ b += line(
+ [
+ [170, 360],
+ [210, 360],
+ [210, y],
+ [270, y],
+ ],
+ 'neutral',
+ { r: 16, dot: false }
+ );
b += `${text(445, y + 5.5, a, { size: 14, mono: true, fill: C.ink })}`;
- b += line([[610, y], [706, y]], 'bad');
+ b += line(
+ [
+ [610, y],
+ [706, y],
+ ],
+ 'bad'
+ );
b += `${text(866, y + 5.5, r, { size: 16, fill: C.clayDeep })}`;
- b += line([[1016, y], [1096, y]], 'ok');
+ b += line(
+ [
+ [1016, y],
+ [1096, y],
+ ],
+ 'ok'
+ );
});
b += datumFrame(1106, 40, 420, 640, '');
- b += text(1316, 330, ['grep · ss -tln', 'dig AAAA ipv4only.arpa'], { size: 17, mono: true, lh: 26, weight: 500 });
+ b += text(1316, 330, ['grep · ss -tln', 'dig AAAA ipv4only.arpa'], {
+ size: 17,
+ mono: true,
+ lh: 26,
+ weight: 500,
+ });
b += text(1316, 410, 'before you move', { size: 17, weight: 500, fill: C.pine });
return svg(W, H, b);
};
// 7 ─ four ways to connect regions
export const image7 = () => {
- const W = 1500, H = 1700;
+ const W = 1500,
+ H = 1700;
let b = '';
// Datum
b += extFrame(220, 30, 1060, 420, 'Datum: one network, every metro, no setup');
@@ -155,9 +464,38 @@ export const image7 = () => {
b += rack(750, 190) + text(750, 262, 'us-central-1', { size: 16, weight: 500 });
b += rack(430, 305) + text(430, 380, 'us-west-1', { size: 16, weight: 500 });
b += rack(1070, 305) + text(1070, 380, 'us-east-1', { size: 16, weight: 500 });
- b += line([[648, 190], [570, 190], [570, 300], [533, 300]], 'ok', { both: true, r: 14 });
- b += line([[852, 190], [930, 190], [930, 300], [967, 300]], 'ok', { both: true, r: 14 });
- b += line([[533, 372], [570, 372], [570, 424], [930, 424], [930, 372], [967, 372]], 'ok', { both: true, r: 14 });
+ b += line(
+ [
+ [648, 190],
+ [570, 190],
+ [570, 300],
+ [533, 300],
+ ],
+ 'ok',
+ { both: true, r: 14 }
+ );
+ b += line(
+ [
+ [852, 190],
+ [930, 190],
+ [930, 300],
+ [967, 300],
+ ],
+ 'ok',
+ { both: true, r: 14 }
+ );
+ b += line(
+ [
+ [533, 372],
+ [570, 372],
+ [570, 424],
+ [930, 424],
+ [930, 372],
+ [967, 372],
+ ],
+ 'ok',
+ { both: true, r: 14 }
+ );
b += tag(750, 424, '1 hop · MTU 1440', { fill: C.pine });
// Fly + Railway
b += extFrame(30, 500, 1440, 300, 'Fly.io and Railway: WireGuard mesh');
@@ -165,48 +503,93 @@ export const image7 = () => {
b += logo(1350, 665, 'railway') + text(1350, 740, 'Railway', { size: 16 });
b += `${text(420, 674, 'lax', { size: 17, fill: C.clayDeep, weight: 500 })}`;
b += `${text(1080, 674, 'iad', { size: 17, fill: C.clayDeep, weight: 500 })}`;
- b += line([[540, 667], [960, 667]], 'bad', { both: true });
+ b += line(
+ [
+ [540, 667],
+ [960, 667],
+ ],
+ 'bad',
+ { both: true }
+ );
b += tag(750, 667, '3 hops · MTU 1370 / 1316', { fill: C.clay });
// Google
b += extFrame(30, 850, 1440, 300, 'Google Cloud: global VPC, you plan it');
b += logo(150, 1015, 'google') + text(150, 1090, 'Google Cloud', { size: 16 });
b += `${text(440, 1008, 'us-west1', { size: 16, weight: 500 })}${mono(440, 1034, '10.10.1.0/24 ✍', { anchor: 'middle' })}`;
b += `${text(1060, 1008, 'us-east4', { size: 16, weight: 500 })}${mono(1060, 1034, '10.10.2.0/24 ✍', { anchor: 'middle' })}`;
- b += line([[580, 1015], [920, 1015]], 'neutral', { both: true });
+ b += line(
+ [
+ [580, 1015],
+ [920, 1015],
+ ],
+ 'neutral',
+ { both: true }
+ );
b += tag(750, 1015, 'firewall rules first');
// Vultr
b += extFrame(30, 1200, 1440, 300, 'Vultr: a VPC per region, IPv4-only');
b += logo(150, 1365, 'vultr') + text(150, 1440, 'Vultr', { size: 16 });
b += `${text(420, 1374, 'ewr VPC', { size: 16, fill: '#fff', weight: 500 })}`;
b += `${text(1080, 1374, 'lax VPC', { size: 16, fill: '#fff', weight: 500 })}`;
- b += line([[540, 1367], [960, 1367]], 'bad', { both: true });
+ b += line(
+ [
+ [540, 1367],
+ [960, 1367],
+ ],
+ 'bad',
+ { both: true }
+ );
b += tag(750, 1367, 'public internet · 14 hops', { fill: C.clay });
return svg(W, H - 140, b);
};
// 11 ─ SRv6 sequence
export const image11 = () => {
- const W = 1620, H = 1180;
+ const W = 1620,
+ H = 1180;
let b = '';
const cols = [
- ['west', 'instance', 150, 'dc'], ['west node', '(kernel + eBPF)', 440, 'dc'], ['next-door', 'instance', 730, 'dc'],
- ['backbone', '', 1020, 'ext'], ['east', 'node', 1240, 'dc'], ['east', 'instance', 1450, 'dc'],
+ ['west', 'instance', 150, 'dc'],
+ ['west node', '(kernel + eBPF)', 440, 'dc'],
+ ['next-door', 'instance', 730, 'dc'],
+ ['backbone', '', 1020, 'ext'],
+ ['east', 'node', 1240, 'dc'],
+ ['east', 'instance', 1450, 'dc'],
];
// bands
b += ``;
b += ``;
- for (const [, , x] of cols) b += ``;
+ for (const [, , x] of cols)
+ b += ``;
for (const [t1, t2, x, k] of cols) {
const dc = k === 'dc';
b += `${text(x, t2 ? 55 : 63, t1, { size: 15, weight: 500 })}${t2 ? text(x, 77, t2, { size: 14, fill: C.muted }) : ''}`;
}
- const note = (x, y, w, lines) => `${text(x, y + 28, lines, { size: 14.5, lh: 22, fill: C.ink })}`;
- const msg = (x1, x2, y, s, kind = 'neutral', t) => line([[x1, y], [x2, y]], kind) + tag((x1 + x2) / 2, y - 22, s, { fill: t ?? C.ink });
- b += label(190, 178, 'next door: same node', { size: 16, bg: C.clayPale, fill: C.clayDeep, weight: 500 });
+ const note = (x, y, w, lines) =>
+ `${text(x, y + 28, lines, { size: 14.5, lh: 22, fill: C.ink })}`;
+ const msg = (x1, x2, y, s, kind = 'neutral', t) =>
+ line(
+ [
+ [x1, y],
+ [x2, y],
+ ],
+ kind
+ ) + tag((x1 + x2) / 2, y - 22, s, { fill: t ?? C.ink });
+ b += label(190, 178, 'next door: same node', {
+ size: 16,
+ bg: C.clayPale,
+ fill: C.clayDeep,
+ weight: 500,
+ });
b += msg(150, 440, 230, 'packet · hop limit 64');
b += note(440, 262, 230, ['kernel routes it', 'inside the VRF']);
b += msg(440, 730, 390, 'packet · hop limit 63');
- b += label(250, 528, 'across the country: wrapped in SRv6', { size: 16, bg: C.mossPale, fill: C.pine, weight: 500 });
+ b += label(250, 528, 'across the country: wrapped in SRv6', {
+ size: 16,
+ bg: C.mossPale,
+ fill: C.pine,
+ weight: 500,
+ });
b += msg(150, 440, 590, 'packet · hop limit 64');
b += note(440, 622, 300, ['eBPF wraps it:', 'outer IPv6 header to', '“east node, blog-net”']);
b += msg(440, 1020, 780, 'outer | packet', 'ok', C.pine);
diff --git a/.claude/skills/datum-diagram/examples/five-articles.mjs b/.claude/skills/datum-diagram/examples/five-articles.mjs
new file mode 100644
index 00000000..9c3abc63
--- /dev/null
+++ b/.claude/skills/datum-diagram/examples/five-articles.mjs
@@ -0,0 +1,302 @@
+import {
+ C,
+ SW,
+ text,
+ tag,
+ label,
+ badge,
+ ext,
+ datum,
+ tile,
+ extFrame,
+ line,
+ deco,
+ svg,
+} from '../kit.mjs';
+
+const chip = (x, y, w, h, lines, kind = 'ext') => {
+ const L = Array.isArray(lines) ? lines : [lines];
+ const fill = kind === 'ok' ? C.mossPale : C.panel,
+ stroke = kind === 'ok' ? C.pine : C.line;
+ return `${text(x + w / 2, y + h / 2 + 5 - (L.length - 1) * 10, L, { size: 15, lh: 20, fill: kind === 'ok' ? C.navy : C.slate })}`;
+};
+const note = (x, y, s, o = {}) => text(x, y, s, { size: 14, fill: C.muted, ...o });
+
+// 1 ─ four flavors: who runs what
+export const fourFlavors = () => {
+ const W = 1240,
+ H = 640;
+ let b = deco(6, 1130, 520, 130);
+ b += extFrame(30, 30, 1180, 560, 'Four flavors: who runs what');
+ const colX = [200, 450, 700, 950],
+ cw = 235;
+ const cols = [
+ {
+ name: 'Public Cloud',
+ sub: 'for sale · multi-tenant',
+ cp: ['Datum', 1],
+ inf: ['Datum', 1, 'picks hardware,|locations and network paths'],
+ best: 'just works, on demand',
+ },
+ {
+ name: 'Dedicated Cloud',
+ sub: 'for sale · fully managed',
+ cp: ['Datum', 1],
+ inf: ['Datum-managed', 1, 'dedicated to you'],
+ best: 'control and isolation',
+ },
+ {
+ name: 'Bring Your Own Cloud',
+ sub: 'for sale',
+ cp: ['Datum', 1],
+ inf: ['You', 0, 'your data plane,|your infrastructure'],
+ best: 'sovereign or regulated',
+ },
+ {
+ name: 'Open Source',
+ sub: 'not for sale · AGPLv3',
+ cp: ['You', 0, 'run it yourself'],
+ inf: ['You', 0, 'run it yourself'],
+ best: 'use it, study it, fix it',
+ },
+ ];
+ b += text(60, 232, 'Control plane', { size: 15, weight: 500, anchor: 'start' });
+ b += text(60, 372, 'Infrastructure', { size: 15, weight: 500, anchor: 'start' });
+ b += text(60, 372 + 14, 'and data plane', { size: 15, weight: 500, anchor: 'start' });
+ b += text(60, 500, 'Good for', { size: 15, weight: 500, anchor: 'start' });
+ cols.forEach((c, i) => {
+ const x = colX[i];
+ b += text(x + cw / 2, 108, c.name, { size: 18, weight: 500 }) + note(x + cw / 2, 132, c.sub);
+ const cell = (y, h, [who, datumOwned, sub]) => {
+ let s = datumOwned ? tile(x + cw / 2, y, cw, h) : chip(x, y, cw, h, '', 'ext');
+ s += text(x + cw / 2, y + h / 2 - (sub ? 4 : -6), who, {
+ size: 18,
+ weight: 500,
+ fill: datumOwned ? C.navy : C.slate,
+ });
+ if (sub)
+ sub.split('|').forEach((l, k) => {
+ s += note(x + cw / 2, y + h / 2 + 18 + k * 17, l);
+ });
+ return s;
+ };
+ b += cell(185, 100, c.cp.length === 2 ? [...c.cp, null] : c.cp);
+ b += cell(325, 120, c.inf.length === 2 ? [...c.inf, null] : c.inf);
+ b += text(x + cw / 2, 504, c.best, { size: 15, fill: C.slate });
+ });
+ return svg(W, H, b);
+};
+
+// 2 ─ Galactic VPC: land anything here
+export const landAnything = () => {
+ const W = 1240,
+ H = 660;
+ let b = deco(4, 1090, -80, 170);
+ b += extFrame(30, 30, 1180, 600, 'Galactic VPC: land anything, on one virtual backbone');
+ const left = [
+ 'MASQUE tunnel',
+ 'WireGuard peer',
+ 'Plain old VPN',
+ 'Layer 2 VLAN',
+ 'Layer 3 BGP session',
+ 'NNI to a carrier',
+ ];
+ b += note(180, 100, 'however the connection arrives', { size: 14 });
+ left.forEach((t, i) => {
+ const y = 130 + i * 78;
+ b +=
+ chip(60, y, 240, 52, t) +
+ line(
+ [
+ [300, y + 26],
+ [468, y + 26],
+ ],
+ 'neutral'
+ );
+ });
+ b +=
+ tile(620, 118, 300, 460) +
+ datum(620, 280, 'hub', { r: 56 }) +
+ text(620, 385, 'Galactic VPC', { size: 20, weight: 500 }) +
+ note(620, 412, 'a virtual meet-me room') +
+ note(620, 432, 'and a virtual backbone') +
+ note(620, 466, 'built on SRv6', { size: 13 });
+ b += note(1060, 100, 'interchangeable providers underneath');
+ ['Bare metal, one provider', 'VMs, another provider', 'GPUs, a third provider'].forEach(
+ (t, i) => {
+ const y = 175 + i * 120;
+ b +=
+ line(
+ [
+ [776, y + 26],
+ [940, y + 26],
+ ],
+ 'ok'
+ ) + chip(948, y, 230, 52, t);
+ }
+ );
+ return svg(W, H, b);
+};
+
+// 3 ─ private by default
+export const privateByDefault = () => {
+ const W = 1240,
+ H = 460;
+ let b = deco(6, 1110, 330, 120);
+ b += extFrame(
+ 30,
+ 30,
+ 1180,
+ 400,
+ 'Private by default: nothing is addressable on the public internet'
+ );
+ const xs = [150, 450, 790, 1080];
+ b +=
+ ext(xs[0], 220, 'laptop') +
+ text(xs[0], 292, 'Docker container', { size: 16, weight: 500 }) +
+ note(xs[0], 314, 'or desktop application');
+ b +=
+ tile(xs[1], 130, 220, 200) +
+ datum(xs[1], 215, 'lock', { r: 42 }) +
+ text(xs[1], 292, 'Datum Tunnel', { size: 17, weight: 500 }) +
+ note(xs[1], 313, 'QUIC · iroh · routes by key');
+ b +=
+ tile(xs[2], 130, 220, 200) +
+ datum(xs[2], 215, 'hub', { r: 42 }) +
+ text(xs[2], 292, 'Galactic VPC', { size: 17, weight: 500 }) +
+ note(xs[2], 313, 'private backbone');
+ b += ext(xs[3], 220, 'cloud') + text(xs[3], 292, 'AWS', { size: 16, weight: 500 });
+ b +=
+ line(
+ [
+ [210, 220],
+ [336, 220],
+ ],
+ 'neutral'
+ ) +
+ line(
+ [
+ [564, 220],
+ [676, 220],
+ ],
+ 'ok'
+ ) +
+ line(
+ [
+ [904, 220],
+ [1022, 220],
+ ],
+ 'ok'
+ );
+ b +=
+ tag(273, 196, 'lands', { fill: C.ink }) +
+ tag(620, 196, 'lands', { fill: C.pine }) +
+ tag(963, 196, 'routes out', { fill: C.pine });
+ b += `${text(620, 388, 'no exposed ports · no overlay to babysit', { size: 15, weight: 500 })}`;
+ return svg(W, H, b);
+};
+
+// 4 ─ every agent needs an edge, tunnels and a backbone
+export const agentThreePieces = () => {
+ const W = 1240,
+ H = 700;
+ let b = deco(3, 20, 560, 100);
+ b += extFrame(30, 30, 1180, 640, 'Every agent gets its own private network');
+ b +=
+ tile(160, 250, 230, 220) +
+ datum(160, 330, 'server', { r: 44 }) +
+ text(160, 415, 'Your agent', { size: 18, weight: 500 }) +
+ note(160, 438, 'and its workloads');
+ const rows = [
+ {
+ y: 105,
+ n: 1,
+ t: 'Edge',
+ d: ['Envoy proxy meets the internet at scale', 'protection from bots, threats and DDoS'],
+ to: 'the public internet',
+ },
+ {
+ y: 285,
+ n: 2,
+ t: 'Tunnels',
+ d: [
+ 'peer-to-peer QUIC connections (Iroh)',
+ 'land application traffic exactly where you want it',
+ ],
+ to: 'your apps and endpoints',
+ },
+ {
+ y: 465,
+ n: 3,
+ t: 'Backbone',
+ d: [
+ 'deterministic, auditable “fast lanes”',
+ 'SRv6 overlay with VPC-style control and observability',
+ ],
+ to: 'clouds, partners, providers',
+ },
+ ];
+ rows.forEach((r) => {
+ b +=
+ tile(620, r.y, 470, 140) +
+ badge(410, r.y + 36, r.n) +
+ text(440, r.y + 42, r.t, { size: 20, weight: 500, anchor: 'start' }) +
+ note(440, r.y + 80, r.d[0], { anchor: 'start' }) +
+ note(440, r.y + 102, r.d[1], { anchor: 'start' });
+ b += line(
+ [
+ [276, 360],
+ [340, 360],
+ [340, r.y + 70],
+ [380, r.y + 70],
+ ],
+ 'ok',
+ { r: 14 }
+ );
+ b += line(
+ [
+ [860, r.y + 70],
+ [940, r.y + 70],
+ ],
+ 'neutral'
+ );
+ b += chip(948, r.y + 40, 220, 60, r.to);
+ });
+ return svg(W, H, b);
+};
+
+// 5 ─ chicken and egg
+export const chickenAndEgg = () => {
+ const W = 1140,
+ H = 560;
+ let b = deco(12, 1060, 40, 46);
+ b += extFrame(30, 30, 1080, 500, 'Compute and Galactic VPC need each other');
+ b +=
+ tile(230, 140, 300, 190) +
+ datum(230, 210, 'server', { r: 40 }) +
+ text(230, 282, 'Datum Compute', { size: 19, weight: 500 }) +
+ note(230, 304, 'microVMs that start in milliseconds');
+ b +=
+ tile(910, 140, 300, 190) +
+ datum(910, 210, 'hub', { r: 40 }) +
+ text(910, 282, 'Galactic VPC', { size: 19, weight: 500 }) +
+ note(910, 304, 'private backbone built on SRv6');
+ b +=
+ line(
+ [
+ [392, 205],
+ [748, 205],
+ ],
+ 'ok'
+ ) + tag(570, 205, 'every workload comes up attached', { fill: C.pine });
+ b +=
+ line(
+ [
+ [748, 268],
+ [392, 268],
+ ],
+ 'neutral'
+ ) + tag(570, 268, 'real workloads show its value', { fill: C.ink });
+ b += `${text(570, 410, 'The payoff is the one the big clouds get', { size: 15, weight: 500 })}${text(570, 438, 'lower egress costs · reduced latency · a smaller security blast radius', { size: 15, fill: C.slate })}`;
+ return svg(W, H, b);
+};
diff --git a/.claude/skills/datum-diagram/examples/mermaid/agentThreePieces.mmd b/.claude/skills/datum-diagram/examples/mermaid/agentThreePieces.mmd
new file mode 100644
index 00000000..94530853
--- /dev/null
+++ b/.claude/skills/datum-diagram/examples/mermaid/agentThreePieces.mmd
@@ -0,0 +1,11 @@
+flowchart TB
+ agent["Your agent
and its workloads"]:::datum
+ edge["1 · Edge
Envoy proxy
bot, threat and
DDoS protection"]:::datum
+ tun["2 · Tunnels
peer-to-peer QUIC
(Iroh)"]:::datum
+ bb["3 · Backbone
SRv6 fast lanes
VPC-style control"]:::datum
+ net["public internet"]:::ext
+ apps["your apps
and endpoints"]:::ext
+ prov["clouds, partners,
providers"]:::ext
+ agent -.-> edge -.-> net
+ agent -.-> tun -.-> apps
+ agent -.-> bb -.-> prov
diff --git a/.claude/skills/datum-diagram/examples/mermaid/chickenAndEgg.mmd b/.claude/skills/datum-diagram/examples/mermaid/chickenAndEgg.mmd
new file mode 100644
index 00000000..566900b0
--- /dev/null
+++ b/.claude/skills/datum-diagram/examples/mermaid/chickenAndEgg.mmd
@@ -0,0 +1,7 @@
+flowchart TB
+ compute["Datum Compute
microVMs that start in milliseconds"]:::datum
+ gvpc["Galactic VPC
private backbone built on SRv6"]:::datum
+ pay["lower egress costs · reduced latency
a smaller security blast radius"]:::ok
+ compute -.->|"every workload comes up attached"| gvpc
+ gvpc -.->|"real workloads show its value"| compute
+ gvpc -.-> pay
diff --git a/.claude/skills/datum-diagram/examples/mermaid/fourFlavors.mmd b/.claude/skills/datum-diagram/examples/mermaid/fourFlavors.mmd
new file mode 100644
index 00000000..2400cd7c
--- /dev/null
+++ b/.claude/skills/datum-diagram/examples/mermaid/fourFlavors.mmd
@@ -0,0 +1,6 @@
+flowchart TB
+ pub["Public Cloud · for sale
Datum runs it all: infrastructure,
hardware, locations, network paths"]:::datum
+ ded["Dedicated Cloud · for sale
fully managed by Datum,
dedicated to you"]:::datum
+ byoc["Bring Your Own Cloud · for sale
Datum runs the control plane,
you bring the infrastructure"]:::datum
+ oss["Open Source · AGPLv3 · not for sale
you run it yourself"]:::ext
+ pub ~~~ ded ~~~ byoc ~~~ oss
diff --git a/.claude/skills/datum-diagram/examples/mermaid/landAnything.mmd b/.claude/skills/datum-diagram/examples/mermaid/landAnything.mmd
new file mode 100644
index 00000000..c67f1215
--- /dev/null
+++ b/.claude/skills/datum-diagram/examples/mermaid/landAnything.mmd
@@ -0,0 +1,24 @@
+flowchart LR
+ subgraph arrive["however the connection arrives"]
+ a1["MASQUE tunnel"]:::ext
+ a2["WireGuard peer"]:::ext
+ a3["plain old VPN"]:::ext
+ a4["Layer 2 VLAN"]:::ext
+ a5["Layer 3 BGP session"]:::ext
+ a6["NNI to a carrier"]:::ext
+ end
+ gvpc["Galactic VPC
a virtual meet-me room
and a virtual backbone
built on SRv6"]:::datum
+ subgraph prov["interchangeable providers"]
+ p1["bare metal,
one provider"]:::ext
+ p2["VMs,
another provider"]:::ext
+ p3["GPUs,
a third provider"]:::ext
+ end
+ a1 -.-> gvpc
+ a2 -.-> gvpc
+ a3 -.-> gvpc
+ a4 -.-> gvpc
+ a5 -.-> gvpc
+ a6 -.-> gvpc
+ gvpc -.-> p1
+ gvpc -.-> p2
+ gvpc -.-> p3
diff --git a/.claude/skills/datum-diagram/examples/mermaid/pathsDirectRelay.mmd b/.claude/skills/datum-diagram/examples/mermaid/pathsDirectRelay.mmd
new file mode 100644
index 00000000..1b787d1b
--- /dev/null
+++ b/.claude/skills/datum-diagram/examples/mermaid/pathsDirectRelay.mmd
@@ -0,0 +1,10 @@
+flowchart LR
+ subgraph both["both paths tried in parallel · fastest wins, automatic failover"]
+ direction LR
+ gw["Gateway endpoint"]:::datum
+ relay["Home relay"]:::datum
+ dn["Desktop node"]:::datum
+ gw <-.->|"encrypted"| relay
+ relay <-.->|"encrypted"| dn
+ gw <-.->|"direct UDP
hole-punched"| dn
+ end
diff --git a/.claude/skills/datum-diagram/examples/mermaid/privateByDefault.mmd b/.claude/skills/datum-diagram/examples/mermaid/privateByDefault.mmd
new file mode 100644
index 00000000..452446e4
--- /dev/null
+++ b/.claude/skills/datum-diagram/examples/mermaid/privateByDefault.mmd
@@ -0,0 +1,11 @@
+flowchart TB
+ c["Docker container
or desktop application"]:::ext
+ subgraph priv["no exposed ports · no overlay to babysit"]
+ direction TB
+ t["Datum Tunnel
QUIC · iroh · routes by key"]:::datum
+ g["Galactic VPC
private backbone"]:::datum
+ end
+ a["AWS"]:::ext
+ c -.->|"lands"| t
+ t -.->|"lands"| g
+ g -.->|"routes out"| a
diff --git a/.claude/skills/datum-diagram/examples/mermaid/requestFlow.mmd b/.claude/skills/datum-diagram/examples/mermaid/requestFlow.mmd
new file mode 100644
index 00000000..4d4b8f3f
--- /dev/null
+++ b/.claude/skills/datum-diagram/examples/mermaid/requestFlow.mmd
@@ -0,0 +1,10 @@
+sequenceDiagram
+ participant E as Envoy
+ participant G as iroh-gateway
+ participant D as Desktop node
+ E->>G: 1 · HTTP request
(via Unix socket)
+ G->>D: 2 · resolve endpoint ID
from request metadata
+ G->>D: 3 · open QUIC stream
(reuse connection or establish new one)
+ G->>D: 4 · forward request
over QUIC stream
+ D-->>G: 5 · HTTP response
+ G-->>E: 6 · HTTP response