Compare commits

..
13 Commits
Author SHA1 Message Date
root-at-skic 2ab4287f02 Merge pull request '0.11.0: arc42 and ArchiMate examples; ref in every diagram kind' (#42) from feat/arc42-archimate-examples into main 2026-11-08 12:54:42 -08:00
root-at-skic 0745de822a 0.11.0: arc42 and ArchiMate examples; ref in every diagram kind
Six example models next to the three C4 ones: arc42 (Cinema Tickets, IoT
Telemetry, Team Wiki) by section, ArchiMate (Insurance Claims, HR
Onboarding, Logistics Tracking) by layer, each validated with 0 errors and
0 warnings. resolveRefs and the editor schemas now take ref: in workflow
and dataflow nodes, sequence participants and lifecycle states, as the
skill already did. Example tests check each framework's folders.
2026-11-08 22:54:23 +02:00
root-at-skic f4df6a0834 Merge pull request '0.10.10: Buy me a coffee button at the top of the README' (#41) from docs/coffee-top into main 2026-11-08 12:09:19 -08:00
root-at-skic 4b36136432 0.10.10: Buy me a coffee button at the top of the README 2026-11-08 22:09:03 +02:00
root-at-skic 7dde9ffffe Merge pull request '0.10.9: square Sync button on model, registry, decision and Graph pages' (#40) from fix/sync-button-width into main 2026-11-08 11:31:37 -08:00
root-at-skic f6f59fae67 0.10.9: square Sync button on model, registry, decision and Graph pages
aspect-ratio with a stretched height left the width at the icon's size.
Fixed sizes instead: 33px beside the model switch, 29px in the Graph
header.
2026-11-08 21:31:22 +02:00
root-at-skic 0752128789 Merge pull request '0.10.8: main.archify opens with its top-level panels collapsed' (#39) from feat/model-panels-collapsed into main 2026-11-08 11:06:10 -08:00
root-at-skic 517697197e Sync button sits in the view switch's row on every page
Model, registry and decision pages: next to the switch, not floating above
it. Graph page: after Export PNG (its header is built by graph.js a moment
later, so the button waits for it) instead of covering Export. Sized to
the controls beside it.
2026-11-08 20:59:16 +02:00
root-at-skic c71296b01a 0.10.8: main.archify opens with its top-level panels collapsed
Shared, Diagrams, Registries and Decisions start closed on the model page;
folders inside keep their own default. Open or closed is still kept in
the webview state while the page is open.
2026-11-08 20:48:35 +02:00
root-at-skic 86a8adf03b Merge pull request '0.10.7: Model | Graph | Source styled like 2D | 3D' (#38) from fix/mode-switch-style into main 2026-11-08 10:37:07 -08:00
root-at-skic 6153c7f145 0.10.7: Model | Graph | Source styled like 2D | 3D
Archify gives every toolbar button a 2.75rem minimum height and a painted
::before; the switch's segments are buttons, so they grew to full height.
Segments now keep 1.75rem inside a 2.75rem switch, like 2D | 3D. The high
contrast rule no longer turns a shared model's Sync button blue.
2026-11-08 20:35:48 +02:00
root-at-skic 885977c4aa Merge pull request '0.10.6: Sync sign-in comes back through 127.0.0.1' (#37) from fix/sync-signin-loopback into main 2026-11-08 10:06:32 -08:00
root-at-skic 95c6e60199 0.10.6: Sync sign-in comes back through 127.0.0.1
Browsers open a vscode:// link only after a click on the page. With a live
identity provider session the sign-in ran without one, so the editor kept
waiting. On the desktop the browser now returns to a one-off listener on
127.0.0.1; remote and web editors keep the URI handler.
2026-11-08 20:05:11 +02:00
87 changed files with 3034 additions and 85 deletions

No files matched your search

+27
View File
@@ -1,5 +1,32 @@
# Changelog # Changelog
## 0.11.0 — 2026-11-08
- **arc42 and ArchiMate examples.** *Archify | New Model…* now offers nine examples, three per framework. **arc42** (Cinema Tickets, IoT Telemetry, Team Wiki): the overview is the business context with quality goals, constraints, quality scenarios, risks and a glossary as cards; then `0300-context`, `0500-building-blocks` (levels 1 and 2), `0600-runtime` (sequence diagrams), `0700-deployment`, and the decisions as section 9. **ArchiMate** (Insurance Claims, HR Onboarding, Logistics Tracking): the overview is a layered view of the business, application and technology layers; then `0100-business` (with the business process as a workflow), `0200-application` and `0300-technology`, one registry file per layer, each element's ArchiMate type under its name.
- **`ref:` in every diagram kind.** Workflow and dataflow nodes, sequence participants and lifecycle states can use registry elements, as architecture components do; the Source view's schema check accepts them too. Sequence participants take no `tag`.
## 0.10.10 — 2026-11-08
- README: the Buy me a coffee button at the top.
## 0.10.9 — 2026-11-08
- The Sync button is square again on the model, registry, decision and Graph pages (0.10.8 squeezed it narrow there).
## 0.10.8 — 2026-11-08
- **A model opens as its outline.** On `main.archify` the top-level panels (Shared, Diagrams, Registries, Decisions) start collapsed; open the one you need. What you open or close is kept while the page is open.
- **Sync button in line on every page.** On the model, registry and decision pages it sits next to Model | Graph | Source instead of floating above it, and on the Graph page it comes after Export PNG instead of covering it. It takes the height of the controls beside it.
## 0.10.7 — 2026-11-08
- **Model | Graph | Source looks like 2D | 3D.** The selected view is the same inset pill, the same height as the other toolbar controls.
- The Sync button of a shared model is green in high contrast themes too.
## 0.10.6 — 2026-11-08
- **Sync sign-in comes back by itself.** The browser now returns the sign-in to a one-off port on this machine (`127.0.0.1`) instead of a `vscode://` link. Browsers open such links only after a click, so when you were already signed in to the identity provider the editor kept waiting. Remote and web editors still use the link.
## 0.10.5 — 2026-11-08 ## 0.10.5 — 2026-11-08
- **Sync button fixed.** It is now an icon only (a cloud; green with a broadcast icon once the model is shared), the same size on every page and theme. On a diagram it sits in the toolbar after **Export** instead of over it; in narrow windows and high contrast themes it no longer shrinks or loses its colour. - **Sync button fixed.** It is now an icon only (a cloud; green with a broadcast icon once the model is shared), the same size on every page and theme. On a diagram it sits in the toolbar after **Export** instead of over it; in narrow windows and high contrast themes it no longer shrinks or loses its colour.
+5 -5
View File
@@ -1,13 +1,13 @@
# Archify Diagram Viewer # Archify Diagram Viewer
[![Buy me a coffee](https://cdn.buymeacoffee.com/buttons/v2/default-yellow.png)](https://buymeacoffee.com/root.at.skic)
Create and view [Archify](https://github.com/tt-a1i/archify) diagrams inside VS Code. You write a diagram as a **`.archify` file in YAML**; clicking it opens the interactive diagram, and the diagram follows your edits as you type. Create and view [Archify](https://github.com/tt-a1i/archify) diagrams inside VS Code. You write a diagram as a **`.archify` file in YAML**; clicking it opens the interactive diagram, and the diagram follows your edits as you type.
![An Archify workflow diagram as shown by the preview](docs/preview.png) ![An Archify workflow diagram as shown by the preview](docs/preview.png)
This is an unofficial extension. It bundles the Archify renderer (MIT, © tt-a1i) and runs it on VS Code's own Node runtime, so nothing else needs to be installed. This is an unofficial extension. It bundles the Archify renderer (MIT, © tt-a1i) and runs it on VS Code's own Node runtime, so nothing else needs to be installed.
Free and open source. If it saves you time, you can [buy me a coffee](https://buymeacoffee.com/root.at.skic) ☕
## Supported files ## Supported files
| File | Format | What the extension does | | File | Format | What the extension does |
@@ -22,7 +22,7 @@ New diagrams should be `.archify` files; everything below describes them unless
The fastest start: right-click a folder in the Explorer and choose **Archify | New Diagram…** (also in *File → New File…* and the Command Palette). Pick a kind of diagram (Architecture, Workflow, Sequence, Data Flow, Lifecycle or Everyday Plan), give it a name, and a small working `.archify` file is created and opened. Change its YAML to fit your case. The fastest start: right-click a folder in the Explorer and choose **Archify | New Diagram…** (also in *File → New File…* and the Command Palette). Pick a kind of diagram (Architecture, Workflow, Sequence, Data Flow, Lifecycle or Everyday Plan), give it a name, and a small working `.archify` file is created and opened. Change its YAML to fit your case.
For a whole set of diagrams, choose **Archify | New Model…** instead and pick one of the examples (C4 - Standard Web App, C4 - E-Shop, C4 - Event-Based Integration). A model folder with context, container and component diagrams and a shared registry is created and its model page opens. See [Models](#models). For a whole set of diagrams, choose **Archify | New Model…** instead and pick one of the examples: three in C4, three in arc42 and three in ArchiMate. A model folder with its diagrams, a shared registry and decisions is created and its model page opens. See [Models](#models).
To write one by hand: To write one by hand:
@@ -64,7 +64,7 @@ The Archify CLI and the AI agents that use it write diagrams as JSON, named `nam
## Features ## Features
- **New diagram from a template**: *Archify | New Diagram…* creates a starter `.archify` file for any of the five diagram kinds, or an everyday plan, in the chosen folder. - **New diagram from a template**: *Archify | New Diagram…* creates a starter `.archify` file for any of the five diagram kinds, or an everyday plan, in the chosen folder.
- **New model from an example**: *Archify | New Model…* creates a complete model folder from a C4 example (Standard Web App, E-Shop, Event-Based Integration). - **New model from an example**: *Archify | New Model…* creates a complete model folder from an example: C4, arc42 or ArchiMate, three of each.
- **Model graph**: the **Graph** switch on any page of a model shows the whole model as one graph, with a query box (`type:database`, `uses:api`, `from:web to:db`, …), a side panel per element and PNG export. See [Models](#models). - **Model graph**: the **Graph** switch on any page of a model shows the whole model as one graph, with a query box (`type:database`, `uses:api`, `from:web to:db`, …), a side panel per element and PNG export. See [Models](#models).
- **Diagram editor**: `.archify` files open as the rendered diagram. **Source** shows the YAML in its place, in an editor inside the page: problems underlined as you type and counted on the switch, completion and hover from the schema, undo, and ⌘S/Ctrl+S to save. **Open in Text Editor** opens the YAML beside the diagram instead, and edits re-render live. - **Diagram editor**: `.archify` files open as the rendered diagram. **Source** shows the YAML in its place, in an editor inside the page: problems underlined as you type and counted on the switch, completion and hover from the schema, undo, and ⌘S/Ctrl+S to save. **Open in Text Editor** opens the YAML beside the diagram instead, and edits re-render live.
- **Live updates**: the diagram re-renders as you edit, and keeps the focus and route you selected (`#focus=…`, `#route=…`). - **Live updates**: the diagram re-renders as you edit, and keeps the focus and route you selected (`#focus=…`, `#route=…`).
@@ -126,7 +126,7 @@ online-shop/
- **Decisions.** One `*.archify-decision` file per architecture decision: id, title, status, date, the registry elements and diagrams it is about, and prose for context, decision and consequences. Diagrams point at decisions with `decisions: [<id>]` on the diagram, a component or a connection. They show as a **Decisions** panel on the `main.archify` page, a **Decisions** rail panel under each diagram (hover lights up what the decision is attached to), on element cards and in the element dialog, and a decision opens as its own page. *Archify | New Decision…* creates one; links are checked in Problems. - **Decisions.** One `*.archify-decision` file per architecture decision: id, title, status, date, the registry elements and diagrams it is about, and prose for context, decision and consequences. Diagrams point at decisions with `decisions: [<id>]` on the diagram, a component or a connection. They show as a **Decisions** panel on the `main.archify` page, a **Decisions** rail panel under each diagram (hover lights up what the decision is attached to), on element cards and in the element dialog, and a decision opens as its own page. *Archify | New Decision…* creates one; links are checked in Problems.
- **Registry view.** A `.archify-registry` file opens as its elements, as cards. Click one and a dialog shows its description, where it is defined and the diagrams that use it; click a diagram to open it. **View | Source** at the top right switches to the YAML, checked against the registry schema as you type. - **Registry view.** A `.archify-registry` file opens as its elements, as cards. Click one and a dialog shows its description, where it is defined and the diagrams that use it; click a diagram to open it. **View | Source** at the top right switches to the YAML, checked against the registry schema as you type.
- **Elements and Connectors.** Under every diagram, Archify's node index is titled **Elements**, and a **Connectors** panel next to it lists every connection as "from → to" with its label. Hovering highlights the connector in the diagram; clicking keeps it highlighted and scrolls to it. - **Elements and Connectors.** Under every diagram, Archify's node index is titled **Elements**, and a **Connectors** panel next to it lists every connection as "from → to" with its label. Hovering highlights the connector in the diagram; clicking keeps it highlighted and scrolls to it.
- **Starting one.** *Archify | New Model…* (right-click a folder) copies one of the shipped examples into a new folder, named as you choose: **C4 - Standard Web App** (single-page app, API, database, cache, storage, identity provider), **C4 - E-Shop** (web shop, API, worker, queue, payment and email providers) or **C4 - Event-Based Integration** (event bus, router, dead-letter queue, event store, consumers). Each has a registry and context, container and component diagrams. For an empty model, *Archify | New Diagram… → Model* creates just a `main.archify`. - **Starting one.** *Archify | New Model…* (right-click a folder) copies one of the shipped examples into a new folder, named as you choose: **C4 - Standard Web App** (single-page app, API, database, cache, storage, identity provider), **C4 - E-Shop** (web shop, API, worker, queue, payment and email providers) or **C4 - Event-Based Integration** (event bus, router, dead-letter queue, event store, consumers), each with context, container and component diagrams. **arc42 - Cinema Tickets**, **arc42 - IoT Telemetry** and **arc42 - Team Wiki** follow arc42's sections: the overview is the business context with goals, constraints, quality scenarios, risks and a glossary as cards, then `0300-context`, `0500-building-blocks`, `0600-runtime` (sequence diagrams) and `0700-deployment`, with section 9 as decisions. **ArchiMate - Insurance Claims**, **ArchiMate - HR Onboarding** and **ArchiMate - Logistics Tracking** are layered: the overview is a layered view, then `0100-business`, `0200-application` and `0300-technology`, one registry file per layer, each element's ArchiMate type as its tag. Every example has a registry and decisions. For an empty model, *Archify | New Diagram… → Model* creates just a `main.archify`.
- **The graph.** The **Model | Graph | Source** switch at the top of the `main.archify` page (next to **Export** when the file has a diagram) replaces the page with the whole model as one graph: every element any diagram draws plus every registry element, every connection between them, each once. Registry elements have a solid border, inline ones a dashed border; colours are Archify's type colours and the legend chips at the bottom hide or show a type. **Decisions** are nodes too (tags coloured by status, dotted links to the elements they are about, dashed links to what they supersede), and so are **registries** (one node per file, dotted links to the elements it defines); their chips hide them when the picture gets busy. The layout is **Layered** by default, like Archify's stack: clients on the left, data on the right, registries in a row above, decisions in a row below, connections drawn orthogonally; click the button for a **Force** layout instead. Drag nodes, scroll to zoom, **Fit** (or <kbd>0</kbd>) frames what matches, **Labels** toggles connection labels, the dotted **Links** from decisions and registries to their elements are on by default and can be switched off, in which case only the selected element's show (hovering an element lights up its connections), the sun/moon button switches the page between its dark and light theme (remembered, shared with the diagram pages' theme choice), **Export PNG** saves the picture. On a diagram page the switch reads **Diagram | Graph | Source** and Graph opens the graph already narrowed to that diagram; **Source** on the graph page goes back to `main.archify` in its Source view. *Archify | Open Model Graph* does the same from the Command Palette or Explorer. - **The graph.** The **Model | Graph | Source** switch at the top of the `main.archify` page (next to **Export** when the file has a diagram) replaces the page with the whole model as one graph: every element any diagram draws plus every registry element, every connection between them, each once. Registry elements have a solid border, inline ones a dashed border; colours are Archify's type colours and the legend chips at the bottom hide or show a type. **Decisions** are nodes too (tags coloured by status, dotted links to the elements they are about, dashed links to what they supersede), and so are **registries** (one node per file, dotted links to the elements it defines); their chips hide them when the picture gets busy. The layout is **Layered** by default, like Archify's stack: clients on the left, data on the right, registries in a row above, decisions in a row below, connections drawn orthogonally; click the button for a **Force** layout instead. Drag nodes, scroll to zoom, **Fit** (or <kbd>0</kbd>) frames what matches, **Labels** toggles connection labels, the dotted **Links** from decisions and registries to their elements are on by default and can be switched off, in which case only the selected element's show (hovering an element lights up its connections), the sun/moon button switches the page between its dark and light theme (remembered, shared with the diagram pages' theme choice), **Export PNG** saves the picture. On a diagram page the switch reads **Diagram | Graph | Source** and Graph opens the graph already narrowed to that diagram; **Source** on the graph page goes back to `main.archify` in its Source view. *Archify | Open Model Graph* does the same from the Command Palette or Explorer.
- **Queries.** Type a few words in the box (<kbd>/</kbd> focuses it); matches stay bright, the rest dims, the count shows "9 of 14 elements". Terms combine with AND and a leading `-` excludes: `database` (id, label, sublabel, brand or type contain it), `type:backend`, `diagram:0200-*` (what those diagrams draw), `decision:<id>` (what a decision is about or is attached to), `uses:api` (an element and its neighbours), `near:api,2` (within two steps), `from:web to:db` (the shortest path, following the arrows first), `unused` (registry elements no diagram draws), `shared` and `-shared` (registry or inline elements only), `type:decision` and `type:registry` for the decision and registry nodes. Decisions and registries follow the elements they are attached to into a result; `-type:decision` or `-type:registry` leaves them out. Walks and paths follow the diagrams' connections only, so a registry is never a shortcut between its elements. The **?** button lists them; clicking an example runs it. - **Queries.** Type a few words in the box (<kbd>/</kbd> focuses it); matches stay bright, the rest dims, the count shows "9 of 14 elements". Terms combine with AND and a leading `-` excludes: `database` (id, label, sublabel, brand or type contain it), `type:backend`, `diagram:0200-*` (what those diagrams draw), `decision:<id>` (what a decision is about or is attached to), `uses:api` (an element and its neighbours), `near:api,2` (within two steps), `from:web to:db` (the shortest path, following the arrows first), `unused` (registry elements no diagram draws), `shared` and `-shared` (registry or inline elements only), `type:decision` and `type:registry` for the decision and registry nodes. Decisions and registries follow the elements they are attached to into a result; `-type:decision` or `-type:registry` leaves them out. Walks and paths follow the diagrams' connections only, so a registry is never a shortcut between its elements. The **?** button lists them; clicking an example runs it.
- **Side panel.** Click an element for its description, type, where it is defined (**Registry** opens the file at the line), the diagrams that draw it (click to open), its connections and its decisions (click to open). **Neighbours** runs `uses:` for it. Click a connection for its labels, the diagrams that draw it and its decisions. Click a decision node for its status, context and decision text, the elements and connections it is about, and **Open decision**; a registry node for the elements it defines, the ones no diagram draws yet, and **Open registry**. - **Side panel.** Click an element for its description, type, where it is defined (**Registry** opens the file at the line), the diagrams that draw it (click to open), its connections and its decisions (click to open). **Neighbours** runs `uses:` for it. Click a connection for its labels, the diagrams that draw it and its decisions. Click a decision node for its status, context and decision text, the elements and connections it is about, and **Open decision**; a registry node for the elements it defines, the ones no diagram draws yet, and **Open registry**.
+2 -2
View File
@@ -1,12 +1,12 @@
{ {
"name": "archify-vscode-ext", "name": "archify-vscode-ext",
"version": "0.10.5", "version": "0.11.0",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "archify-vscode-ext", "name": "archify-vscode-ext",
"version": "0.10.5", "version": "0.11.0",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@codemirror/autocomplete": "^6.20.4", "@codemirror/autocomplete": "^6.20.4",
+1 -1
View File
@@ -2,7 +2,7 @@
"name": "archify-vscode-ext", "name": "archify-vscode-ext",
"displayName": "Archify Diagram Viewer", "displayName": "Archify Diagram Viewer",
"description": "Archify diagrams in VS Code. Write diagrams as .archify files in YAML: they open as the interactive diagram, with live updates, validation, templates and completion. Also previews Archify's own JSON files (*.architecture.json, …) and opens rendered Archify HTML.", "description": "Archify diagrams in VS Code. Write diagrams as .archify files in YAML: they open as the interactive diagram, with live updates, validation, templates and completion. Also previews Archify's own JSON files (*.architecture.json, …) and opens rendered Archify HTML.",
"version": "0.10.5", "version": "0.11.0",
"publisher": "root-at-skic", "publisher": "root-at-skic",
"license": "MIT", "license": "MIT",
"sponsor": { "sponsor": {
+1 -1
View File
@@ -214,7 +214,7 @@
"ref": { "ref": {
"type": "string", "type": "string",
"pattern": "^[a-zA-Z][a-zA-Z0-9_-]*$", "pattern": "^[a-zA-Z][a-zA-Z0-9_-]*$",
"description": "A shared element from the model’s registry (*.archify-registry). Its type, label, sublabel, brand and sources come from there; the diagram adds pos/size and may override label, sublabel and tag." "description": "A shared element from the model’s registry (*.archify-registry). Its type, label, sublabel, brand and sources come from there; the diagram adds its own placement and may override label, sublabel and tag."
}, },
"diagram": { "diagram": {
"type": "string", "type": "string",
+28 -8
View File
@@ -129,13 +129,6 @@
"items": { "items": {
"type": "object", "type": "object",
"additionalProperties": false, "additionalProperties": false,
"required": [
"id",
"type",
"label",
"stage",
"row"
],
"properties": { "properties": {
"id": { "id": {
"$ref": "common.schema.json#/$defs/id" "$ref": "common.schema.json#/$defs/id"
@@ -188,8 +181,35 @@
"minLength": 1 "minLength": 1
}, },
"description": "Ids of architecture decisions (*.archify-decision files of the model) this is about. Shown in the Decisions panel under the diagram." "description": "Ids of architecture decisions (*.archify-decision files of the model) this is about. Shown in the Decisions panel under the diagram."
},
"ref": {
"type": "string",
"pattern": "^[a-zA-Z][a-zA-Z0-9_-]*$",
"description": "A shared element from the model’s registry (*.archify-registry). Its type, label, sublabel, brand and sources come from there; the diagram adds its own placement and may override label, sublabel and tag."
},
"diagram": {
"type": "string",
"description": "Diagram that explains this element, relative to the model root (main.archify). Double-click the node to open it."
} }
} },
"anyOf": [
{
"required": [
"ref",
"stage",
"row"
]
},
{
"required": [
"id",
"type",
"label",
"stage",
"row"
]
}
]
} }
}, },
"flows": { "flows": {
+28 -8
View File
@@ -145,13 +145,6 @@
"items": { "items": {
"type": "object", "type": "object",
"additionalProperties": false, "additionalProperties": false,
"required": [
"id",
"type",
"label",
"lane",
"col"
],
"properties": { "properties": {
"id": { "id": {
"$ref": "common.schema.json#/$defs/id" "$ref": "common.schema.json#/$defs/id"
@@ -216,8 +209,35 @@
"minLength": 1 "minLength": 1
}, },
"description": "Ids of architecture decisions (*.archify-decision files of the model) this is about. Shown in the Decisions panel under the diagram." "description": "Ids of architecture decisions (*.archify-decision files of the model) this is about. Shown in the Decisions panel under the diagram."
},
"ref": {
"type": "string",
"pattern": "^[a-zA-Z][a-zA-Z0-9_-]*$",
"description": "A shared element from the model’s registry (*.archify-registry). Its type, label, sublabel, brand and sources come from there; the diagram adds its own placement and may override label, sublabel and tag."
},
"diagram": {
"type": "string",
"description": "Diagram that explains this element, relative to the model root (main.archify). Double-click the node to open it."
} }
} },
"anyOf": [
{
"required": [
"ref",
"lane",
"col"
]
},
{
"required": [
"id",
"type",
"label",
"lane",
"col"
]
}
]
} }
}, },
"transitions": { "transitions": {
+24 -6
View File
@@ -117,11 +117,6 @@
"items": { "items": {
"type": "object", "type": "object",
"additionalProperties": false, "additionalProperties": false,
"required": [
"id",
"type",
"label"
],
"properties": { "properties": {
"id": { "id": {
"$ref": "common.schema.json#/$defs/id" "$ref": "common.schema.json#/$defs/id"
@@ -152,8 +147,31 @@
"minLength": 1 "minLength": 1
}, },
"description": "Ids of architecture decisions (*.archify-decision files of the model) this is about. Shown in the Decisions panel under the diagram." "description": "Ids of architecture decisions (*.archify-decision files of the model) this is about. Shown in the Decisions panel under the diagram."
},
"ref": {
"type": "string",
"pattern": "^[a-zA-Z][a-zA-Z0-9_-]*$",
"description": "A shared element from the model’s registry (*.archify-registry). Its type, label, sublabel, brand and sources come from there; the diagram adds its own placement and may override label, sublabel and tag."
},
"diagram": {
"type": "string",
"description": "Diagram that explains this element, relative to the model root (main.archify). Double-click the node to open it."
} }
} },
"anyOf": [
{
"required": [
"ref"
]
},
{
"required": [
"id",
"type",
"label"
]
}
]
} }
}, },
"segments": { "segments": {
+28 -8
View File
@@ -280,13 +280,6 @@
"items": { "items": {
"type": "object", "type": "object",
"additionalProperties": false, "additionalProperties": false,
"required": [
"id",
"lane",
"col",
"type",
"label"
],
"properties": { "properties": {
"id": { "id": {
"$ref": "common.schema.json#/$defs/id" "$ref": "common.schema.json#/$defs/id"
@@ -339,8 +332,35 @@
"minLength": 1 "minLength": 1
}, },
"description": "Ids of architecture decisions (*.archify-decision files of the model) this is about. Shown in the Decisions panel under the diagram." "description": "Ids of architecture decisions (*.archify-decision files of the model) this is about. Shown in the Decisions panel under the diagram."
},
"ref": {
"type": "string",
"pattern": "^[a-zA-Z][a-zA-Z0-9_-]*$",
"description": "A shared element from the model’s registry (*.archify-registry). Its type, label, sublabel, brand and sources come from there; the diagram adds its own placement and may override label, sublabel and tag."
},
"diagram": {
"type": "string",
"description": "Diagram that explains this element, relative to the model root (main.archify). Double-click the node to open it."
} }
} },
"anyOf": [
{
"required": [
"ref",
"lane",
"col"
]
},
{
"required": [
"id",
"lane",
"col",
"type",
"label"
]
}
]
} }
}, },
"edges": { "edges": {
+16 -8
View File
@@ -1,7 +1,7 @@
#!/usr/bin/env node #!/usr/bin/env node
// Adds the model keys to the editor schemas in schemas/ (the CLI keeps the // Adds the model keys to the editor schemas in schemas/ (the CLI keeps the
// strict originals): `model` on main.archify, `ref` and `diagram` on // strict originals): `model` on main.archify, `ref` and `diagram` on each
// architecture components, and a schema for *.archify-registry files. // kind's nodes, and a schema for *.archify-registry files.
// Run after sync-archify.mjs, or on its own after editing this file. // Run after sync-archify.mjs, or on its own after editing this file.
import fs from 'node:fs'; import fs from 'node:fs';
import path from 'node:path'; import path from 'node:path';
@@ -28,6 +28,7 @@ const DIAGRAM_LINK = { type: 'string', description: 'Diagram that explains this
const DECISIONS = { type: 'array', items: { type: 'string', minLength: 1 }, description: 'Ids of architecture decisions (*.archify-decision files of the model) this is about. Shown in the Decisions panel under the diagram.' }; const DECISIONS = { type: 'array', items: { type: 'string', minLength: 1 }, description: 'Ids of architecture decisions (*.archify-decision files of the model) this is about. Shown in the Decisions panel under the diagram.' };
const NODE_COLLECTIONS = ['components', 'nodes', 'participants', 'states']; const NODE_COLLECTIONS = ['components', 'nodes', 'participants', 'states'];
const EDGE_COLLECTIONS = ['connections', 'edges', 'flows', 'transitions', 'messages']; const EDGE_COLLECTIONS = ['connections', 'edges', 'flows', 'transitions', 'messages'];
const NODE_COLLECTION_OF = { architecture: 'components', workflow: 'nodes', dataflow: 'nodes', sequence: 'participants', lifecycle: 'states' };
for (const file of fs.readdirSync(schemas)) { for (const file of fs.readdirSync(schemas)) {
if (!/^(architecture|workflow|sequence|dataflow|lifecycle)\.schema\.json$/.test(file)) continue; if (!/^(architecture|workflow|sequence|dataflow|lifecycle)\.schema\.json$/.test(file)) continue;
@@ -38,13 +39,20 @@ for (const file of fs.readdirSync(schemas)) {
const items = schema.properties[key]?.items; const items = schema.properties[key]?.items;
if (items?.properties) items.properties.decisions = DECISIONS; if (items?.properties) items.properties.decisions = DECISIONS;
} }
if (file === 'architecture.schema.json') { // The list where each kind keeps its nodes takes `ref` (as the extension's resolver does).
const item = schema.properties.components.items; const nodes = NODE_COLLECTION_OF[file.replace('.schema.json', '')];
item.properties.ref = { type: 'string', pattern: '^[a-zA-Z][a-zA-Z0-9_-]*$', description: 'A shared element from the model’s registry (*.archify-registry). Its type, label, sublabel, brand and sources come from there; the diagram adds pos/size and may override label, sublabel and tag.' }; const item = schema.properties[nodes]?.items;
if (item?.properties) {
item.properties.ref = { type: 'string', pattern: '^[a-zA-Z][a-zA-Z0-9_-]*$', description: 'A shared element from the model’s registry (*.archify-registry). Its type, label, sublabel, brand and sources come from there; the diagram adds its own placement and may override label, sublabel and tag.' };
item.properties.diagram = DIAGRAM_LINK; item.properties.diagram = DIAGRAM_LINK;
// A ref brings type and label with it; an inline component must state them. // A ref brings type and label with it; an inline node must state them. Re-runs keep the original list.
item.anyOf = [{ required: ['ref'] }, { required: ['id', 'type', 'label'] }]; const required = item.required ?? item.anyOf?.[1]?.required;
delete item.required; if (required) {
// Placement (lane/col, stage/row) stays the diagram's own; only id, type and label may come from the ref.
const placement = required.filter((key) => !['id', 'type', 'label'].includes(key));
item.anyOf = [{ required: ['ref', ...placement] }, { required }];
delete item.required;
}
} }
write(file, schema); write(file, schema);
} }
+11 -1
View File
@@ -4,7 +4,8 @@ import { modelIdFromName, titleFromName } from './templates';
/** /**
* Example models shipped with the extension (templates/models/<folder>). * Example models shipped with the extension (templates/models/<folder>).
* "Archify | New Model…" offers them; each is a complete model folder: * "Archify | New Model…" offers them; each is a complete model folder:
* main.archify, registry/*.archify-registry and diagrams by level. * main.archify, registry/*.archify-registry, decisions and diagrams in the
* framework's folders: C4 levels, arc42 sections or ArchiMate layers.
*/ */
export interface ModelExample { export interface ModelExample {
/** Folder under templates/models and the default name of the new model folder. */ /** Folder under templates/models and the default name of the new model folder. */
@@ -17,8 +18,17 @@ export const MODEL_EXAMPLES: readonly ModelExample[] = [
{ folder: 'c4-standard-web-app', label: 'C4 - Standard Web App', description: 'Single-page app, API, database, cache, file storage, identity provider' }, { folder: 'c4-standard-web-app', label: 'C4 - Standard Web App', description: 'Single-page app, API, database, cache, file storage, identity provider' },
{ folder: 'c4-e-shop', label: 'C4 - E-Shop', description: 'Web shop, API, worker, queue, payment and email providers' }, { folder: 'c4-e-shop', label: 'C4 - E-Shop', description: 'Web shop, API, worker, queue, payment and email providers' },
{ folder: 'c4-event-based-integration', label: 'C4 - Event-Based Integration', description: 'Event bus, router, dead-letter queue, event store, consumers' }, { folder: 'c4-event-based-integration', label: 'C4 - Event-Based Integration', description: 'Event bus, router, dead-letter queue, event store, consumers' },
{ folder: 'arc42-cinema-tickets', label: 'arc42 - Cinema Tickets', description: 'Context, building blocks, runtime and deployment views; seat booking with payment' },
{ folder: 'arc42-iot-telemetry', label: 'arc42 - IoT Telemetry', description: 'Devices over MQTT, ingest, time-series store, alerting, dashboards' },
{ folder: 'arc42-team-wiki', label: 'arc42 - Team Wiki', description: 'A small self-hosted wiki: the arc42 sections at their smallest' },
{ folder: 'archimate-insurance-claims', label: 'ArchiMate - Insurance Claims', description: 'Business, application and technology layers of claim handling' },
{ folder: 'archimate-hr-onboarding', label: 'ArchiMate - HR Onboarding', description: 'Onboarding a new employee across HR, identity and IT services' },
{ folder: 'archimate-logistics-tracking', label: 'ArchiMate - Logistics Tracking', description: 'Shipment tracking from pick-up to proof of delivery, layer by layer' },
]; ];
/** The framework prefix of an example folder; the model id is the folder without it. */
export const EXAMPLE_PREFIX = /^(c4|arc42|archimate)-/;
/** /**
* The example's main.archify for a new model folder. When the folder keeps * The example's main.archify for a new model folder. When the folder keeps
* the example's name the file is used as written; otherwise id, name and the * the example's name the file is used as written; otherwise id, name and the
+23 -11
View File
@@ -163,13 +163,21 @@ export interface DecisionRef {
/** Collections whose items may carry `decisions` (and `diagram` links), per diagram kind. */ /** Collections whose items may carry `decisions` (and `diagram` links), per diagram kind. */
const NODE_COLLECTIONS = ['components', 'nodes', 'participants', 'states'] as const; const NODE_COLLECTIONS = ['components', 'nodes', 'participants', 'states'] as const;
/** The list that holds a diagram kind's nodes, where `ref` is resolved. */
const NODE_COLLECTION_OF: Record<string, (typeof NODE_COLLECTIONS)[number]> = {
architecture: 'components',
workflow: 'nodes',
dataflow: 'nodes',
sequence: 'participants',
lifecycle: 'states',
};
const EDGE_COLLECTIONS = ['connections', 'edges', 'flows', 'transitions', 'messages'] as const; const EDGE_COLLECTIONS = ['connections', 'edges', 'flows', 'transitions', 'messages'] as const;
export interface ResolvedDiagram { export interface ResolvedDiagram {
/** The diagram as Archify expects it: refs expanded, model block and links removed. */ /** The diagram as Archify expects it: refs expanded, model block and links removed. */
diagram: Obj; diagram: Obj;
problems: ModelProblem[]; problems: ModelProblem[];
/** Component index → the registry element it came from. */ /** Node index (in the kind's node list) → the registry element it came from. */
refs: Map<number, RegistryElement>; refs: Map<number, RegistryElement>;
/** Node id → diagram path (relative to the model root) it links to. */ /** Node id → diagram path (relative to the model root) it links to. */
links: Map<string, string>; links: Map<string, string>;
@@ -253,10 +261,14 @@ export function resolveRefs(doc: unknown, options: ResolveOptions): ResolvedDiag
delete diagram.model; delete diagram.model;
const decisionRefs = takeDecisionRefs(diagram, options, problems); const decisionRefs = takeDecisionRefs(diagram, options, problems);
const { registry } = options; const { registry } = options;
const components = Array.isArray(diagram.components) ? diagram.components : undefined; // Each kind keeps its nodes in its own list; sequence participants have no tag.
if (!components) return { diagram, problems, refs, links, used, descriptions, decisionRefs }; const kind = str(diagram.diagram_type);
const collection = (kind && NODE_COLLECTION_OF[kind]) ?? NODE_COLLECTIONS.find((key) => Array.isArray(diagram[key]));
const nodes = collection && Array.isArray(diagram[collection]) ? (diagram[collection] as unknown[]) : undefined;
if (!collection || !nodes) return { diagram, problems, refs, links, used, descriptions, decisionRefs };
const copied: readonly string[] = kind === 'sequence' ? COPIED_FIELDS.filter((key) => key !== 'tag') : COPIED_FIELDS;
diagram.components = components.map((raw, index) => { diagram[collection] = nodes.map((raw, index) => {
if (!isObj(raw)) return raw; if (!isObj(raw)) return raw;
const ref = str(raw.ref); const ref = str(raw.ref);
const linkFromComponent = str(raw.diagram); const linkFromComponent = str(raw.diagram);
@@ -275,7 +287,7 @@ export function resolveRefs(doc: unknown, options: ResolveOptions): ResolvedDiag
? `Unknown element "${ref}": no registry of this model defines it.` ? `Unknown element "${ref}": no registry of this model defines it.`
: `"${ref}" cannot be resolved: ${options.fileName.split(/[\\/]/).pop()} is not inside a model (no ${MAIN_FILE} above it).`, : `"${ref}" cannot be resolved: ${options.fileName.split(/[\\/]/).pop()} is not inside a model (no ${MAIN_FILE} above it).`,
severity: 'error', severity: 'error',
pointer: `/components/${index}/ref`, pointer: `/${collection}/${index}/ref`,
}); });
return { type: 'external', label: ref, ...own, id }; return { type: 'external', label: ref, ...own, id };
} }
@@ -283,19 +295,19 @@ export function resolveRefs(doc: unknown, options: ResolveOptions): ResolvedDiag
refs.set(index, element); refs.set(index, element);
if (element.description) descriptions.set(id, element.description.trim()); if (element.description) descriptions.set(id, element.description.trim());
for (const key of Object.keys(own)) { for (const key of Object.keys(own)) {
if ((COPIED_FIELDS as readonly string[]).includes(key) && !(OVERRIDABLE_FIELDS as readonly string[]).includes(key)) { if (copied.includes(key) && !(OVERRIDABLE_FIELDS as readonly string[]).includes(key)) {
problems.push({ problems.push({
message: `"${key}" comes from the registry element "${ref}" and cannot be changed in a diagram.`, message: `"${key}" comes from the registry element "${ref}" and cannot be changed in a diagram.`,
severity: 'error', severity: 'error',
pointer: `/components/${index}/${key}`, pointer: `/${collection}/${index}/${key}`,
}); });
} }
} }
const merged: Obj = { id }; const merged: Obj = { id };
for (const key of COPIED_FIELDS) if (element[key] !== undefined) merged[key] = element[key]; for (const key of copied) if (element[key as (typeof COPIED_FIELDS)[number]] !== undefined) merged[key] = element[key as (typeof COPIED_FIELDS)[number]];
for (const [key, value] of Object.entries(own)) { for (const [key, value] of Object.entries(own)) {
if (key === 'id') continue; if (key === 'id') continue;
if (!(COPIED_FIELDS as readonly string[]).includes(key) || (OVERRIDABLE_FIELDS as readonly string[]).includes(key)) merged[key] = value; if (!copied.includes(key) || (OVERRIDABLE_FIELDS as readonly string[]).includes(key)) merged[key] = value;
} }
const link = linkFromComponent ?? element.diagram; const link = linkFromComponent ?? element.diagram;
if (link) links.set(id, link); if (link) links.set(id, link);
@@ -353,8 +365,8 @@ export function summarizeDiagram(text: string, relativePath: string): DiagramSum
} }
const obj = isObj(doc) ? doc : {}; const obj = isObj(doc) ? doc : {};
const meta = isObj(obj.meta) ? obj.meta : {}; const meta = isObj(obj.meta) ? obj.meta : {};
const components = Array.isArray(obj.components) ? obj.components : []; const items = NODE_COLLECTIONS.flatMap((key) => (Array.isArray(obj[key]) ? (obj[key] as unknown[]) : []));
const used = [...new Set(components.map((c) => (isObj(c) ? str(c.ref) : undefined)).filter((r): r is string => !!r))]; const used = [...new Set(items.map((c) => (isObj(c) ? str(c.ref) : undefined)).filter((r): r is string => !!r))];
const decisions = takeDecisionRefs({ ...obj }, { registry: { elements: new Map(), files: [], problems: [] }, fileName: relativePath }, []); const decisions = takeDecisionRefs({ ...obj }, { registry: { elements: new Map(), files: [], problems: [] }, fileName: relativePath }, []);
const nodes: DiagramNode[] = []; const nodes: DiagramNode[] = [];
const seen = new Set<string>(); const seen = new Set<string>();
+36 -3
View File
@@ -1,4 +1,5 @@
import * as crypto from 'node:crypto'; import * as crypto from 'node:crypto';
import * as http from 'node:http';
import * as vscode from 'vscode'; import * as vscode from 'vscode';
import { SyncApi, SyncApiError } from './api'; import { SyncApi, SyncApiError } from './api';
import { Account, TokenResponse } from './protocol'; import { Account, TokenResponse } from './protocol';
@@ -100,8 +101,13 @@ export class SyncAuth implements vscode.AuthenticationProvider, vscode.Disposabl
const verifier = crypto.randomBytes(32).toString('base64url'); const verifier = crypto.randomBytes(32).toString('base64url');
const challenge = crypto.createHash('sha256').update(verifier).digest('base64url'); const challenge = crypto.createHash('sha256').update(verifier).digest('base64url');
const state = crypto.randomBytes(16).toString('base64url'); const state = crypto.randomBytes(16).toString('base64url');
const callback = await vscode.env.asExternalUri(vscode.Uri.parse(`${vscode.env.uriScheme}://${this.context.extension.id}/auth`)); // On the desktop the browser comes back to a port on this machine: no
const redirectUri = callback.toString(true); // click needed, and it reaches this window. Remote and web editors use
// the extension's URI handler (vscode://…/auth) instead.
const loopback = vscode.env.remoteName === undefined && vscode.env.uiKind === vscode.UIKind.Desktop ? await this.listen().catch(() => undefined) : undefined;
const redirectUri = loopback
? `http://127.0.0.1:${loopback.port}/auth`
: (await vscode.env.asExternalUri(vscode.Uri.parse(`${vscode.env.uriScheme}://${this.context.extension.id}/auth`))).toString(true);
const query = new URLSearchParams({ const query = new URLSearchParams({
response_type: 'code', response_type: 'code',
redirect_uri: redirectUri, redirect_uri: redirectUri,
@@ -119,6 +125,7 @@ export class SyncAuth implements vscode.AuthenticationProvider, vscode.Disposabl
const done = () => { const done = () => {
clearTimeout(timer); clearTimeout(timer);
this.pending.delete(state); this.pending.delete(state);
loopback?.close();
}; };
this.pending.set(state, { this.pending.set(state, {
resolve: (c) => (done(), resolve(c)), resolve: (c) => (done(), resolve(c)),
@@ -137,9 +144,35 @@ export class SyncAuth implements vscode.AuthenticationProvider, vscode.Disposabl
return session; return session;
} }
/** A one-off HTTP listener on 127.0.0.1 for the browser's way back; closed when the sign-in ends. */
private listen(): Promise<{ port: number; close: () => void }> {
return new Promise((resolve, reject) => {
const server = http.createServer((req, res) => {
const url = new URL(req.url ?? '/', 'http://127.0.0.1');
const known = url.pathname === '/auth' && this.pending.has(url.searchParams.get('state') ?? '');
res.writeHead(known ? 200 : 404, { 'content-type': 'text/html; charset=utf-8', 'cache-control': 'no-store' });
res.end(
known
? '<!doctype html><meta charset="utf-8"><title>Archify Sync</title><body style="font:15px system-ui;margin:48px auto;max-width:420px"><h2>Signed in</h2><p>You can close this tab and go back to the editor.</p></body>'
: 'Not found',
);
if (known) this.completeSignIn(url.searchParams);
});
server.once('error', reject);
server.listen(0, '127.0.0.1', () => {
const address = server.address();
if (!address || typeof address === 'string') return reject(new Error('no port'));
resolve({ port: address.port, close: () => server.close() });
});
});
}
/** The browser came back: `…/auth?code=&state=`. */ /** The browser came back: `…/auth?code=&state=`. */
handleAuthUri(uri: vscode.Uri): void { handleAuthUri(uri: vscode.Uri): void {
const query = new URLSearchParams(uri.query); this.completeSignIn(new URLSearchParams(uri.query));
}
private completeSignIn(query: URLSearchParams): void {
const state = query.get('state') ?? ''; const state = query.get('state') ?? '';
const code = query.get('code'); const code = query.get('code');
const pending = this.pending.get(state); const pending = this.pending.get(state);
+29 -13
View File
@@ -208,6 +208,7 @@ function bridgeScript(options: PageOptions): string {
}, true); }, true);
// Collapsible panels of the model page; open/closed is kept in the webview state. // Collapsible panels of the model page; open/closed is kept in the webview state.
// The model page's top-level panels start closed: a model opens as its outline.
function panelStates() { return (vscode.getState() || {}).modelPanels || {}; } function panelStates() { return (vscode.getState() || {}).modelPanels || {}; }
function panel(key, className, title, count, defaultOpen) { function panel(key, className, title, count, defaultOpen) {
// count: a number or a short text such as "3 files". // count: a number or a short text such as "3 files".
@@ -624,7 +625,7 @@ function bridgeScript(options: PageOptions): string {
live.hidden = true; live.hidden = true;
body.appendChild(live); body.appendChild(live);
var count = model.groups.reduce(function (n, g) { return n + g.diagrams.length; }, 0); var count = model.groups.reduce(function (n, g) { return n + g.diagrams.length; }, 0);
var diagramsPanel = panel('diagrams', 'archify-model-diagrams', 'Diagrams', count, true); var diagramsPanel = panel('diagrams', 'archify-model-diagrams', 'Diagrams', count, false);
body.appendChild(diagramsPanel); body.appendChild(diagramsPanel);
if (!count) diagramsPanel.appendChild(el('p', 'archify-model-empty', 'No diagrams yet. Add .archify files next to main.archify or in subfolders.')); if (!count) diagramsPanel.appendChild(el('p', 'archify-model-empty', 'No diagrams yet. Add .archify files next to main.archify or in subfolders.'));
model.groups.forEach(function (group) { model.groups.forEach(function (group) {
@@ -658,7 +659,7 @@ function bridgeScript(options: PageOptions): string {
byFile[element.file].push(element); byFile[element.file].push(element);
}); });
files.sort(); files.sort();
var registryPanel = panel('registry', 'archify-model-registry', 'Registries', files.length + (files.length === 1 ? ' file' : ' files'), true); var registryPanel = panel('registry', 'archify-model-registry', 'Registries', files.length + (files.length === 1 ? ' file' : ' files'), false);
body.appendChild(registryPanel); body.appendChild(registryPanel);
files.forEach(function (file) { files.forEach(function (file) {
var elements = byFile[file].slice().sort(function (a, b) { return a.line - b.line; }); var elements = byFile[file].slice().sort(function (a, b) { return a.line - b.line; });
@@ -676,7 +677,7 @@ function bridgeScript(options: PageOptions): string {
} }
// Decisions: one list, live ones first, superseded and rejected dimmed and last. // Decisions: one list, live ones first, superseded and rejected dimmed and last.
if (model.decisions && model.decisions.length) { if (model.decisions && model.decisions.length) {
var decisionsPanel = panel('decisions', 'archify-model-decisions', 'Decisions', model.decisions.length, true); var decisionsPanel = panel('decisions', 'archify-model-decisions', 'Decisions', model.decisions.length, false);
body.appendChild(decisionsPanel); body.appendChild(decisionsPanel);
var decisionList = el('ul', 'archify-model-list archify-decision-list'); var decisionList = el('ul', 'archify-model-list archify-decision-list');
model.decisions.forEach(function (row) { decisionList.appendChild(decisionRowItem(row)); }); model.decisions.forEach(function (row) { decisionList.appendChild(decisionRowItem(row)); });
@@ -723,7 +724,7 @@ function bridgeScript(options: PageOptions): string {
if (!info) { holder.hidden = true; return; } if (!info) { holder.hidden = true; return; }
holder.hidden = false; holder.hidden = false;
var others = info.people.filter(function (p) { return !p.me; }); var others = info.people.filter(function (p) { return !p.me; });
var livePanel = panel('live', 'archify-model-live is-' + info.state, 'Shared \u00b7 ' + info.name, others.length ? others.length + 1 + ' here' : info.host, true); var livePanel = panel('live', 'archify-model-live is-' + info.state, 'Shared \u00b7 ' + info.name, others.length ? others.length + 1 + ' here' : info.host, false);
var status = el('div', 'archify-live-status'); var status = el('div', 'archify-live-status');
status.appendChild(el('span', 'archify-live-dot')); status.appendChild(el('span', 'archify-live-dot'));
status.appendChild(el('span', 'archify-live-state', (LIVE_STATE[info.state] || info.state) + (info.state === 'online' ? ' on ' + info.host : ''))); status.appendChild(el('span', 'archify-live-state', (LIVE_STATE[info.state] || info.state) + (info.state === 'online' ? ' on ' + info.host : '')));
@@ -901,14 +902,23 @@ function bridgeScript(options: PageOptions): string {
button.type = 'button'; button.type = 'button';
button.title = 'Archify Sync: sign in, share this model with others, or join one'; button.title = 'Archify Sync: sign in, share this model with others, or join one';
button.addEventListener('click', function (event) { event.stopPropagation(); post({ type: 'syncMenu' }); }); button.addEventListener('click', function (event) { event.stopPropagation(); post({ type: 'syncMenu' }); });
// A diagram page has Archify's toolbar at the top right: the button goes // Last in the row that holds the view switch: Archify's toolbar on a
// last in it, after Export. Other pages have nothing there; it floats. // diagram (after Export), the Graph page's header actions (after Export,
var toolbar = document.querySelector('body > .toolbar'); // built a moment later by graph.js), next to the switch on the other
if (toolbar) { // pages. It floats only while there is no such row.
document.body.appendChild(button);
function place() {
var modeSwitch = document.getElementById('archify-mode-switch');
var row = document.querySelector('.ag-actions') || (modeSwitch && modeSwitch.parentElement) || document.querySelector('body > .toolbar');
if (!row || row === document.body) return false;
button.classList.add('in-toolbar'); button.classList.add('in-toolbar');
toolbar.appendChild(button); row.classList.add('archify-sync-row');
} else { row.appendChild(button);
document.body.appendChild(button); return true;
}
if (!place() && window.MutationObserver) {
var watch = new MutationObserver(function () { if (place()) watch.disconnect(); });
watch.observe(document.body, { childList: true, subtree: true });
} }
showSyncState(false); showSyncState(false);
post({ type: 'syncReady' }); post({ type: 'syncReady' });
@@ -939,8 +949,11 @@ function bridgeScript(options: PageOptions): string {
+ ' display: flex !important; align-items: center !important; justify-content: center !important; border-radius: 8px !important; cursor: pointer !important;' + ' display: flex !important; align-items: center !important; justify-content: center !important; border-radius: 8px !important; cursor: pointer !important;'
+ ' color: #fff !important; background: var(--vscode-button-background, #0e639c) !important; border: 1px solid var(--vscode-contrastBorder, transparent) !important;' + ' color: #fff !important; background: var(--vscode-button-background, #0e639c) !important; border: 1px solid var(--vscode-contrastBorder, transparent) !important;'
+ ' box-shadow: 0 1px 4px rgba(0,0,0,.35) !important; }' + ' box-shadow: 0 1px 4px rgba(0,0,0,.35) !important; }'
+ ' .archify-model-switch-row.archify-sync-row { display: flex !important; align-items: center !important; gap: 0.5rem !important; }'
+ ' #archify-sync-button.in-toolbar { position: relative !important; top: auto !important; right: auto !important; align-self: center !important; flex: none !important; width: 36px !important; height: 36px !important; }' + ' #archify-sync-button.in-toolbar { position: relative !important; top: auto !important; right: auto !important; align-self: center !important; flex: none !important; width: 36px !important; height: 36px !important; }'
+ ' body.vscode-high-contrast #archify-sync-button { background: #0e639c !important; }' + ' .archify-model-switch-row #archify-sync-button.in-toolbar { width: 33px !important; height: 33px !important; }'
+ ' .ag-actions #archify-sync-button.in-toolbar { width: 29px !important; height: 29px !important; }'
+ ' body.vscode-high-contrast #archify-sync-button:not(.is-shared) { background: #0e639c !important; }'
+ ' #archify-sync-button svg { display: block !important; width: 16px !important; height: 16px !important; flex: none !important; }' + ' #archify-sync-button svg { display: block !important; width: 16px !important; height: 16px !important; flex: none !important; }'
+ ' #archify-sync-button:hover { background: var(--vscode-button-hoverBackground, #1177bb) !important; }' + ' #archify-sync-button:hover { background: var(--vscode-button-hoverBackground, #1177bb) !important; }'
+ ' #archify-sync-button.is-shared { background: #1f8f5f !important; }' + ' #archify-sync-button.is-shared { background: #1f8f5f !important; }'
@@ -1073,10 +1086,13 @@ const MODEL_STYLE = `
/* The Model | Graph switch: in Archify's toolbar it is the 2D | 3D switch's twin; on the model page it sits top right. */ /* The Model | Graph switch: in Archify's toolbar it is the 2D | 3D switch's twin; on the model page it sits top right. */
const SWITCH_STYLE = ` const SWITCH_STYLE = `
.archify-mode-switch { display: inline-flex; align-items: center; padding: 0 0.25rem; gap: 0; color: var(--text-muted, var(--vscode-descriptionForeground)); position: relative; } .archify-mode-switch { display: inline-flex; align-items: center; padding: 0 0.25rem; gap: 0; color: var(--text-muted, var(--vscode-descriptionForeground)); position: relative; }
.toolbar .archify-mode-switch { height: 2.75rem; box-sizing: border-box; }
.toolbar .archify-mode-switch::before { content: ""; position: absolute; inset: 0.25rem 0; border-radius: 0.6875rem; background: color-mix(in srgb, var(--panel) 78%, transparent); box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--panel-border) 85%, transparent), 0 1px 2px rgba(15, 23, 42, 0.04); backdrop-filter: blur(12px); } .toolbar .archify-mode-switch::before { content: ""; position: absolute; inset: 0.25rem 0; border-radius: 0.6875rem; background: color-mix(in srgb, var(--panel) 78%, transparent); box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--panel-border) 85%, transparent), 0 1px 2px rgba(15, 23, 42, 0.04); backdrop-filter: blur(12px); }
.toolbar .archify-mode-switch:hover::before { box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--text) 22%, transparent), 0 1px 2px rgba(15, 23, 42, 0.04); } .toolbar .archify-mode-switch:hover::before { box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--text) 22%, transparent), 0 1px 2px rgba(15, 23, 42, 0.04); }
/* Anchored on the id: Archify's .toolbar button rules (one of them with :not(#btn-theme)) would win otherwise. */ /* Anchored on the id: Archify's .toolbar button rules (one of them with :not(#btn-theme)) would win otherwise. */
#archify-mode-switch .archify-mode-seg { position: relative; display: inline-flex; align-items: center; height: 1.75rem; padding: 0 0.7rem; border: 0; border-radius: 0.5rem; background: transparent; color: inherit; font: inherit; font-weight: 550; font-size: 0.8125rem; cursor: pointer; transition: background 0.15s, color 0.15s; } #archify-mode-switch .archify-mode-seg { position: relative; display: inline-flex; align-items: center; height: 1.75rem; min-height: 0; margin: 0; padding: 0 0.7rem; border: 0; border-radius: 0.5rem; background: transparent; color: inherit; font: inherit; font-weight: 550; font-size: 0.8125rem; cursor: pointer; transition: background 0.15s, color 0.15s; }
/* Archify's toolbar buttons are 2.75rem tall with a painted ::before; the segments are not, like 2D | 3D. */
#archify-mode-switch .archify-mode-seg::before, #archify-mode-switch .archify-mode-seg::after { content: none; }
#archify-mode-switch .archify-mode-seg.on[aria-pressed="true"] { color: var(--bg, var(--vscode-editor-background)); background: var(--text, var(--vscode-foreground)); box-shadow: 0 1px 2px rgba(15, 23, 42, 0.12), 0 6px 16px -8px rgba(15, 23, 42, 0.4); } #archify-mode-switch .archify-mode-seg.on[aria-pressed="true"] { color: var(--bg, var(--vscode-editor-background)); background: var(--text, var(--vscode-foreground)); box-shadow: 0 1px 2px rgba(15, 23, 42, 0.12), 0 6px 16px -8px rgba(15, 23, 42, 0.4); }
#archify-mode-switch .archify-mode-seg:hover:not(.on) { color: var(--text, var(--vscode-foreground)); } #archify-mode-switch .archify-mode-seg:hover:not(.on) { color: var(--text, var(--vscode-foreground)); }
#archify-mode-switch .archify-mode-seg:focus-visible { outline: 2px solid var(--frontend-stroke, var(--vscode-focusBorder)); outline-offset: 1px; } #archify-mode-switch .archify-mode-seg:focus-visible { outline: 2px solid var(--frontend-stroke, var(--vscode-focusBorder)); outline-offset: 1px; }
@@ -0,0 +1,30 @@
schema_version: 1
diagram_type: architecture
meta:
title: Technical context
output: technical-context.html
components:
- ref: customer
pos: [40, 60]
size: [130, 60]
- ref: usher
pos: [40, 260]
size: [130, 60]
- ref: tickets
pos: [290, 160]
size: [150, 60]
- ref: psp
pos: [560, 60]
size: [150, 60]
- ref: mail
pos: [560, 160]
size: [150, 60]
- ref: pos
pos: [560, 260]
size: [150, 60]
connections:
- { from: customer, to: tickets, label: HTTPS JSON, variant: emphasis }
- { from: usher, to: tickets, label: HTTPS app }
- { from: tickets, to: psp, label: REST webhook, variant: security }
- { from: tickets, to: mail, label: SMTP TLS }
- { from: tickets, to: pos, label: REST VPN }
@@ -0,0 +1,49 @@
schema_version: 1
diagram_type: architecture
meta:
title: Booking API - level 2
output: booking.html
decisions: [seat-holds-with-ttl]
components:
- ref: web
pos: [40, 160]
size: [130, 60]
- id: programme
type: backend
label: Programme
sublabel: shows, halls
pos: [250, 60]
size: [130, 60]
- id: seats
type: backend
label: Seat Holds
sublabel: lock, release
pos: [250, 160]
size: [130, 60]
- id: checkout
type: backend
label: Checkout
sublabel: orders, webhook
pos: [250, 260]
size: [130, 60]
- ref: seatlock
pos: [460, 160]
size: [130, 60]
decisions: [seat-holds-with-ttl]
- ref: db
pos: [460, 60]
size: [130, 60]
- ref: psp
pos: [460, 360]
size: [150, 60]
boundaries:
- kind: region
label: Booking API
wraps: [programme, seats, checkout]
connections:
- { from: web, to: programme, label: browse }
- { from: web, to: seats, label: hold, variant: emphasis }
- { from: web, to: checkout, label: pay }
- { from: programme, to: db, label: SQL }
- { from: seats, to: seatlock, label: SET NX, decisions: [seat-holds-with-ttl] }
- { from: checkout, to: psp, label: session, variant: security }
@@ -0,0 +1,64 @@
schema_version: 1
diagram_type: architecture
meta:
title: Building blocks - level 1
output: level-1.html
decisions: [booking-owns-seat-inventory]
components:
- ref: web
pos: [40, 110]
size: [130, 60]
- ref: mobile
pos: [40, 210]
size: [130, 60]
- ref: scanner
pos: [40, 360]
size: [130, 60]
- ref: booking
pos: [250, 160]
size: [130, 60]
- ref: seatlock
pos: [460, 60]
size: [130, 60]
- ref: db
pos: [460, 260]
size: [130, 60]
- ref: ticketing
pos: [250, 360]
size: [130, 60]
- ref: possync
pos: [670, 260]
size: [130, 60]
- ref: psp
pos: [900, 160]
size: [150, 60]
- ref: pos
pos: [900, 260]
size: [150, 60]
- ref: mail
pos: [900, 460]
size: [150, 60]
boundaries:
- kind: region
label: Cinema Tickets
wraps: [web, mobile, scanner, booking, seatlock, db, ticketing, possync]
connections:
- { from: web, to: booking, label: JSON, variant: emphasis }
- { from: mobile, to: booking, label: JSON }
- { from: booking, to: seatlock, label: hold }
- { from: booking, to: db, label: SQL }
- { from: booking, to: psp, label: checkout, variant: security }
- { from: booking, to: ticketing, label: issue, labelAt: [315, 234] }
- { from: scanner, to: ticketing, label: check-in }
- { from: ticketing, to: db, label: SQL }
- { from: ticketing, to: mail, label: QR mail }
- { from: possync, to: db, label: SQL }
- { from: possync, to: pos, label: sales, decisions: [booking-owns-seat-inventory] }
cards:
- dot: amber
title: 8 Crosscutting concepts
items:
- "Times in UTC; shown in cinema local time"
- "Every write carries an idempotency key"
- "QR payload signed with an Ed25519 key"
- "Structured logs with order id as trace key"
@@ -0,0 +1,27 @@
schema_version: 1
diagram_type: sequence
meta:
title: Runtime - book seats
output: book-seats.html
viewBox: [1060, 680]
column_fit: spread
decisions: [seat-holds-with-ttl]
participants:
- { ref: customer }
- { ref: web }
- { ref: booking }
- { ref: seatlock }
- { ref: psp }
- { ref: ticketing }
- { ref: mail }
messages:
- { from: customer, to: web, y: 170, label: pick seats, variant: emphasis }
- { from: web, to: booking, y: 214, label: POST /holds }
- { from: booking, to: seatlock, y: 258, label: SET NX TTL 8m, decisions: [seat-holds-with-ttl] }
- { from: seatlock, to: booking, y: 302, label: OK, variant: return }
- { from: booking, to: web, y: 346, label: hold id, variant: return }
- { from: web, to: psp, y: 390, label: hosted checkout, variant: security }
- { from: psp, to: booking, y: 434, label: webhook paid, variant: security }
- { from: booking, to: ticketing, y: 478, label: issue tickets }
- { from: ticketing, to: mail, y: 522, label: QR e-mail }
- { from: mail, to: customer, y: 566, label: tickets, variant: return }
@@ -0,0 +1,19 @@
schema_version: 1
diagram_type: sequence
meta:
title: Runtime - check-in at the door
output: check-in.html
viewBox: [900, 480]
column_fit: spread
decisions: [signed-qr-codes]
participants:
- { ref: usher }
- { ref: scanner }
- { ref: ticketing }
- { ref: db }
messages:
- { from: usher, to: scanner, y: 180, label: scan QR, variant: emphasis, note: signature checked offline, decisions: [signed-qr-codes] }
- { from: scanner, to: ticketing, y: 230, label: POST /checkins }
- { from: ticketing, to: db, y: 280, label: first scan wins }
- { from: ticketing, to: scanner, y: 330, label: valid seat 7F, variant: return }
- { from: scanner, to: usher, y: 380, label: green light, variant: return }
@@ -0,0 +1,58 @@
schema_version: 1
diagram_type: architecture
meta:
title: Deployment - production
output: production.html
components:
- ref: customer
pos: [40, 160]
size: [130, 60]
- id: ingress
type: security
label: Ingress
sublabel: TLS, WAF
pos: [250, 160]
size: [130, 60]
- ref: web
pos: [460, 60]
size: [130, 60]
- ref: booking
pos: [460, 160]
size: [130, 60]
- ref: ticketing
pos: [460, 260]
size: [130, 60]
- ref: possync
pos: [460, 360]
size: [130, 60]
- ref: seatlock
pos: [710, 110]
size: [130, 60]
- ref: db
pos: [710, 260]
size: [130, 60]
- ref: pos
pos: [250, 460]
size: [150, 60]
- ref: scanner
pos: [40, 460]
size: [130, 60]
boundaries:
- kind: region
label: EU cloud - k8s cluster
wraps: [ingress, web, booking, ticketing, possync]
- kind: security-group
label: Managed data zone
wraps: [seatlock, db]
- kind: region
label: Cinema site
wraps: [pos, scanner]
connections:
- { from: customer, to: ingress, label: HTTPS, variant: emphasis }
- { from: ingress, to: web, label: route }
- { from: ingress, to: booking, label: route }
- { from: booking, to: seatlock, label: TLS }
- { from: booking, to: db, label: TLS }
- { from: ticketing, to: db, label: TLS }
- { from: possync, to: pos, label: VPN, variant: security }
- { from: scanner, to: ingress, label: HTTPS }
@@ -0,0 +1,20 @@
schema_version: 1
decision:
id: seat-holds-with-ttl
title: Seat holds as Redis keys with a TTL
status: accepted
date: 2026-09-08
elements: [booking, seatlock]
diagrams: [0500-building-blocks/booking.archify, 0600-runtime/book-seats.archify]
context: >
A customer may spend minutes at the payment page. Holding seats
as rows in the database left orphans when a browser closed, and
a nightly job freed them far too late for Friday shows.
decision: >
A hold is one Redis key per seat, set with SET NX and an
8-minute TTL. The payment webhook turns holds into an order in
PostgreSQL; an expired key frees the seat without any job.
consequences: >
Holds survive no Redis restart; at worst a customer re-picks
seats. The webhook must check the hold still exists before it
confirms the order, otherwise it refunds.
@@ -0,0 +1,20 @@
schema_version: 1
decision:
id: booking-owns-seat-inventory
title: The Booking API owns the seat inventory
status: accepted
date: 2026-09-22
elements: [booking, db, possync, pos]
diagrams: [0500-building-blocks/level-1.archify]
context: >
The box-office POS and the web shop both sold seats from their
own copy of the seat map. Twice a month a seat was sold at the
counter and online.
decision: >
The Tickets DB is the only seat inventory. The POS asks POS Sync
for a seat before it prints a ticket; POS Sync writes the sale
through the same rules as online orders.
consequences: >
The counter needs the network to sell; a fallback block of
reserved seats per show covers outages. The POS vendor adapter
is ours to maintain.
@@ -0,0 +1,19 @@
schema_version: 1
decision:
id: signed-qr-codes
title: Signed QR codes checked offline at the door
status: accepted
date: 2026-10-06
elements: [ticketing, scanner]
diagrams: [0600-runtime/check-in.archify]
context: >
Cinema Wi-Fi drops when a full hall arrives. A scanner that
needs the server for every ticket stops the queue.
decision: >
The Ticket Service signs each QR payload (ticket id, show,
seat) with a private key. The scanner app verifies it with the
public key and queues check-ins until it is online again.
consequences: >
A forwarded ticket scans twice while offline; the second scan
is flagged when the queue syncs. Key rotation needs an app
update window.
@@ -0,0 +1,72 @@
schema_version: 1
model:
id: cinema-tickets
name: Cinema Tickets
description: >
An arc42 example: online and mobile seat booking for a small
cinema chain. This overview is section 3 (business context) with
cards for sections 1, 2, 10, 11 and 12. 0300-context holds the
technical context (3), 0500-building-blocks the building block
view (5), 0600-runtime the runtime view (6), 0700-deployment the
deployment view (7), decisions/ the architecture decisions (9).
Crosscutting concepts (8) are a card on the level-1 diagram.
diagram_type: architecture
meta:
title: Cinema Tickets - business context
output: main.html
components:
- ref: customer
pos: [40, 60]
size: [130, 60]
- ref: usher
pos: [40, 260]
size: [130, 60]
- ref: tickets
pos: [280, 160]
size: [150, 60]
- ref: psp
pos: [540, 60]
size: [150, 60]
- ref: mail
pos: [540, 160]
size: [150, 60]
- ref: pos
pos: [540, 260]
size: [150, 60]
connections:
- { from: customer, to: tickets, label: books seats, variant: emphasis }
- { from: usher, to: tickets, label: checks in }
- { from: tickets, to: psp, label: payments, variant: security }
- { from: tickets, to: mail, label: tickets }
- { from: tickets, to: pos, label: seat sales }
cards:
- dot: cyan
title: 1 Quality goals
items:
- "No seat is ever sold twice"
- "Checkout under 2 s at Friday peak"
- "Door check-in works offline"
- dot: slate
title: 2 Constraints
items:
- "Existing box-office POS stays"
- "Card data only at the provider"
- "Team of four; hosted in the EU"
- dot: emerald
title: 10 Quality scenarios
items:
- "500 holds on one show in 1 min: no double sale"
- "Wi-Fi down at the door: scans go on, sync later"
- "Payment fails: seats free again within 8 min"
- dot: rose
title: 11 Risks
items:
- "POS vendor API is slow and undocumented"
- "Forwarded QR e-mails: first scan wins"
- dot: violet
title: 12 Glossary
items:
- "Show: one screening of a film in a hall"
- "Hold: a seat locked for one customer"
- "Check-in: first valid scan of a ticket"
- "POS: point-of-sale till at the box office"
@@ -0,0 +1,57 @@
schema_version: 1
elements:
web:
type: frontend
label: Web Shop
sublabel: React
brand: react
description: >
Programme, seat map and checkout in the browser.
mobile:
type: frontend
label: Mobile App
sublabel: iOS and Android
description: >
The same booking flow as the web shop, plus a ticket wallet.
scanner:
type: frontend
label: Scanner App
sublabel: door phones
description: >
Reads QR codes at the door; checks signatures offline.
booking:
type: backend
label: Booking API
sublabel: Node.js
brand: node-js
description: >
Shows, seat maps, seat holds and orders; the only writer of
the seat inventory.
diagram: 0500-building-blocks/booking.archify
ticketing:
type: backend
label: Ticket Service
sublabel: QR, check-in
description: >
Issues signed QR tickets, sends them by e-mail and records
check-ins at the door.
possync:
type: backend
label: POS Sync
sublabel: adapter
description: >
Exchanges seat sales with the box-office POS in both directions.
seatlock:
type: database
label: Seat Locks
sublabel: Redis
brand: redis
description: >
Short-lived seat holds with a TTL while the customer pays.
db:
type: database
label: Tickets DB
sublabel: PostgreSQL
brand: postgresql
description: >
Shows, seats, orders and tickets; the source of truth.
@@ -0,0 +1,14 @@
schema_version: 1
elements:
customer:
type: external
label: Moviegoer
sublabel: web and mobile
description: >
Picks a show, holds seats and pays; gets the tickets by e-mail.
usher:
type: external
label: Usher
sublabel: door staff
description: >
Scans tickets at the auditorium door with the scanner app.
@@ -0,0 +1,30 @@
schema_version: 1
elements:
tickets:
type: cloud
label: Cinema Tickets
sublabel: the system
description: >
Online booking, seat holds, e-mail tickets and door check-in
for the five cinemas of the fictional Lantern Cinemas chain.
diagram: 0500-building-blocks/level-1.archify
psp:
type: external
label: Payment provider
sublabel: hosted checkout
description: >
Takes card and wallet payments on its own page; we never see
card numbers.
pos:
type: external
label: Box-office POS
sublabel: in each cinema
description: >
Till system at the counter; sells walk-in seats from the same
seat map.
mail:
type: external
label: E-mail service
sublabel: transactional
description: >
Delivers ticket e-mails with QR codes and receipts.
@@ -0,0 +1,35 @@
schema_version: 1
diagram_type: architecture
meta:
title: Technical context
output: technical-context.html
decisions: [mqtt-with-mutual-tls]
components:
- ref: homeowner
pos: [40, 60]
size: [130, 60]
- ref: ops-engineer
pos: [40, 160]
size: [130, 60]
- ref: installer
pos: [40, 260]
size: [130, 60]
- ref: telemetry-platform
pos: [280, 160]
size: [150, 60]
- ref: heat-pump
pos: [540, 60]
size: [130, 60]
- ref: push-service
pos: [540, 160]
size: [130, 60]
- ref: service-desk
pos: [540, 260]
size: [130, 60]
connections:
- { from: homeowner, to: telemetry-platform, label: HTTPS }
- { from: ops-engineer, to: telemetry-platform, label: HTTPS, variant: security }
- { from: installer, to: telemetry-platform, label: HTTPS }
- { from: heat-pump, to: telemetry-platform, label: MQTT 8883, variant: emphasis, decisions: [mqtt-with-mutual-tls] }
- { from: telemetry-platform, to: push-service, label: HTTP/2 }
- { from: telemetry-platform, to: service-desk, label: webhook }
@@ -0,0 +1,50 @@
schema_version: 1
diagram_type: architecture
meta:
title: Ingest, level 2
output: ingest.html
components:
- ref: mqtt-broker
pos: [40, 160]
size: [130, 60]
- id: decoder
type: backend
label: Decoder
sublabel: payload v1, v2
pos: [250, 160]
size: [130, 60]
- id: validator
type: backend
label: Validator
sublabel: ranges, clock
pos: [460, 160]
size: [130, 60]
- id: writer
type: backend
label: Batch writer
sublabel: 500 rows
pos: [670, 160]
size: [130, 60]
- id: dead-letter
type: database
label: Dead letters
sublabel: rejected readings
pos: [460, 310]
size: [130, 60]
- ref: tsdb
pos: [880, 160]
size: [130, 60]
- ref: rules
pos: [670, 310]
size: [130, 60]
boundaries:
- kind: region
label: Ingest
wraps: [decoder, validator, writer, dead-letter]
connections:
- { from: mqtt-broker, to: decoder, label: messages, variant: emphasis }
- { from: decoder, to: validator, label: readings, variant: emphasis }
- { from: validator, to: writer, label: valid, variant: emphasis }
- { from: validator, to: dead-letter, label: invalid, variant: dashed, labelAt: [525, 234] }
- { from: writer, to: tsdb, label: COPY, variant: emphasis }
- { from: writer, to: rules, label: stream, labelAt: [735, 234] }
@@ -0,0 +1,77 @@
schema_version: 1
diagram_type: architecture
meta:
title: Building blocks, level 1
output: level-1.html
decisions: [timescaledb-for-telemetry]
components:
- ref: heat-pump
pos: [40, 160]
size: [130, 60]
- ref: installer
pos: [40, 360]
size: [130, 60]
- ref: mqtt-broker
pos: [250, 160]
size: [130, 60]
decisions: [mqtt-with-mutual-tls]
- ref: provisioning
pos: [250, 360]
size: [130, 60]
- ref: rules
pos: [460, 60]
size: [130, 60]
- ref: ingest
pos: [460, 160]
size: [130, 60]
- ref: device-registry
pos: [460, 360]
size: [130, 60]
- ref: tsdb
pos: [670, 160]
size: [130, 60]
- ref: device-api
pos: [670, 360]
size: [130, 60]
- ref: ops-dashboard
pos: [880, 160]
size: [130, 60]
- ref: customer-app
pos: [880, 360]
size: [130, 60]
- ref: push-service
pos: [1090, 60]
size: [130, 60]
- ref: ops-engineer
pos: [1090, 160]
size: [130, 60]
- ref: homeowner
pos: [1090, 360]
size: [130, 60]
boundaries:
- kind: region
label: IoT Telemetry
wraps: [mqtt-broker, provisioning, rules, ingest, device-registry, tsdb, device-api, ops-dashboard, customer-app]
connections:
- { from: heat-pump, to: mqtt-broker, label: publish, variant: emphasis, decisions: [mqtt-with-mutual-tls] }
- { from: mqtt-broker, to: ingest, label: subscribe, variant: emphasis }
- { from: ingest, to: tsdb, label: write, variant: emphasis }
- { from: ingest, to: rules, label: stream }
- { from: ingest, to: device-registry, label: lookup, labelAt: [525, 234] }
- { from: rules, to: push-service, label: alert }
- { from: installer, to: provisioning, label: register }
- { from: provisioning, to: mqtt-broker, label: certs, variant: security }
- { from: provisioning, to: device-registry, label: store }
- { from: device-api, to: tsdb, label: read }
- { from: device-api, to: device-registry, label: read }
- { from: tsdb, to: ops-dashboard, label: query }
- { from: customer-app, to: device-api, label: REST }
- { from: ops-engineer, to: ops-dashboard, label: views }
- { from: homeowner, to: customer-app, label: uses }
cards:
- dot: amber
title: 8 Crosscutting concepts
items:
- Device identity, one X.509 certificate per device
- Idempotent writes, keyed by device id and timestamp
- Observability, metrics and traces on every service
@@ -0,0 +1,20 @@
schema_version: 1
diagram_type: sequence
meta:
title: Device provisioning
output: device-provisioning.html
decisions: [mqtt-with-mutual-tls]
participants:
- { ref: installer }
- { ref: provisioning }
- { ref: device-registry, label: Devices }
- { ref: heat-pump }
- { ref: mqtt-broker }
messages:
- { from: installer, to: provisioning, y: 160, label: register serial, variant: emphasis }
- { from: provisioning, to: device-registry, y: 210, label: store device }
- { from: provisioning, to: installer, y: 260, label: certificate, variant: return }
- { from: installer, to: heat-pump, y: 310, label: load certificate }
- { from: heat-pump, to: mqtt-broker, y: 360, label: connect mTLS, variant: emphasis }
- { from: mqtt-broker, to: provisioning, y: 410, label: check revocation }
- { from: mqtt-broker, to: heat-pump, y: 460, label: CONNACK, variant: return }
@@ -0,0 +1,20 @@
schema_version: 1
diagram_type: sequence
meta:
title: Fault reading to alert
output: telemetry-to-alert.html
participants:
- { ref: heat-pump }
- { ref: mqtt-broker }
- { ref: ingest }
- { ref: tsdb, label: TSDB }
- { ref: rules }
- { ref: push-service }
messages:
- { from: heat-pump, to: mqtt-broker, y: 160, label: publish fault, variant: emphasis }
- { from: mqtt-broker, to: ingest, y: 210, label: deliver QoS 1 }
- { from: ingest, to: tsdb, y: 260, label: write batch }
- { from: tsdb, to: ingest, y: 310, label: committed, variant: return }
- { from: ingest, to: mqtt-broker, y: 360, label: ack, variant: return }
- { from: ingest, to: rules, y: 410, label: reading }
- { from: rules, to: push-service, y: 460, label: send alert, variant: emphasis }
@@ -0,0 +1,44 @@
schema_version: 1
diagram_type: architecture
meta:
title: Deployment, production
output: production.html
decisions: [timescaledb-for-telemetry]
components:
- ref: heat-pump
pos: [40, 160]
size: [130, 60]
- ref: mqtt-broker
pos: [280, 160]
size: [130, 60]
- ref: ingest
pos: [490, 160]
size: [130, 60]
- ref: rules
pos: [490, 60]
size: [130, 60]
- ref: provisioning
pos: [280, 260]
size: [130, 60]
- ref: device-api
pos: [490, 260]
size: [130, 60]
- ref: tsdb
pos: [760, 160]
size: [130, 60]
- ref: device-registry
pos: [760, 260]
size: [130, 60]
boundaries:
- kind: region
label: Kubernetes cluster, eu-central
wraps: [mqtt-broker, ingest, rules, provisioning, device-api]
- kind: security-group
label: Managed DB zone
wraps: [tsdb, device-registry]
connections:
- { from: heat-pump, to: mqtt-broker, label: "8883", variant: emphasis }
- { from: mqtt-broker, to: ingest, label: in-cluster }
- { from: ingest, to: tsdb, label: "5432", variant: emphasis }
- { from: ingest, to: rules, label: gRPC }
- { from: device-api, to: device-registry, label: "5432" }
@@ -0,0 +1,21 @@
schema_version: 1
decision:
id: mqtt-with-mutual-tls
title: MQTT with mutual TLS for device traffic
status: accepted
date: 2026-08-12
elements: [heat-pump, mqtt-broker, provisioning]
diagrams: [0300-context/technical-context.archify, 0500-building-blocks/level-1.archify]
context: >
Heat pumps sit behind home routers on slow and flaky links.
The first prototype polled an HTTPS endpoint and drained
mobile data plans; shared API keys leaked from one device.
decision: >
Devices publish over MQTT 3.1.1 on port 8883. Each device
has its own client certificate issued by Provisioning;
the broker rejects any connection without one.
consequences: >
Small messages and one long-lived connection per device.
Certificates must be rotated and revoked, so Provisioning
is a critical building block. Installers need a step to
register the device before it can connect.
@@ -0,0 +1,20 @@
schema_version: 1
decision:
id: timescaledb-for-telemetry
title: TimescaleDB for telemetry
status: accepted
date: 2026-09-03
elements: [tsdb, ingest, ops-dashboard]
diagrams: [0500-building-blocks/level-1.archify, 0700-deployment/production.archify]
context: >
The fleet sends about 4,000 readings a second. A plain
PostgreSQL table grew too fast and dashboards slowed down
after a few weeks of data.
decision: >
Store telemetry in TimescaleDB hypertables with hourly
continuous aggregates. Raw data is kept 90 days, rollups
10 years. The team already runs PostgreSQL.
consequences: >
SQL stays the query language for the API and the
dashboard. Retention jobs must be monitored. Very high
device counts may need sharding later (see risk R1).
@@ -0,0 +1,76 @@
schema_version: 1
model:
id: iot-telemetry
name: IoT Telemetry
description: >
An arc42 example: a fictional heat pump maker collects device telemetry
over MQTT, stores it, raises alerts and shows it to operations and customers.
main.archify holds section 3 business context with cards for sections
1 goals, 2 constraints, 10 quality, 11 risks and 12 glossary;
0300-context the technical context; 0500-building-blocks section 5;
0600-runtime section 6; 0700-deployment section 7; decisions/ section 9;
the crosscutting concepts of section 8 are a card on level-1.
diagram_type: architecture
meta:
title: Business context
output: main.html
components:
- ref: homeowner
pos: [40, 60]
size: [130, 60]
- ref: ops-engineer
pos: [40, 160]
size: [130, 60]
- ref: installer
pos: [40, 260]
size: [130, 60]
- ref: telemetry-platform
pos: [280, 160]
size: [150, 60]
- ref: heat-pump
pos: [540, 60]
size: [130, 60]
- ref: push-service
pos: [540, 160]
size: [130, 60]
- ref: service-desk
pos: [540, 260]
size: [130, 60]
connections:
- { from: homeowner, to: telemetry-platform, label: checks }
- { from: ops-engineer, to: telemetry-platform, label: monitors }
- { from: installer, to: telemetry-platform, label: registers }
- { from: heat-pump, to: telemetry-platform, label: telemetry, variant: emphasis }
- { from: telemetry-platform, to: push-service, label: alerts }
- { from: telemetry-platform, to: service-desk, label: tickets }
cards:
- dot: cyan
title: 1 Quality goals
items:
- Reliability, no telemetry lost once accepted
- Timeliness, alert within 60 s of a fault
- Security, every device authenticated
- dot: slate
title: 2 Constraints
items:
- Devices speak MQTT only; firmware by a partner
- Data stays in one EU region
- Team of six; PostgreSQL skills in house
- dot: emerald
title: 10 Quality scenarios
items:
- A fault code reaches the homeowner within 60 s
- Broker restart, devices reconnect, no data lost
- A stolen certificate is revoked within 15 min
- dot: rose
title: 11 Risks
items:
- R1 fleet growth beyond one TSDB node
- R2 partner firmware delays certificate rotation
- dot: violet
title: 12 Glossary
items:
- Telemetry, readings a device sends
- Rollup, hourly aggregate of telemetry
- Provisioning, giving a device its identity
- Fault code, error number from the device
@@ -0,0 +1,61 @@
schema_version: 1
elements:
mqtt-broker:
type: messagebus
label: MQTT broker
sublabel: mutual TLS
description: >
Terminates device connections; one topic per device.
ingest:
type: backend
label: Ingest
sublabel: Go
brand: go
description: >
Decodes, validates and stores telemetry; forwards it to the rules.
diagram: 0500-building-blocks/ingest.archify
tsdb:
type: database
label: Time series DB
sublabel: TimescaleDB
brand: postgresql
description: >
Raw telemetry for 90 days and hourly rollups for 10 years.
rules:
type: backend
label: Rules engine
sublabel: alerting
description: >
Evaluates thresholds and fault codes; raises alerts.
provisioning:
type: security
label: Provisioning
sublabel: device certs
description: >
Registers devices and issues their client certificates.
device-registry:
type: database
label: Device registry
sublabel: PostgreSQL
brand: postgresql
description: >
Devices, owners, firmware versions and certificate state.
device-api:
type: backend
label: Device API
sublabel: REST
description: >
Serves telemetry and device data to the customer app.
ops-dashboard:
type: frontend
label: Ops dashboard
sublabel: Grafana
brand: grafana
description: >
Fleet view for the operations team, read only on the TSDB.
customer-app:
type: frontend
label: Customer app
sublabel: mobile
description: >
Shows the homeowner their heat pump and its alerts.
@@ -0,0 +1,20 @@
schema_version: 1
elements:
homeowner:
type: external
label: Homeowner
sublabel: customer app
description: >
Owns a heat pump; checks temperatures and gets fault alerts.
ops-engineer:
type: external
label: Ops engineer
sublabel: operations team
description: >
Watches the fleet, handles alerts and plans service visits.
installer:
type: external
label: Installer
sublabel: field service
description: >
Mounts heat pumps on site and registers them with the platform.
@@ -0,0 +1,27 @@
schema_version: 1
elements:
telemetry-platform:
type: cloud
label: IoT Telemetry
sublabel: the system
description: >
Everything we build and run; broker, ingest, stores, rules and apps.
diagram: 0500-building-blocks/level-1.archify
heat-pump:
type: external
label: Heat pump
sublabel: field device
description: >
Sends telemetry every 30 seconds over MQTT; built by a partner.
push-service:
type: external
label: Push service
sublabel: mobile push
description: >
Delivers push notifications to the customer app.
service-desk:
type: external
label: Service desk
sublabel: ticketing
description: >
Opens service tickets for faults that need a visit.
@@ -0,0 +1,26 @@
schema_version: 1
diagram_type: architecture
meta:
title: Technical context
output: technical-context.html
components:
- ref: engineer
pos: [40, 60]
size: [130, 60]
- ref: wiki-admin
pos: [40, 260]
size: [130, 60]
- ref: wiki
pos: [280, 160]
size: [150, 60]
- ref: idp
pos: [520, 60]
size: [150, 60]
- ref: object-store
pos: [520, 260]
size: [150, 60]
connections:
- { from: engineer, to: wiki, label: HTTPS, variant: emphasis }
- { from: wiki-admin, to: wiki, label: kubectl }
- { from: wiki, to: idp, label: OIDC, variant: security }
- { from: wiki, to: object-store, label: S3 API }
@@ -0,0 +1,47 @@
schema_version: 1
diagram_type: architecture
meta:
title: Building blocks - level 1
output: level-1.html
decisions: [postgres-full-text-search]
components:
- ref: engineer
pos: [40, 160]
size: [130, 60]
- ref: web
pos: [250, 160]
size: [130, 60]
- ref: api
pos: [460, 160]
size: [130, 60]
- ref: db
pos: [460, 360]
size: [130, 60]
- ref: backup-job
pos: [670, 360]
size: [130, 60]
- ref: idp
pos: [670, 60]
size: [130, 60]
- ref: object-store
pos: [880, 160]
size: [130, 60]
boundaries:
- kind: region
label: Team Wiki
wraps: [web, api, db, backup-job]
connections:
- { from: engineer, to: web, label: HTTPS, variant: emphasis }
- { from: web, to: api, label: JSON, variant: emphasis }
- { from: api, to: db, label: SQL, labelAt: [525, 234], decisions: [postgres-full-text-search] }
- { from: api, to: idp, label: OIDC, variant: security }
- { from: api, to: object-store, label: files }
- { from: backup-job, to: db, label: dump }
- { from: backup-job, to: object-store, label: upload }
cards:
- dot: amber
title: 8 Crosscutting concepts
items:
- Access - every request checked against space groups
- Logs - JSON to stdout, collected by the cluster
- Config - environment variables, secrets from the vault
@@ -0,0 +1,55 @@
schema_version: 1
diagram_type: architecture
meta:
title: Building blocks - level 2, Wiki API
output: wiki-api.html
components:
- ref: web
pos: [40, 160]
size: [130, 60]
- id: auth
type: security
label: Auth
sublabel: OIDC session
pos: [250, 60]
size: [130, 60]
- id: pages
type: backend
label: Pages
sublabel: revisions
pos: [250, 160]
size: [130, 60]
- id: search
type: backend
label: Search
sublabel: full-text
pos: [250, 260]
size: [130, 60]
decisions: [postgres-full-text-search]
- id: attachments
type: backend
label: Attachments
pos: [250, 360]
size: [130, 60]
- ref: idp
pos: [460, 60]
size: [130, 60]
- ref: db
pos: [460, 210]
size: [130, 60]
- ref: object-store
pos: [460, 360]
size: [130, 60]
boundaries:
- kind: region
label: Wiki API
wraps: [auth, pages, search, attachments]
connections:
- { from: web, to: auth, label: sign in, variant: security }
- { from: web, to: pages, label: edit, labelAt: [222, 182] }
- { from: web, to: search, label: query }
- { from: web, to: attachments, label: upload }
- { from: auth, to: idp, label: OIDC, variant: security }
- { from: pages, to: db, label: SQL }
- { from: search, to: db, label: SQL }
- { from: attachments, to: object-store, label: S3 }
@@ -0,0 +1,20 @@
schema_version: 1
diagram_type: sequence
meta:
title: Runtime - search and open a page
output: search-page.html
participants:
- ref: engineer
- ref: web
- ref: api
- { ref: db, label: DB }
- { ref: object-store, label: Object store }
messages:
- { from: engineer, to: web, y: 160, label: type query, variant: emphasis }
- { from: web, to: api, y: 210, label: GET /search }
- { from: api, to: db, y: 260, label: full-text query }
- { from: db, to: api, y: 310, label: ranked pages, variant: return }
- { from: api, to: web, y: 360, label: results, variant: return }
- { from: web, to: api, y: 410, label: GET /pages/id }
- { from: api, to: object-store, y: 460, label: signed URL }
- { from: api, to: web, y: 510, label: page, variant: return }
@@ -0,0 +1,19 @@
schema_version: 1
diagram_type: sequence
meta:
title: Runtime - sign in
output: sign-in.html
decisions: [sso-only-via-company-idp]
participants:
- ref: engineer
- ref: web
- ref: api
- { ref: idp, label: Company IdP }
messages:
- { from: engineer, to: web, y: 160, label: open wiki, variant: emphasis }
- { from: web, to: api, y: 210, label: GET /session }
- { from: api, to: web, y: 260, label: redirect, variant: return }
- { from: web, to: idp, y: 310, label: OIDC login, variant: security }
- { from: idp, to: api, y: 360, label: code, variant: return }
- { from: api, to: idp, y: 410, label: token exchange, variant: security }
- { from: api, to: web, y: 460, label: session cookie, variant: return }
@@ -0,0 +1,48 @@
schema_version: 1
diagram_type: architecture
meta:
title: Deployment - production
output: production.html
components:
- ref: engineer
pos: [40, 160]
size: [130, 60]
- id: ingress
type: security
label: Ingress
sublabel: TLS
pos: [250, 160]
size: [130, 60]
- ref: web
pos: [460, 60]
size: [130, 60]
- ref: api
pos: [460, 160]
size: [130, 60]
- ref: db
pos: [670, 160]
size: [130, 60]
- ref: backup-job
pos: [670, 360]
size: [130, 60]
- ref: idp
pos: [460, 460]
size: [130, 60]
- ref: object-store
pos: [880, 460]
size: [130, 60]
boundaries:
- kind: region
label: Company cluster
wraps: [ingress, web, api, db, backup-job]
- kind: security-group
label: Namespace team-wiki
wraps: [web, api, db, backup-job]
connections:
- { from: engineer, to: ingress, label: HTTPS, variant: emphasis }
- { from: ingress, to: web, label: static }
- { from: ingress, to: api, label: /api, variant: emphasis }
- { from: api, to: db, label: SQL }
- { from: api, to: idp, label: OIDC, variant: security, labelAt: [525, 234] }
- { from: backup-job, to: db, label: dump }
- { from: backup-job, to: object-store, label: S3 }
@@ -0,0 +1,20 @@
schema_version: 1
decision:
id: postgres-full-text-search
title: PostgreSQL full-text search, no search engine
status: accepted
date: 2026-10-05
elements: [api, db]
diagrams: [0500-building-blocks/level-1.archify, 0500-building-blocks/wiki-api.archify]
context: >
The wiki holds a few thousand pages for one team. A
separate search engine would be one more service to run,
back up and keep in sync.
decision: >
Search uses PostgreSQL full-text search on a generated
column of each page. The API queries it in the same
transaction as the page data.
consequences: >
Results are never stale and backups cover search too.
Ranking is simpler than a search engine. Revisit when
the wiki grows past 100000 pages.
@@ -0,0 +1,19 @@
schema_version: 1
decision:
id: sso-only-via-company-idp
title: Sign-in only through the company identity provider
status: accepted
date: 2026-10-12
elements: [idp, api, web]
diagrams: [0300-context/technical-context.archify, 0600-runtime/sign-in.archify]
context: >
Leavers kept access to the old wiki because it had its
own passwords. Nobody owned the user list.
decision: >
The wiki has no local accounts. Users sign in with OIDC
at the company identity provider; team groups map to
wiki spaces.
consequences: >
Offboarding removes wiki access at once. The wiki cannot
be used when the identity provider is down. The admin
needs a break-glass account kept in the vault.
@@ -0,0 +1,66 @@
schema_version: 1
model:
id: team-wiki
name: Team Wiki
description: >
An arc42 example: a small self-hosted wiki for an engineering
team. Sections 1 goals, 2 constraints, 3 business context,
10 quality, 11 risks and 12 glossary are on this overview;
0300-context holds 3 technical context, 0500-building-blocks
5 building blocks, 0600-runtime 6 runtime, 0700-deployment
7 deployment and decisions 9 architecture decisions.
diagram_type: architecture
meta:
title: Team Wiki - business context
output: main.html
decisions: [sso-only-via-company-idp]
components:
- ref: engineer
pos: [40, 60]
size: [130, 60]
- ref: wiki-admin
pos: [40, 260]
size: [130, 60]
- ref: wiki
pos: [280, 160]
size: [150, 60]
- ref: idp
pos: [520, 60]
size: [150, 60]
- ref: object-store
pos: [520, 260]
size: [150, 60]
connections:
- { from: engineer, to: wiki, label: reads, variant: emphasis }
- { from: wiki-admin, to: wiki, label: runs }
- { from: wiki, to: idp, label: sign-in, variant: security }
- { from: wiki, to: object-store, label: files }
cards:
- dot: cyan
title: 1 Quality goals
items:
- Findability - any page found in two searches
- Security - only current staff can read
- Operability - one person runs it in an hour a week
- dot: slate
title: 2 Constraints
items:
- Runs on the company Kubernetes cluster
- Sign-in only via the company identity provider
- dot: emerald
title: 10 Quality scenarios
items:
- A search over 5000 pages answers in under 1 s
- A leaver loses access within 5 minutes
- The admin restores last night's state in 30 minutes
- dot: rose
title: 11 Risks
items:
- Identity provider outage blocks all sign-ins
- Backups untested until a restore drill is run
- dot: violet
title: 12 Glossary
items:
- Space - pages of one team, mapped to a group
- Revision - one saved version of a page
- Attachment - a file stored beside a page
@@ -0,0 +1,33 @@
schema_version: 1
elements:
web:
type: frontend
label: Web App
sublabel: React
brand: react
description: >
Page editor, viewer and search box in the browser; talks
only to the API.
api:
type: backend
label: Wiki API
sublabel: REST
description: >
Pages, search, attachments and access checks; the only
writer of the database.
diagram: 0500-building-blocks/wiki-api.archify
db:
type: database
label: Database
sublabel: PostgreSQL
brand: postgresql
description: >
Pages, revisions, users and the full-text index; the
source of truth.
backup-job:
type: backend
label: Backup job
sublabel: nightly
description: >
Dumps the database at 02:00 and writes it to the backup
bucket; keeps 30 days.
@@ -0,0 +1,15 @@
schema_version: 1
elements:
engineer:
type: external
label: Engineer
sublabel: team member
description: >
Reads and edits pages, searches, attaches files; signs in
with the company account.
wiki-admin:
type: external
label: Wiki admin
sublabel: platform team
description: >
Runs the wiki, restores backups and manages spaces.
@@ -0,0 +1,24 @@
schema_version: 1
elements:
wiki:
type: cloud
label: Team Wiki
sublabel: the system
description: >
Everything we build and run; web app, API, database and
the backup job.
diagram: 0500-building-blocks/level-1.archify
idp:
type: security
label: Identity provider
sublabel: company SSO
description: >
The company identity provider; signs users in over OIDC
and owns team membership.
object-store:
type: external
label: Object storage
sublabel: S3-compatible
description: >
Company object storage; holds attachments and nightly
backups in separate buckets.
@@ -0,0 +1,36 @@
schema_version: 1
diagram_type: architecture
meta:
title: Business actors and services
output: business-services.html
components:
- ref: hr
pos: [40, 60]
size: [130, 60]
- ref: manager
pos: [40, 160]
size: [130, 60]
- ref: service-desk
pos: [40, 260]
size: [130, 60]
- ref: onboard
pos: [250, 160]
size: [150, 60]
diagram: 0100-business/onboard-employee.archify
- ref: onboarding-svc
pos: [480, 60]
size: [130, 60]
- ref: workplace-svc
pos: [480, 260]
size: [130, 60]
- ref: new-hire
pos: [690, 160]
size: [130, 60]
connections:
- { from: hr, to: onboard, label: assigned }
- { from: manager, to: onboard, label: assigned }
- { from: service-desk, to: onboard, label: assigned }
- { from: onboard, to: onboarding-svc, label: realizes }
- { from: onboard, to: workplace-svc, label: realizes }
- { from: onboarding-svc, to: new-hire, label: serves, variant: emphasis }
- { from: workplace-svc, to: new-hire, label: serves }
@@ -0,0 +1,25 @@
schema_version: 2
diagram_type: workflow
meta:
title: Onboard Employee
output: onboard-employee.html
viewBox: [1040, 660]
lanes:
- { id: hr-lane, label: HR }
- { id: manager-lane, label: Hiring manager }
- { id: desk-lane, label: IT service desk }
- { id: hire-lane, label: New hire }
mainPath: [contract-signed, record, request, create-accounts, issue-laptop, first-day]
nodes:
- { ref: contract-signed, lane: hr-lane, col: 0, width: 120 }
- { id: record, lane: hr-lane, col: 1, type: backend, label: Create record, tag: Business Process }
- { id: request, lane: manager-lane, col: 2, type: frontend, label: Request access, tag: Business Process }
- { ref: create-accounts, lane: desk-lane, col: 3, width: 120, decisions: [hr-system-drives-identity] }
- { ref: issue-laptop, lane: desk-lane, col: 4, decisions: [zero-touch-laptops] }
- { ref: first-day, lane: hire-lane, col: 5 }
edges:
- { from: contract-signed, to: record, label: triggers }
- { from: record, to: request, label: notifies }
- { from: request, to: create-accounts, label: ticket }
- { from: create-accounts, to: issue-laptop, label: then }
- { from: issue-laptop, to: first-day, label: hand over }
@@ -0,0 +1,44 @@
schema_version: 1
diagram_type: architecture
meta:
title: Application components, services and data
output: application-components.html
components:
- ref: provisioning-svc
pos: [250, 60]
size: [130, 60]
- ref: portal
pos: [460, 60]
size: [130, 60]
- ref: device-svc
pos: [670, 60]
size: [130, 60]
- ref: hr-system
pos: [40, 160]
size: [130, 60]
- ref: iam
pos: [250, 160]
size: [130, 60]
- ref: itsm
pos: [460, 160]
size: [130, 60]
- ref: mdm-app
pos: [670, 160]
size: [130, 60]
- ref: employee-record
pos: [40, 260]
size: [130, 60]
- ref: account
pos: [250, 260]
size: [130, 60]
- ref: asset
pos: [460, 260]
size: [130, 60]
connections:
- { from: iam, to: provisioning-svc, label: realizes }
- { from: itsm, to: portal, label: realizes }
- { from: mdm-app, to: device-svc, label: realizes }
- { from: hr-system, to: employee-record, label: writes, labelAt: [105, 234] }
- { from: iam, to: account, label: writes, labelAt: [315, 234] }
- { from: itsm, to: asset, label: writes, labelAt: [525, 234] }
- { from: mdm-app, to: asset, label: reads }
@@ -0,0 +1,29 @@
schema_version: 1
diagram_type: architecture
meta:
title: Application cooperation
output: application-cooperation.html
decisions: [hr-system-drives-identity]
components:
- ref: hr-system
pos: [40, 160]
size: [130, 60]
- ref: iam
pos: [250, 60]
size: [130, 60]
diagram: 0200-application/application-components.archify
- ref: itsm
pos: [250, 260]
size: [130, 60]
- ref: collab
pos: [460, 60]
size: [130, 60]
- ref: mdm-app
pos: [460, 260]
size: [130, 60]
connections:
- { from: hr-system, to: iam, label: joiner event, variant: emphasis, decisions: [hr-system-drives-identity] }
- { from: hr-system, to: itsm, label: laptop request }
- { from: iam, to: collab, label: provisions }
- { from: iam, to: mdm-app, label: users }
- { from: itsm, to: mdm-app, label: assigns }
@@ -0,0 +1,42 @@
schema_version: 1
diagram_type: architecture
meta:
title: Technology layer
output: infrastructure.html
decisions: [zero-touch-laptops]
components:
- ref: hr-saas
pos: [40, 60]
size: [130, 60]
- ref: sso
pos: [250, 60]
size: [130, 60]
- ref: directory
pos: [460, 60]
size: [130, 60]
- ref: itsm-saas
pos: [40, 160]
size: [130, 60]
- ref: mdm-saas
pos: [460, 160]
size: [130, 60]
- ref: laptop
pos: [460, 360]
size: [130, 60]
- ref: office-net
pos: [250, 360]
size: [130, 60]
boundaries:
- kind: region
label: SaaS tenants
wraps: [hr-saas, sso, directory, itsm-saas, mdm-saas]
- kind: security-group
label: Office
wraps: [laptop, office-net]
connections:
- { from: directory, to: sso, label: realizes }
- { from: sso, to: hr-saas, label: serves }
- { from: sso, to: itsm-saas, label: serves }
- { from: directory, to: mdm-saas, label: devices, labelAt: [525, 134] }
- { from: mdm-saas, to: laptop, label: manages, labelAt: [525, 234], variant: security, decisions: [zero-touch-laptops] }
- { from: laptop, to: office-net, label: connects }
@@ -0,0 +1,20 @@
schema_version: 1
decision:
id: hr-system-drives-identity
title: The HR system drives identity
status: accepted
date: 2026-09-08
elements: [hr-system, iam, account, employee-record]
diagrams: [0200-application/application-cooperation.archify, 0100-business/onboard-employee.archify]
context: >
Accounts were created by hand from emails sent by HR.
New hires waited days for access, and leavers kept
accounts for weeks after their last day.
decision: >
The HR system is the source of truth for joiners and
leavers. Identity management reads its events and
creates, changes and disables accounts automatically.
consequences: >
No account without an employee record. HR data quality
now decides access, so start dates must be right. The
service desk only handles exceptions.
@@ -0,0 +1,20 @@
schema_version: 1
decision:
id: zero-touch-laptops
title: Zero-touch enrollment for laptops
status: accepted
date: 2026-10-05
elements: [mdm-app, mdm-saas, laptop, device-svc, asset]
diagrams: [0300-technology/infrastructure.archify, 0100-business/onboard-employee.archify]
context: >
Every laptop was imaged by hand at the service desk,
which took half a day per device and left settings that
differed from one laptop to the next.
decision: >
Laptops are registered in device management at purchase.
They configure themselves at first sign-in over any
internet connection; the office network needs no VPN.
consequences: >
Laptops can be shipped straight to the new hire. A lost
laptop is wiped remotely. Setup depends on the MDM
tenant being reachable on the first day.
@@ -0,0 +1,59 @@
schema_version: 1
model:
id: hr-onboarding
name: HR Onboarding
description: >
An ArchiMate example: how a new employee is onboarded, from signed
contract to first day. Business, application and technology layers
map to folders; each element's ArchiMate type is shown as its tag.
diagram_type: architecture
meta:
title: HR Onboarding - layered view
output: main.html
components:
- ref: new-hire
pos: [40, 70]
size: [170, 60]
- ref: onboarding-svc
pos: [270, 70]
size: [170, 60]
- ref: onboard
pos: [500, 70]
size: [170, 60]
- ref: portal
pos: [40, 270]
size: [170, 60]
- ref: iam
pos: [270, 270]
size: [170, 60]
- ref: provisioning-svc
pos: [500, 270]
size: [170, 60]
- ref: office-net
pos: [40, 470]
size: [170, 60]
- ref: sso
pos: [270, 470]
size: [170, 60]
- ref: directory
pos: [500, 470]
size: [170, 60]
boundaries:
- kind: region
label: Business layer
wraps: [new-hire, onboarding-svc, onboard]
- kind: region
label: Application layer
wraps: [portal, iam, provisioning-svc]
- kind: region
label: Technology layer
wraps: [office-net, sso, directory]
connections:
- { from: onboarding-svc, to: new-hire, label: serves, variant: emphasis }
- { from: onboard, to: onboarding-svc, label: realizes }
- { from: portal, to: new-hire, label: serves }
- { from: provisioning-svc, to: onboard, label: serves }
- { from: iam, to: provisioning-svc, label: realizes }
- { from: office-net, to: portal, label: serves }
- { from: sso, to: iam, label: serves }
- { from: directory, to: sso, label: realizes }
@@ -0,0 +1,81 @@
schema_version: 1
elements:
hr-system:
type: backend
label: HR system
sublabel: App Component
tag: App Component
description: >
Holds employee records and contracts; the source of truth for joiners.
diagram: 0200-application/application-cooperation.archify
iam:
type: security
label: Identity mgmt
sublabel: App Component
tag: App Component
description: >
Creates and removes accounts from HR events; single sign-on for all apps.
diagram: 0200-application/application-components.archify
itsm:
type: backend
label: IT service mgmt
sublabel: App Component
tag: App Component
description: >
Tickets, requests and the asset inventory of the IT service desk.
mdm-app:
type: backend
label: Device mgmt
sublabel: App Component
tag: App Component
description: >
Enrolls laptops, pushes policies and apps, wipes lost devices.
collab:
type: backend
label: Mail and chat
sublabel: App Component
tag: App Component
description: >
Mailbox, calendar and chat; accounts provisioned by identity management.
provisioning-svc:
type: cloud
label: Provisioning
sublabel: App Service
tag: App Service
description: >
Account create, change and disable, triggered by the HR system.
device-svc:
type: cloud
label: Enrollment
sublabel: App Service
tag: App Service
description: >
Zero-touch enrollment; the laptop configures itself at first sign-in.
portal:
type: frontend
label: Service portal
sublabel: App Interface
tag: App Interface
description: >
Where managers request equipment and access for a new hire.
employee-record:
type: database
label: Employee record
sublabel: Data Object
tag: Data Object
description: >
Name, role, department, manager and start date of an employee.
account:
type: database
label: Account
sublabel: Data Object
tag: Data Object
description: >
User id, groups and licences of one person.
asset:
type: database
label: Asset
sublabel: Data Object
tag: Data Object
description: >
A laptop with serial number, owner and enrollment state.
@@ -0,0 +1,81 @@
schema_version: 1
elements:
new-hire:
type: external
label: New hire
sublabel: Business Actor
tag: Business Actor
description: >
The person joining the company; signs the contract and starts on day one.
hr:
type: external
label: HR officer
sublabel: Business Role
tag: Business Role
description: >
Prepares the contract and the employee record; owns the onboarding process.
manager:
type: external
label: Hiring manager
sublabel: Business Role
tag: Business Role
description: >
Requests access and equipment for the new hire and plans the first day.
service-desk:
type: external
label: IT service desk
sublabel: Business Role
tag: Business Role
description: >
Creates accounts, prepares the laptop and hands it over.
onboard:
type: backend
label: Onboard Employee
sublabel: Business Process
tag: Business Process
description: >
From signed contract to a productive first day.
diagram: 0100-business/onboard-employee.archify
onboarding-svc:
type: cloud
label: Onboarding
sublabel: Business Service
tag: Business Service
description: >
What the new hire gets from HR; contract, record, welcome and first day plan.
diagram: 0100-business/business-services.archify
workplace-svc:
type: cloud
label: Workplace setup
sublabel: Business Service
tag: Business Service
description: >
Accounts and a ready laptop, delivered by the IT service desk.
contract-signed:
type: messagebus
label: Contract signed
sublabel: Business Event
tag: Business Event
description: >
Starts the process; HR records the start date.
create-accounts:
type: backend
label: Create accounts
sublabel: Business Process
tag: Business Process
description: >
Service desk confirms the accounts the identity system provisioned.
issue-laptop:
type: backend
label: Issue laptop
sublabel: Business Process
tag: Business Process
description: >
An enrolled laptop is assigned and handed over.
first-day:
type: messagebus
label: First day
sublabel: Business Event
tag: Business Event
description: >
The new hire signs in for the first time; the process ends.
@@ -0,0 +1,53 @@
schema_version: 1
elements:
hr-saas:
type: cloud
label: HR tenant
sublabel: Node
tag: Node
description: >
The SaaS tenant that runs the HR system.
diagram: 0300-technology/infrastructure.archify
itsm-saas:
type: cloud
label: ITSM tenant
sublabel: Node
tag: Node
description: >
The SaaS tenant that runs IT service management.
mdm-saas:
type: cloud
label: MDM tenant
sublabel: Node
tag: Node
description: >
The SaaS tenant that runs device management.
directory:
type: backend
label: Directory
sublabel: System Software
tag: System Software
description: >
Cloud directory service; users, groups and devices.
diagram: 0300-technology/infrastructure.archify
sso:
type: security
label: Single sign-on
sublabel: Tech Service
tag: Tech Service
description: >
SAML and OIDC sign-in for every SaaS tenant, backed by the directory.
laptop:
type: frontend
label: Laptop
sublabel: Device
tag: Device
description: >
Company laptop, enrolled before it is handed over.
office-net:
type: messagebus
label: Office network
sublabel: Network
tag: Network
description: >
Wired and wireless network of the office; internet only, no VPN.
@@ -0,0 +1,31 @@
schema_version: 1
diagram_type: architecture
meta:
title: Business layer
output: business-layer.html
components:
- ref: customer
pos: [40, 160]
size: [130, 60]
- ref: insurant
pos: [250, 160]
size: [130, 60]
- ref: claim-registration
pos: [460, 60]
size: [150, 60]
- ref: claim-payment
pos: [460, 260]
size: [150, 60]
- ref: handle-claim
pos: [690, 160]
size: [150, 60]
- ref: claims-handler
pos: [920, 160]
size: [130, 60]
connections:
- { from: customer, to: insurant, label: assigned to }
- { from: claim-registration, to: insurant, label: serves, variant: emphasis }
- { from: claim-payment, to: insurant, label: serves }
- { from: handle-claim, to: claim-registration, label: realizes }
- { from: handle-claim, to: claim-payment, label: realizes }
- { from: claims-handler, to: handle-claim, label: assigned to }
@@ -0,0 +1,29 @@
schema_version: 2
diagram_type: workflow
meta:
title: Handle Claim
output: handle-claim.html
viewBox: [840, 534]
lanes:
- { id: insurant-lane, label: Insurant }
- { id: handler-lane, label: Claims handler }
- { id: systems-lane, label: Applications }
mainPath: [damage, register, accept, valuate, pay]
nodes:
- { id: damage, lane: insurant-lane, col: 0, type: external, label: Damage occurs, tag: Business Event }
- { ref: insurant, lane: insurant-lane, col: 5 }
- { id: register, lane: handler-lane, col: 1, type: backend, label: Register, tag: Business Process }
- { id: accept, lane: handler-lane, col: 2, type: security, label: Accept, sublabel: check coverage, tag: Business Process }
- { id: valuate, lane: handler-lane, col: 3, type: backend, label: Valuate, tag: Business Process }
- { id: pay, lane: handler-lane, col: 4, type: backend, label: Pay, tag: Business Process }
- { ref: claims-app, lane: systems-lane, col: 2 }
- { ref: payment, lane: systems-lane, col: 4 }
edges:
- { from: damage, to: register, label: report }
- { from: register, to: accept }
- { from: accept, to: valuate, label: covered }
- { from: valuate, to: pay, label: approved }
- { from: pay, to: insurant, label: paid }
- { from: accept, to: insurant, label: rejected, role: branch, variant: dashed }
- { from: claims-app, to: accept, label: serves, role: async }
- { from: payment, to: pay, label: serves, role: async }
@@ -0,0 +1,32 @@
schema_version: 1
diagram_type: architecture
meta:
title: Application cooperation
output: application-cooperation.html
components:
- ref: portal
pos: [40, 260]
size: [130, 60]
- ref: claims-app
pos: [250, 260]
size: [130, 60]
- ref: crm
pos: [250, 60]
size: [130, 60]
- ref: policy-admin
pos: [460, 160]
size: [130, 60]
decisions: [policies-stay-on-mainframe]
- ref: doc-mgmt
pos: [460, 360]
size: [130, 60]
decisions: [documents-in-object-storage]
- ref: payment
pos: [250, 460]
size: [130, 60]
connections:
- { from: portal, to: claims-app, label: claims, variant: emphasis }
- { from: crm, to: claims-app, label: customer, labelAt: [315, 134] }
- { from: policy-admin, to: claims-app, label: coverage, decisions: [policies-stay-on-mainframe] }
- { from: claims-app, to: doc-mgmt, label: documents }
- { from: claims-app, to: payment, label: payments, labelAt: [315, 334] }
@@ -0,0 +1,36 @@
schema_version: 1
diagram_type: architecture
meta:
title: Application structure
output: application-structure.html
components:
- ref: portal
pos: [40, 160]
size: [130, 60]
- ref: claims-admin
pos: [250, 160]
size: [130, 60]
- ref: claims-app
pos: [460, 160]
size: [130, 60]
- ref: claim
pos: [460, 360]
size: [130, 60]
- ref: crm
pos: [670, 60]
size: [130, 60]
- ref: customer-file
pos: [880, 60]
size: [130, 60]
- ref: policy-admin
pos: [670, 260]
size: [130, 60]
- ref: policy
pos: [880, 260]
size: [130, 60]
connections:
- { from: claims-admin, to: portal, label: serves, variant: emphasis }
- { from: claims-app, to: claims-admin, label: realizes }
- { from: claims-app, to: claim, label: accesses, labelAt: [525, 234] }
- { from: crm, to: customer-file, label: accesses }
- { from: policy-admin, to: policy, label: accesses }
@@ -0,0 +1,45 @@
schema_version: 1
diagram_type: architecture
meta:
title: Technology layer
output: infrastructure.html
components:
- ref: dmz
pos: [40, 160]
size: [130, 60]
- ref: doc-store
pos: [250, 60]
size: [130, 60]
decisions: [documents-in-object-storage]
- ref: app-servers
pos: [250, 160]
size: [130, 60]
- ref: db-service
pos: [250, 260]
size: [130, 60]
- ref: db-cluster
pos: [250, 360]
size: [130, 60]
- ref: mq
pos: [460, 160]
size: [130, 60]
- ref: mainframe
pos: [670, 160]
size: [130, 60]
decisions: [policies-stay-on-mainframe]
- ref: dc-lan
pos: [670, 360]
size: [130, 60]
boundaries:
- kind: region
label: Data centre
wraps: [doc-store, app-servers, db-service, db-cluster, mq, mainframe, dc-lan]
connections:
- { from: dmz, to: app-servers, label: HTTPS, variant: security }
- { from: app-servers, to: doc-store, label: S3 API }
- { from: db-service, to: app-servers, label: serves }
- { from: db-cluster, to: db-service, label: realizes }
- { from: app-servers, to: mq, label: MQ }
- { from: mq, to: mainframe, label: bridge }
- { from: db-cluster, to: dc-lan, label: LAN }
- { from: mainframe, to: dc-lan, label: LAN, labelAt: [735, 234] }
@@ -0,0 +1,20 @@
schema_version: 1
decision:
id: policies-stay-on-mainframe
title: Policies stay on the mainframe
status: accepted
date: 2026-09-08
elements: [policy-admin, mainframe, mq, claims-app]
diagrams: [0200-application/application-cooperation.archify, 0300-technology/infrastructure.archify]
context: >
The new claims application needs coverage data for every
claim. Policy administration has run on the mainframe for
twenty years and a migration was estimated at three years.
decision: >
Policy administration stays on the mainframe. The claims
application reads coverage through the message queue bridge
and never copies policies into its own database.
consequences: >
Accepting a claim depends on the mainframe being up during
office hours. The queue bridge must be kept and tested.
A later migration needs its own decision.
@@ -0,0 +1,20 @@
schema_version: 1
decision:
id: documents-in-object-storage
title: Claim documents in object storage
status: accepted
date: 2026-10-14
elements: [doc-mgmt, doc-store, claims-app]
diagrams: [0200-application/application-cooperation.archify, 0300-technology/infrastructure.archify]
context: >
Photos and expert reports were stored as blobs in the claims
database. The database grew by forty gigabytes a month and
backups no longer fitted the nightly window.
decision: >
Document management stores every file in the document store
and keeps only a reference in the claim. The claims
application never writes files itself.
consequences: >
Database backups shrink and stay fast. Documents get their own
retention rules. A claim and its files are restored from two
places after an incident.
@@ -0,0 +1,59 @@
schema_version: 1
model:
id: insurance-claims
name: Insurance Claims
description: >
ArchiMate example: how Halbrook Mutual handles claims. The business,
application and technology layers are mapped to folders, and each
element's ArchiMate type is shown as its tag.
diagram_type: architecture
meta:
title: Insurance Claims - layered view
output: main.html
components:
- ref: insurant
pos: [40, 70]
size: [170, 60]
- ref: claim-registration
pos: [270, 70]
size: [170, 60]
- ref: handle-claim
pos: [500, 70]
size: [170, 60]
- ref: portal
pos: [40, 270]
size: [170, 60]
- ref: claims-app
pos: [270, 270]
size: [170, 60]
- ref: claims-admin
pos: [500, 270]
size: [170, 60]
- ref: dmz
pos: [40, 470]
size: [170, 60]
- ref: app-servers
pos: [270, 470]
size: [170, 60]
- ref: db-cluster
pos: [500, 470]
size: [170, 60]
boundaries:
- kind: region
label: Business layer
wraps: [insurant, claim-registration, handle-claim]
- kind: region
label: Application layer
wraps: [portal, claims-app, claims-admin]
- kind: region
label: Technology layer
wraps: [dmz, app-servers, db-cluster]
connections:
- { from: claim-registration, to: insurant, label: serves, variant: emphasis }
- { from: handle-claim, to: claim-registration, label: realizes }
- { from: portal, to: insurant, label: serves }
- { from: claims-admin, to: handle-claim, label: serves }
- { from: claims-app, to: claims-admin, label: realizes }
- { from: dmz, to: portal, label: serves }
- { from: app-servers, to: claims-app, label: serves }
- { from: db-cluster, to: app-servers, label: serves }
@@ -0,0 +1,76 @@
schema_version: 1
elements:
portal:
type: frontend
label: Customer Portal
sublabel: App Interface
tag: App Interface
description: >
Web and mobile front end where customers report claims and
follow their status.
claims-admin:
type: cloud
label: Claims Admin
sublabel: App Service
tag: App Service
description: >
Register, look up and update claims; used by the portal and
by claims handlers.
claims-app:
type: backend
label: Claims App
sublabel: App Component
tag: App Component
description: >
Owns claims from registration to payment; the system of
record for claims.
diagram: 0200-application/application-structure.archify
crm:
type: backend
label: CRM
sublabel: App Component
tag: App Component
description: >
Customer contacts, addresses and the customer file.
policy-admin:
type: backend
label: Policy Admin
sublabel: App Component
tag: App Component
description: >
Policies, coverage and premiums; runs on the mainframe.
doc-mgmt:
type: backend
label: Document Mgmt
sublabel: App Component
tag: App Component
description: >
Scanned letters, photos and expert reports attached to claims.
payment:
type: backend
label: Payment
sublabel: App Component
tag: App Component
description: >
Sends approved claim payments to the bank in daily batches.
claim:
type: database
label: Claim
sublabel: Data Object
tag: Data Object
description: >
One reported damage, its valuation and its payment status.
policy:
type: database
label: Policy
sublabel: Data Object
tag: Data Object
description: >
Coverage, deductible and term of an insurance contract.
customer-file:
type: database
label: Customer File
sublabel: Data Object
tag: Data Object
description: >
Everything known about a customer; contacts, policies, claims.
@@ -0,0 +1,50 @@
schema_version: 1
elements:
customer:
type: external
label: Customer
sublabel: Business Actor
tag: Business Actor
description: >
A person or small business holding one or more policies
with Halbrook Mutual.
insurant:
type: external
label: Insurant
sublabel: Business Role
tag: Business Role
description: >
The role a customer plays while a claim is open; reports
damage and receives the payment.
claims-handler:
type: external
label: Claims handler
sublabel: Business Role
tag: Business Role
description: >
Employee of the claims department who accepts, valuates
and approves claims.
handle-claim:
type: backend
label: Handle Claim
sublabel: Business Process
tag: Business Process
description: >
Register, accept, valuate and pay a claim; the core process
of the claims department.
diagram: 0100-business/handle-claim.archify
claim-registration:
type: frontend
label: Claim Registration
sublabel: Business Service
tag: Business Service
description: >
Lets an insurant report damage by phone, mail or the
customer portal.
claim-payment:
type: frontend
label: Claim Payment
sublabel: Business Service
tag: Business Service
description: >
Pays the valuated amount to the insurant or a repairer.
@@ -0,0 +1,62 @@
schema_version: 1
elements:
app-servers:
type: backend
label: App Servers
sublabel: Node
tag: Node
description: >
Two virtual machine hosts running the claims app, CRM and the
portal back end.
diagram: 0300-technology/infrastructure.archify
db-cluster:
type: database
label: DB Cluster
sublabel: Node
brand: postgresql
tag: Node
description: >
Primary and standby database servers for claims and CRM data.
doc-store:
type: database
label: Document Store
sublabel: System Software
tag: System Software
description: >
Object storage for scans and photos; written by document
management only.
mainframe:
type: backend
label: Mainframe
sublabel: Node
tag: Node
description: >
Hosts policy administration and the nightly payment batch.
mq:
type: messagebus
label: Message Queue
sublabel: System Software
tag: System Software
description: >
Queue manager between the app servers and the mainframe.
db-service:
type: cloud
label: DB Service
sublabel: Tech Service
tag: Tech Service
description: >
Managed SQL access with backups, offered to all applications.
dc-lan:
type: messagebus
label: Data Centre LAN
sublabel: Network
tag: Network
description: >
Internal network linking app servers, databases and mainframe.
dmz:
type: security
label: DMZ Gateway
sublabel: Network
tag: Network
description: >
Reverse proxy and firewall between the internet and the LAN.
@@ -0,0 +1,34 @@
schema_version: 1
diagram_type: architecture
meta:
title: Business actors and services
output: business-services.html
components:
- ref: shipper
pos: [40, 60]
size: [130, 60]
- ref: recipient
pos: [40, 260]
size: [130, 60]
- ref: delivery-service
pos: [250, 60]
size: [130, 60]
- ref: tracking-service
pos: [250, 260]
size: [130, 60]
- ref: deliver-shipment
pos: [460, 160]
size: [130, 60]
- ref: driver
pos: [670, 60]
size: [130, 60]
- ref: customer-service
pos: [670, 260]
size: [130, 60]
connections:
- { from: delivery-service, to: shipper, label: serves, variant: emphasis }
- { from: tracking-service, to: recipient, label: serves }
- { from: deliver-shipment, to: delivery-service, label: realizes }
- { from: deliver-shipment, to: tracking-service, label: realizes }
- { from: driver, to: deliver-shipment, label: assigned to }
- { from: customer-service, to: tracking-service, label: assigned to }
@@ -0,0 +1,25 @@
schema_version: 2
diagram_type: workflow
meta:
title: Deliver Shipment
output: deliver-shipment.html
viewBox: [840, 534]
lanes:
- { id: customer-lane, label: Shipper and recipient }
- { id: driver-lane, label: Driver }
- { id: hub-lane, label: Hub }
mainPath: [book, pick-up, sort, line-haul, last-mile, pod-step]
nodes:
- { id: book, lane: customer-lane, col: 0, type: external, label: Book shipment, tag: Business Event }
- { id: pick-up, lane: driver-lane, col: 1, type: backend, label: Pick up, tag: Business Process }
- { id: sort, lane: hub-lane, col: 2, type: backend, label: Sort at hub, tag: Business Process }
- { id: line-haul, lane: hub-lane, col: 3, type: backend, label: Line haul, tag: Business Process }
- { id: last-mile, lane: driver-lane, col: 4, type: backend, label: Last mile, tag: Business Process }
- { id: pod-step, lane: customer-lane, col: 5, type: security, label: Delivered, sublabel: proof of delivery, tag: Business Process }
edges:
- { from: book, to: pick-up, label: triggers }
- { from: pick-up, to: sort, label: parcel }
- { from: sort, to: line-haul, label: trailer }
- { from: line-haul, to: last-mile, label: route }
- { from: last-mile, to: pod-step, label: hand over }
- { from: last-mile, to: sort, label: not home, role: return, variant: dashed }
@@ -0,0 +1,32 @@
schema_version: 1
diagram_type: architecture
meta:
title: Application cooperation
output: application-cooperation.html
decisions: [edi-gateway-in-front-of-tms]
components:
- ref: shipper
pos: [40, 260]
size: [130, 60]
- ref: edi-gateway
pos: [250, 260]
size: [130, 60]
decisions: [edi-gateway-in-front-of-tms]
- ref: tms
pos: [460, 260]
size: [130, 60]
- ref: driver-app
pos: [460, 60]
size: [130, 60]
- ref: telematics
pos: [460, 460]
size: [130, 60]
- ref: tracking-portal
pos: [670, 260]
size: [130, 60]
connections:
- { from: shipper, to: edi-gateway, label: bookings, variant: emphasis }
- { from: edi-gateway, to: tms, label: shipments, variant: emphasis }
- { from: driver-app, to: tms, label: scans, labelAt: [525, 134], decisions: [broker-for-tracking-events] }
- { from: telematics, to: tms, label: positions, decisions: [broker-for-tracking-events] }
- { from: tms, to: tracking-portal, label: status }
@@ -0,0 +1,34 @@
schema_version: 1
diagram_type: architecture
meta:
title: Application services and data
output: application-services.html
components:
- ref: tracking-portal
pos: [40, 160]
size: [130, 60]
- ref: tracking-svc
pos: [250, 160]
size: [130, 60]
- ref: tms
pos: [460, 160]
size: [130, 60]
- ref: driver-app
pos: [670, 160]
size: [130, 60]
- ref: shipment
pos: [250, 360]
size: [130, 60]
- ref: tracking-event
pos: [460, 360]
size: [130, 60]
- ref: pod
pos: [670, 360]
size: [130, 60]
connections:
- { from: tracking-svc, to: tracking-portal, label: serves, variant: emphasis }
- { from: tms, to: tracking-svc, label: realizes }
- { from: driver-app, to: tms, label: uses }
- { from: tms, to: shipment, label: accesses }
- { from: tms, to: tracking-event, label: accesses, labelAt: [525, 234] }
- { from: driver-app, to: pod, label: accesses, labelAt: [735, 234] }
@@ -0,0 +1,44 @@
schema_version: 1
diagram_type: architecture
meta:
title: Technology infrastructure
output: infrastructure.html
decisions: [broker-for-tracking-events]
components:
- ref: handhelds
pos: [40, 160]
size: [130, 60]
- ref: telematics-units
pos: [40, 360]
size: [130, 60]
- ref: mobile-network
pos: [250, 260]
size: [130, 60]
- ref: cloud-cluster
pos: [460, 160]
size: [130, 60]
- ref: message-broker
pos: [460, 360]
size: [130, 60]
decisions: [broker-for-tracking-events]
- ref: db-server
pos: [670, 160]
size: [130, 60]
- ref: event-streaming
pos: [670, 360]
size: [130, 60]
boundaries:
- kind: region
label: Field devices
wraps: [handhelds, telematics-units]
- kind: security-group
label: Cloud platform
wraps: [cloud-cluster, message-broker, db-server, event-streaming]
connections:
- { from: handhelds, to: mobile-network, label: LTE }
- { from: telematics-units, to: mobile-network, label: LTE }
- { from: mobile-network, to: cloud-cluster, label: HTTPS, variant: security }
- { from: mobile-network, to: message-broker, label: MQTT, variant: security }
- { from: cloud-cluster, to: message-broker, label: hosts, labelAt: [525, 234] }
- { from: cloud-cluster, to: db-server, label: SQL }
- { from: message-broker, to: event-streaming, label: realizes }
@@ -0,0 +1,20 @@
schema_version: 1
decision:
id: broker-for-tracking-events
title: A message broker for tracking events
status: accepted
date: 2026-09-08
elements: [message-broker, telematics, driver-app, tms, tracking-event]
diagrams: [0300-technology/infrastructure.archify, 0200-application/application-cooperation.archify]
context: >
Handhelds and telematics units posted every scan and position
straight to the TMS. At evening peaks the TMS fell behind and
dropped events when a device retried over a weak signal.
decision: >
Devices publish tracking events to a Kafka message broker on
the cloud cluster. The TMS and telematics read the topics at
their own pace; events are kept for seven days for replay.
consequences: >
Spikes no longer reach the TMS. Status on the tracking portal
can lag by seconds. Consumers must handle duplicate events,
and the broker is one more platform service to run.
@@ -0,0 +1,20 @@
schema_version: 1
decision:
id: edi-gateway-in-front-of-tms
title: An EDI gateway in front of the TMS
status: accepted
date: 2026-09-22
elements: [edi-gateway, tms, shipper, shipment]
diagrams: [0200-application/application-cooperation.archify]
context: >
Large shippers send bookings as EDI files in three dialects.
Each dialect was parsed inside the TMS, so a new shipper meant
a TMS release.
decision: >
A separate EDI gateway receives, validates and translates
every booking into one shipment message for the TMS. It also
sends status messages back in the shipper's dialect.
consequences: >
New shippers are onboarded in the gateway alone. The TMS has
one booking interface. A rejected booking now fails at the
gateway, which customer service must be able to see.
@@ -0,0 +1,17 @@
schema_version: 1
decision:
id: photo-proof-of-delivery
title: Photo as proof of delivery
status: proposed
date: 2026-10-27
elements: [pod, driver-app, handhelds]
diagrams: [0200-application/application-services.archify, 0100-business/deliver-shipment.archify]
context: >
Most parcels are left at the door when nobody signs. Disputes
about such parcels take customer service days to settle.
decision: >
The driver app takes a photo of every parcel left without a
signature and stores it with the proof of delivery.
consequences: >
Disputes are settled from the photo. Photos are personal data
and are deleted after 90 days. Uploads add mobile data cost.
@@ -0,0 +1,59 @@
schema_version: 1
model:
id: logistics-tracking
name: Logistics Tracking
description: >
An ArchiMate example: a parcel carrier lets shippers and recipients
track shipments. Business, application and technology layers are
mapped to folders; each element's ArchiMate type is shown as its tag.
diagram_type: architecture
meta:
title: Logistics Tracking - layered view
output: main.html
components:
- ref: recipient
pos: [40, 70]
size: [170, 60]
- ref: tracking-service
pos: [270, 70]
size: [170, 60]
- ref: deliver-shipment
pos: [500, 70]
size: [170, 60]
- ref: tracking-portal
pos: [40, 270]
size: [170, 60]
- ref: tms
pos: [270, 270]
size: [170, 60]
- ref: tracking-svc
pos: [500, 270]
size: [170, 60]
- ref: mobile-network
pos: [40, 470]
size: [170, 60]
- ref: cloud-cluster
pos: [270, 470]
size: [170, 60]
- ref: event-streaming
pos: [500, 470]
size: [170, 60]
boundaries:
- kind: region
label: Business layer
wraps: [recipient, tracking-service, deliver-shipment]
- kind: region
label: Application layer
wraps: [tracking-portal, tms, tracking-svc]
- kind: region
label: Technology layer
wraps: [mobile-network, cloud-cluster, event-streaming]
connections:
- { from: tracking-service, to: recipient, label: serves, variant: emphasis }
- { from: deliver-shipment, to: tracking-service, label: realizes }
- { from: tracking-portal, to: recipient, label: serves }
- { from: tracking-svc, to: deliver-shipment, label: serves }
- { from: tms, to: tracking-svc, label: realizes }
- { from: mobile-network, to: tracking-portal, label: serves }
- { from: cloud-cluster, to: tms, label: serves }
- { from: event-streaming, to: tracking-svc, label: serves }
@@ -0,0 +1,67 @@
schema_version: 1
elements:
tms:
type: backend
label: TMS
sublabel: App Component
tag: App Component
description: >
Owns shipments, routes and hub plans; the source of truth for status.
diagram: 0200-application/application-cooperation.archify
tracking-portal:
type: frontend
label: Tracking portal
sublabel: App Component
tag: App Component
description: >
Public site where shippers and recipients follow a shipment.
edi-gateway:
type: security
label: EDI gateway
sublabel: App Component
tag: App Component
description: >
Receives booking messages from large shippers and returns status.
driver-app:
type: frontend
label: Driver app
sublabel: App Component
tag: App Component
description: >
Scans parcels at every hand-over and captures proof of delivery.
telematics:
type: backend
label: Telematics
sublabel: App Component
tag: App Component
description: >
Collects vehicle positions and turns them into arrival estimates.
tracking-svc:
type: backend
label: Tracking service
sublabel: App Service
tag: App Service
description: >
Status and arrival estimate of one shipment, offered by the TMS.
diagram: 0200-application/application-services.archify
shipment:
type: database
label: Shipment
sublabel: Data Object
tag: Data Object
description: >
One booked parcel with sender, recipient, route and status.
tracking-event:
type: database
label: Tracking event
sublabel: Data Object
tag: Data Object
description: >
A scan or position report; the history of a shipment.
pod:
type: database
label: Proof of delivery
sublabel: Data Object
tag: Data Object
description: >
Signature or photo taken at the door; closes the shipment.
@@ -0,0 +1,54 @@
schema_version: 1
elements:
shipper:
type: external
label: Shipper
sublabel: Business Actor
tag: Business Actor
description: >
Books shipments, hands parcels over at pick-up and follows
them until delivery.
recipient:
type: external
label: Recipient
sublabel: Business Actor
tag: Business Actor
description: >
Receives the parcel; tracks it and may reschedule delivery.
driver:
type: external
label: Driver
sublabel: Business Role
tag: Business Role
description: >
Collects parcels, drives the routes and captures proof of delivery.
customer-service:
type: external
label: Customer service
sublabel: Business Role
tag: Business Role
description: >
Answers shipper and recipient questions about late or lost parcels.
deliver-shipment:
type: backend
label: Deliver Shipment
sublabel: Business Process
tag: Business Process
description: >
Pick up, sort at hub, line haul, last mile and proof of delivery.
diagram: 0100-business/deliver-shipment.archify
delivery-service:
type: backend
label: Parcel delivery
sublabel: Business Service
tag: Business Service
description: >
What the carrier sells; a parcel collected and delivered on time.
diagram: 0100-business/business-services.archify
tracking-service:
type: backend
label: Shipment tracking
sublabel: Business Service
tag: Business Service
description: >
Where a parcel is and when it arrives, for shippers and recipients.
@@ -0,0 +1,55 @@
schema_version: 1
elements:
cloud-cluster:
type: cloud
label: Cloud cluster
sublabel: Node
brand: kubernetes
tag: Node
description: >
Container cluster that runs the TMS, portal, EDI gateway and telematics.
diagram: 0300-technology/infrastructure.archify
message-broker:
type: messagebus
label: Message broker
sublabel: System Software
brand: apache-kafka
tag: System Software
description: >
Carries tracking events from devices and vehicles to the applications.
db-server:
type: database
label: Database server
sublabel: System Software
brand: postgresql
tag: System Software
description: >
Stores shipments, tracking events and proofs of delivery.
event-streaming:
type: backend
label: Event streaming
sublabel: Tech Service
tag: Tech Service
description: >
Durable, ordered topics that applications publish to and read from.
handhelds:
type: external
label: Handhelds
sublabel: Device
tag: Device
description: >
Scanners carried by drivers and hub staff; run the driver app.
telematics-units:
type: external
label: Telematics units
sublabel: Device
tag: Device
description: >
GPS boxes in vans and trucks; report position every minute.
mobile-network:
type: security
label: Mobile network
sublabel: Network
tag: Network
description: >
Carrier-grade mobile network that links devices to the cluster.
+17 -9
View File
@@ -5,7 +5,7 @@ import path from 'node:path';
import { test } from 'node:test'; import { test } from 'node:test';
import { parse } from 'yaml'; import { parse } from 'yaml';
import { validate } from '../src/archify/cli'; import { validate } from '../src/archify/cli';
import { MODEL_EXAMPLES, mainFromExample } from '../src/language/examples'; import { EXAMPLE_PREFIX, MODEL_EXAMPLES, mainFromExample } from '../src/language/examples';
import { MAIN_FILE, REGISTRY_EXT, hasDiagramBody, mergeRegistries, parseRegistry, readModelMeta, resolveRefs } from '../src/model/format'; import { MAIN_FILE, REGISTRY_EXT, hasDiagramBody, mergeRegistries, parseRegistry, readModelMeta, resolveRefs } from '../src/model/format';
import { DECISION_EXT, mergeDecisions, parseDecision, validateDecisions } from '../src/model/decisions'; import { DECISION_EXT, mergeDecisions, parseDecision, validateDecisions } from '../src/model/decisions';
import { readDiagramSource } from '../src/sources/detect'; import { readDiagramSource } from '../src/sources/detect';
@@ -26,13 +26,21 @@ function walk(dir: string, rel = ''): string[] {
}); });
} }
test('every example folder is listed, every listed example exists, labels are the three C4 examples', () => { // The folders each framework's examples must have, besides main.archify, registry/ and decisions/.
const FRAMEWORK_FOLDERS: Record<string, string[]> = {
c4: ['0100-context', '0200-containers', '0300-components'],
arc42: ['0300-context', '0500-building-blocks', '0600-runtime', '0700-deployment'],
archimate: ['0100-business', '0200-application', '0300-technology'],
};
test('every example folder is listed, every listed example exists, three examples per framework', () => {
const folders = fs.readdirSync(examples, { withFileTypes: true }).filter((e) => e.isDirectory()).map((e) => e.name).sort(); const folders = fs.readdirSync(examples, { withFileTypes: true }).filter((e) => e.isDirectory()).map((e) => e.name).sort();
assert.deepEqual(MODEL_EXAMPLES.map((e) => e.folder).sort(), folders); assert.deepEqual(MODEL_EXAMPLES.map((e) => e.folder).sort(), folders);
assert.deepEqual( for (const [framework, label] of [['c4', 'C4'], ['arc42', 'arc42'], ['archimate', 'ArchiMate']]) {
MODEL_EXAMPLES.map((e) => e.label), const listed = MODEL_EXAMPLES.filter((e) => e.folder.startsWith(`${framework}-`));
['C4 - Standard Web App', 'C4 - E-Shop', 'C4 - Event-Based Integration'], assert.equal(listed.length, 3, framework);
); for (const e of listed) assert.ok(e.label.startsWith(`${label} - `), e.label);
}
}); });
for (const example of MODEL_EXAMPLES) { for (const example of MODEL_EXAMPLES) {
@@ -45,7 +53,7 @@ for (const example of MODEL_EXAMPLES) {
assert.ok(files.includes(MAIN_FILE)); assert.ok(files.includes(MAIN_FILE));
const main = parse(read(MAIN_FILE)); const main = parse(read(MAIN_FILE));
const meta = readModelMeta(main); const meta = readModelMeta(main);
assert.equal(meta?.id, example.folder.replace(/^c4-/, '')); assert.equal(meta?.id, example.folder.replace(EXAMPLE_PREFIX, ''));
assert.ok(meta?.name && meta.description); assert.ok(meta?.name && meta.description);
assert.ok(hasDiagramBody(main)); assert.ok(hasDiagramBody(main));
@@ -68,9 +76,9 @@ for (const example of MODEL_EXAMPLES) {
assert.deepEqual(validateDecisions(decisions, registry, diagramPaths), []); assert.deepEqual(validateDecisions(decisions, registry, diagramPaths), []);
for (const d of decisions.decisions.values()) assert.ok(d.title && d.status && d.context && d.decision && d.consequences, `${d.id} is complete`); for (const d of decisions.decisions.values()) assert.ok(d.title && d.status && d.context && d.decision && d.consequences, `${d.id} is complete`);
// Diagrams: the three C4 levels, each resolving without problems and valid for Archify. // Diagrams: the framework's folders, each diagram resolving without problems and valid for Archify.
const diagrams = files.filter((f) => f.endsWith('.archify')).sort(); const diagrams = files.filter((f) => f.endsWith('.archify')).sort();
for (const level of ['0100-context', '0200-containers', '0300-components']) { for (const level of FRAMEWORK_FOLDERS[example.folder.split('-')[0]]) {
assert.ok(diagrams.some((d) => d.startsWith(`${level}/`)), `missing ${level}`); assert.ok(diagrams.some((d) => d.startsWith(`${level}/`)), `missing ${level}`);
} }
const used = new Set<string>(); const used = new Set<string>();
+5 -1
View File
@@ -449,7 +449,11 @@ export async function run(): Promise<void> {
window.showQuickPick = originalPick; window.showQuickPick = originalPick;
window.showInputBox = originalInput; window.showInputBox = originalInput;
} }
assert.deepEqual(labels, ['C4 - Standard Web App', 'C4 - E-Shop', 'C4 - Event-Based Integration']); assert.deepEqual(labels, [
'C4 - Standard Web App', 'C4 - E-Shop', 'C4 - Event-Based Integration',
'arc42 - Cinema Tickets', 'arc42 - IoT Telemetry', 'arc42 - Team Wiki',
'ArchiMate - Insurance Claims', 'ArchiMate - HR Onboarding', 'ArchiMate - Logistics Tracking',
]);
assert.match(String(existingNameMessage), /already exists/); assert.match(String(existingNameMessage), /already exists/);
const folder = vscode.Uri.joinPath(parent, 'shop-two'); const folder = vscode.Uri.joinPath(parent, 'shop-two');
const main = vscode.Uri.joinPath(folder, 'main.archify'); const main = vscode.Uri.joinPath(folder, 'main.archify');