A design spec your agents can build from and your team can actually review.
archspec replaces the design doc and the architecture diagram with one compact YAML
file: systems, components, connections, data and the step-by-step flow. People review it as
diagrams, a checker catches design mistakes before any code exists, and each coding agent gets
a precise brief for the step it builds.
Spec-driven workflows have agents write requirements, plans and task lists as
pages of prose. It reads well, but it is long, nothing checks it, and reviewers end up skimming.
In archspec the design is data, so it can be validated, diffed line by line, drawn as diagrams and
sliced into exactly what one agent needs.
Prose specMarkdown, per feature
…and the next page. Reviewed by reading; ambiguity is found in code review.
archspeconline-shop.spec.yaml, step S6
- id: S6
via: storefront_to_order-processing-api
call: orders.CreateOrder
description: Stores the order and queues it for fulfilment, in one transaction.
errors: { ALREADY_EXISTS: returns the order already stored for that idempotency key }
request:
build: CreateOrderRequest
map:
customerId: request.customerId
idempotencyKey: headers.Idempotency-Key
itemCount: cart.item_count
totalCents: cart.total_cents
checked: 0 errorsdrawn at 3 levelsbrief: archspec task S6
Prose spec
archspec
What reviewers read
Pages of text per feature
A YAML diff, the diagrams and the findings
Precision
"should", "usually", "a few seconds"
Types, fields, ports, auth and timeouts that 33 rules check
What an agent gets
The whole document in its context
A brief for one step: its payloads, mappings, tables and errors
Feedback while drafting
Review comments, after the code exists
Findings with line numbers, as text, JSON or SARIF
Diagrams
Drawn separately, if at all
Generated from the spec, at every level
Staying current
Drifts from the code
Lives in the repo; CI checks it on every pull request
Draft
You or your agent write the YAML. Start with the business flow and add detail only where it matters.
Check and review
The checker reports mistakes with line numbers. Reviewers read the diagrams and the diff, and record decisions in the spec.
Build
Hand each agent the brief for its step: what it receives, what it writes, what it sends and how it handles errors.
Keep in sync
The spec stays next to the code. CI checks it on every pull request, so the design and the build don't drift apart.
See it work
What changes when the design is a spec.
01
Start with the business, then drill down.
Begin with people and systems, in words a business stakeholder recognises. Open a system to
see the services, queues and databases inside it; open a service to see its endpoints and
what it owns. Every level comes from the same file, so they never disagree.
One diagram per level, drawn for you
Lines roll up: one arrow between systems stands for every connection inside
Links with no design under them yet are flagged, so you see what is left to do
Go as deep as you need: stop at a business sketch, map the components, or specify every endpoint and payload
The business view: who uses which system.
02
One file, every view your reviewers ask for.
Architects get diagrams, security gets the ports-and-protocols matrix with data
sensitivity, developers get an API reference and DDL, and coding agents get a brief for the
one step they are building. All generated, all in sync.
Export SVG, PNG, Markdown, CSV, OpenAPI, Mermaid and LikeC4
Briefs carry exactly the payloads, mappings and tables a step needs
Conn
From → To
Direction
Mode
Protocol
Port
In transit
At rest
Auth
Boundary
Timeout
Data
customer_to_storefront
customer → storefront
inbound
sync
HTTPS
443
TLS1.2+
—
oauth2-jwt
internet->dmz
5s
CartUpdate: What is in the customer's cart; CartSaved: The saved cart's id; CheckoutRequest: The cart the customer is checking out; CreateOrderResponse: Order id and status [PII]
storefront_to_carts_db
storefront → carts_db
internal
sync
Redis
6379
TLS1.2+
KMS
auth-token
dmz->private
50ms
carts_db.carts: Open carts, kept for 7 days [PII]
storefront_to_order-processing-api
storefront → order-processing-api
internal
sync
gRPC
443
TLS1.3 mTLS
—
mtls-identity
dmz->private
2s
CreateOrderRequest: Order built from the cart; CreateOrderResponse: Order id and status [PII]
order-processing-api_to_orders_db
order-processing-api → orders_db
internal
sync
PostgreSQL
5432
TLS1.2+
KMS
iam-db-auth
—
2s
orders_db.orders: Orders; orders_db.outbox: Events waiting to be published [PII]
outbox-relay_to_orders_db
outbox-relay → orders_db
internal
sync
PostgreSQL
5432
TLS1.2+
KMS
iam-db-auth
—
2s
orders_db.outbox: Events waiting to be published [PII]
outbox-relay_to_order-events
outbox-relay → order-events
internal
async / at-least-once
AWS-SQS
443
TLS1.2+
SSE-KMS
iam-role
—
—
OrderPlacedEvent: Placed order, for fulfilment [PII]
order-events_to_fulfillment-worker
order-events → fulfillment-worker
internal
async / at-least-once
AWS-SQS
443
TLS1.2+
SSE-KMS
iam-role
—
30s
OrderPlacedEvent: Placed order, for fulfilment [PII]
fulfillment-worker_to_fulfillment_db
fulfillment-worker → fulfillment_db
internal
sync
PostgreSQL
5432
TLS1.2+
KMS
secrets-manager
—
2s
fulfillment_db.processed_events: Dedupe keys for consumed events; fulfillment_db.shipments: Shipments to pick and pack [PII]
Storefront API (storefront)
PUT /cart (S1)
Saves the customer's cart, replacing what was there.
Called by customer via customer_to_storefront: HTTPS 443, auth oauth2-jwt, crosses internet->dmz, timeout 5s
Request:
CartUpdate: What is in the customer's cart
Field
Type
Required
Rules
Data
Description
cartId
uuid
yes
Chosen by the browser when the cart is first saved
customerId
uuid
yes
PII
The signed-in customer
itemCount
int
yes
min 0
Number of items; 0 empties the cart
totalCents
int
yes
min 0
Cart total in cents
Response 200:
CartSaved: The saved cart's id
Field
Type
Required
Rules
Data
Description
cartId
uuid
yes
Error 401: missing or expired token
Error 422: itemCount or totalCents is negative
Implementation: S2 TX: INSERT carts
POST /checkout (S4)
Turns the saved cart into an order. Safe to retry with the same Idempotency-Key.
Called by customer via customer_to_storefront: HTTPS 443, auth oauth2-jwt, crosses internet->dmz, timeout 5s
Header Idempotency-Key: required
Request:
CheckoutRequest: The cart the customer is checking out
Field
Type
Required
Rules
Data
Description
cartId
uuid
yes
The cart saved with PUT /cart
customerId
uuid
yes
PII
Must match the token's subject
Response 201:
CreateOrderResponse: Order id and status
Field
Type
Required
Rules
Data
Description
orderId
uuid
yes
Use it to track the order
status
string
yes
one of PLACED
Error 401: missing or expired token
Error 404: no saved cart with that id
Error 409: the Idempotency-Key was used for a different cart
order-processing-api: orders.CreateOrder (S6 via storefront_to_order-processing-api) with CreateOrderRequest
# Task: implement S6 of `online-shop`
You are implementing part of the system described in `online-shop.spec.yaml`. That spec is the source of truth:
do not change contracts, tables or connections in code. If the spec looks wrong or incomplete, stop and
propose a change to the spec instead of working around it.
**Component(s) you are changing:** storefront
## Steps to implement
```yaml
- id: S6
via: storefront_to_order-processing-api
call: orders.CreateOrder
description: Stores the order and queues it for fulfilment, in one transaction.
errors:
ALREADY_EXISTS: returns the order already stored for that idempotency key
request:
build: CreateOrderRequest
map:
customerId: request.customerId
idempotencyKey: headers.Idempotency-Key
itemCount: cart.item_count
totalCents: cart.total_cents
```
## Surrounding steps (context only; already implemented elsewhere)
```yaml
- id: S4
via: customer_to_storefront
call: POST /checkout
description: Turns the saved cart into an order. Safe to retry with the same Idempotency-Key.
headers:
Idempotency-Key: required
request: CheckoutRequest
errors:
"401": missing or expired token
"404": no saved cart with that id
"409": the Idempotency-Key was used for a different cart
- id: S5
via: storefront_to_carts_db
transaction:
- select: carts_db.carts
as: cart
where: id = request.cartId
- id: S7
via: order-processing-api_to_orders_db
transaction:
- insert: orders_db.orders
as: order
map:
id: $uuid
customer_id: request.customerId
status: "'PLACED'"
item_count: request.itemCount
total_cents: request.totalCents
idempotency_key: request.idempotencyKey
on_conflict:
idempotency_key: return the existing order
- insert: orders_db.outbox
as: outbox
map:
id: $uuid
aggregate_id: order.id
event_type: "'order.placed'"
payload:
build: OrderPlacedEvent
map:
eventId: outbox.id
eventType: "'order.placed'"
schemaVersion: "1"
orderId: order.id
customerId: order.customer_id
itemCount: order.item_count
- id: S8
via: storefront_to_order-processing-api
respond:
status: 200
body:
build: CreateOrderResponse
map:
orderId: order.id
status: order.status
- id: S9
via: customer_to_storefront
respond:
status: 201
body: CreateOrderResponse
```
## Connections
```yaml
storefront_to_order-processing-api:
from: storefront
to: order-processing-api
type: internal-grpc
auth: mtls-identity
timeout: 2s
deadlinePropagation: true
carries:
- CreateOrderRequest
- CreateOrderResponse
customer_to_storefront:
from: customer
to: storefront
type: https-rest
auth: oauth2-jwt
boundary: internet->dmz
edgeProtection: WAF managed rules, 20 req/s per customer
timeout: 5s
retryPolicy: client retries with the same Idempotency-Key
carries:
- CartUpdate
- CartSaved
- CheckoutRequest
- CreateOrderResponse
storefront_to_carts_db:
from: storefront
to: carts_db
type: redis
auth: auth-token
timeout: 50ms
order-processing-api_to_orders_db:
from: order-processing-api
to: orders_db
type: postgres
auth: iam-db-auth
statementTimeout: 2s
connectionPool: 20
```
Connection types (platform standards):
```yaml
https-rest:
mode: sync
protocol: HTTPS
port: 443
encryption:
inTransit: TLS1.2+
auth:
- oauth2-jwt
- api-key
requires:
- timeout
- retryPolicy
internal-grpc:
mode: sync
protocol: gRPC
port: 443
encryption:
inTransit: TLS1.3
mTLS: true
auth:
- mtls-identity
requires:
- timeout
- deadlinePropagation
postgres:
mode: sync
protocol: PostgreSQL
port: 5432
encryption:
inTransit: TLS1.2+
atRest: KMS
auth:
- iam-db-auth
- secrets-manager
requires:
- statementTimeout
- connectionPool
redis:
mode: sync
protocol: Redis
port: 6379
encryption:
inTransit: TLS1.2+
atRest: KMS
auth:
- iam-auth
- auth-token
requires:
- timeout
```
## Payload types
```yaml
CreateOrderRequest:
description: Order built from the cart
fields:
customerId:
type: uuid
pii: true
idempotencyKey:
type: string
itemCount:
type: int
min: 1
totalCents:
type: int
min: 0
CreateOrderResponse:
description: Order id and status
fields:
orderId:
type: uuid
description: Use it to track the order
status:
type: string
enum:
- PLACED
CartUpdate:
description: What is in the customer's cart
fields:
cartId:
type: uuid
description: Chosen by the browser when the cart is first saved
customerId:
type: uuid
pii: true
description: The signed-in customer
itemCount:
type: int
min: 0
description: Number of items; 0 empties the cart
totalCents:
type: int
min: 0
description: Cart total in cents
tax included: null
CartSaved:
description: The saved cart's id
fields:
cartId:
type: uuid
CheckoutRequest:
description: The cart the customer is checking out
fields:
cartId:
type: uuid
description: The cart saved with PUT /cart
customerId:
type: uuid
pii: true
description: Must match the token's subject
OrderPlacedEvent:
description: Placed order, for fulfilment
classification: confidential
fields:
eventId:
type: uuid
eventType:
type: string
const: order.placed
schemaVersion:
type: int
const: 1
orderId:
type: uuid
customerId:
type: uuid
pii: true
itemCount:
type: int
```
## Tables
```yaml
carts_db.carts:
description: Open carts, kept for 7 days
columns:
id:
type: uuid
pk: true
customer_id:
type: uuid
pii: true
item_count:
type: int
total_cents:
type: bigint
orders_db.orders:
description: Orders
columns:
id:
type: uuid
pk: true
customer_id:
type: uuid
pii: true
status:
type: text
enum:
- PLACED
- CANCELLED
item_count:
type: int
total_cents:
type: bigint
idempotency_key:
type: text
unique: true
created_at:
type: timestamptz
default: now()
orders_db.outbox:
description: Events waiting to be published
columns:
id:
type: uuid
pk: true
aggregate_id:
type: uuid
event_type:
type: text
payload:
type: jsonb
schema: OrderPlacedEvent
created_at:
type: timestamptz
default: now()
published_at:
type: timestamptz
nullable: true
```
## Invariants that must hold
- one checkout creates at most one order, even when the customer retries
- every orders row has exactly one outbox row (written in the same transaction)
- at most one shipment per order_id, even with duplicate SQS delivery
## Policies that apply
- **P2**: Connections crossing a trust boundary must authenticate
- **P5**: A flow step's payload must be listed in its connection's `carries`
- **P6**: Downstream sync timeouts must fit within the entry point's timeout
- **TYP002**: Mapping sources must be existing fields of the payload in scope
- **TYP003**: Mapped source and target types must be compatible
## Acceptance scenarios (write one test per scenario)
- [ ] **happy path**: when the customer checks out a cart with 2 items → 201 with orderId; 1 orders row; 1 outbox row then published_at set; 1 PENDING shipment with item_count 2
- [ ] **customer retries checkout**: when the same checkout is sent twice with the same Idempotency-Key → same orderId returned both times; 1 orders row; 1 outbox row
## Definition of done
- Every mapping is implemented exactly as specified (field names, types, enum values, on_conflict behaviour).
- Each acceptance scenario has an automated test. Use real dependencies in containers (e.g. Testcontainers for Postgres, LocalStack for SQS); don't mock the database or queue.
- The error-handling paths in the steps (`on_error`, `on_conflict`, `on_unknown_schema_version`) are tested.
- `archspec check online-shop.spec.yaml` still passes with no new errors.
-- Generated by archspec from online-shop.spec.yaml. Do not edit; change the spec instead.
CREATE SCHEMA IF NOT EXISTS carts_db;
CREATE SCHEMA IF NOT EXISTS fulfillment_db;
CREATE SCHEMA IF NOT EXISTS orders_db;
CREATE TABLE carts_db.carts (
id UUID PRIMARY KEY,
customer_id UUID NOT NULL,
item_count INT NOT NULL,
total_cents BIGINT NOT NULL
);
COMMENT ON COLUMN carts_db.carts.customer_id IS 'PII';
CREATE TABLE orders_db.orders (
id UUID PRIMARY KEY,
customer_id UUID NOT NULL,
status TEXT NOT NULL CHECK (status IN ('PLACED', 'CANCELLED')),
item_count INT NOT NULL,
total_cents BIGINT NOT NULL,
idempotency_key TEXT NOT NULL UNIQUE,
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
COMMENT ON COLUMN orders_db.orders.customer_id IS 'PII';
CREATE TABLE orders_db.outbox (
id UUID PRIMARY KEY,
aggregate_id UUID NOT NULL,
event_type TEXT NOT NULL,
payload JSONB NOT NULL,
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
published_at TIMESTAMPTZ
);
COMMENT ON COLUMN orders_db.outbox.payload IS 'schema: OrderPlacedEvent';
CREATE TABLE fulfillment_db.shipments (
id UUID PRIMARY KEY,
order_id UUID NOT NULL UNIQUE,
customer_id UUID NOT NULL,
item_count INT NOT NULL,
status TEXT NOT NULL CHECK (status IN ('PENDING', 'SHIPPED')),
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
COMMENT ON COLUMN fulfillment_db.shipments.customer_id IS 'PII';
CREATE TABLE fulfillment_db.processed_events (
event_id UUID PRIMARY KEY,
processed_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
03
Mistakes show up while you type, not in development.
Someone takes a shortcut: the fulfillment worker reads the orders database directly. On a
whiteboard that is one more arrow. In archspec it is three errors, each pointing at the line
to fix, before a single line of code is written.
iDES001order-processing-engine_to_warehouse (order-processing-engine → warehouse: sends picking lists) is a high-level link with no technical design under it yet
3 errors
✗P3fulfillment-worker_to_orders_db (postgres) is missing required statementTimeout, connectionPool
✗P7fulfillment-worker_to_orders_db: fulfillment-worker accesses orders_db, which is owned by order-processing-api; go through the owner's API or events
✗P11fulfillment-worker_to_orders_db: auth 'password' is not allowed for postgres (allowed: iam-db-auth, secrets-manager)
A clean design: no errors.
Why
A picture can't be wrong. A spec can.
Visio, draw.io and Mermaid draw pictures, and design docs describe them in prose. You can't drill from the business view into a
service's contract, and the drawing won't notice that card numbers cross the internet
unencrypted, that two services write the same database, or that a timeout is shorter than
the calls behind it. Those mistakes turn up in code review, in testing or in production.
archspec treats the design as code. One file says who uses which systems, what runs inside
them, how they connect, what data moves and in which order. Because it is precise, it can be
checked, and every view is generated from it, so the diagrams never drift from the spec.
✗P2web_to_api crosses trust boundary internet->dmz without authentication
✗NET003web_to_api is a public endpoint (web -> api) but documents no edgeProtection (WAF, rate limiting, captcha)
✗P11api_to_data: auth 'password' is not allowed for postgres (allowed: iam-db-auth, secrets-manager)
✗P3api_to_data (postgres) is missing required connectionPool
✗P7api_to_data: api accesses data, which is owned by nobody; go through the owner's API or events
What you get from one file
Every view your reviewers need, generated and kept in sync.
Checks like a static analyzer
33 rules type-check field mappings and catch unencrypted personal data, dual
writes, missing timeouts, synchronous call cycles, unowned database access and untested
steps, each with the line to fix.
Diagrams you can edit
Architecture and sequence diagrams drawn natively, with a legend. Add boxes, connections
and steps on the diagram: archspec writes them into the YAML and keeps your comments.
Business view first
Start with people and systems joined by plain-language links. Open a system to design its
services, queues and databases; open a service to see its endpoints and payloads.
Security review built in
A ports-and-protocols matrix with direction, encryption, auth and the sensitivity of the
data on every connection (PII, PCI, PHI), and a table of every trust-boundary crossing.
API reference and OpenAPI
A Swagger-style reference for any box: endpoints, payloads, responses, errors, message
handlers and the calls it makes. Download OpenAPI 3.1 for each service.
Briefs for coding agents
Slice the spec into the exact brief a developer or coding agent needs to build one step:
the payloads, mappings, tables and error handling, and nothing else.
Reviews live in the spec
Mark a finding reviewed, or done with a comment. The review is written into the file, so
it is versioned and discussed in the same pull request as the design.
Exports
SVG and PNG diagrams, Markdown and CSV security tables, Postgres DDL, Mermaid, LikeC4 and
a design doc. The spec itself is plain YAML you keep next to the code.
Private by design
Your architecture never leaves your machine.
The playground is a single page with the engine, the rule pack and the guide built in. After
it loads it makes no network requests at all, and a Content-Security-Policy that blocks every
outbound connection makes the browser hold it to that.
No account, no server, no analytics
Work is kept in your browser between visits
Open and save .spec.yaml files on your own computer
Keeps working offline once loaded
In your pipeline
The same engine checks every pull request.
Keep specs next to the code. The command line checks them in CI with text, JSON or SARIF
output, so findings appear as annotations on the pull request, and renders every diagram and
table for your docs.
$ archspec check orders.spec.yaml✗ NET003 orders.spec.yaml:27 client_to_orders-api is a public endpoint (client -> orders-api) but documents no edgeProtection (WAF, rate limiting, captcha)✗ P9 orders.spec.yaml:53 synchronous call cycle: orders-api -> inventory-api -> orders-api (via orders-api_to_inventory-api, inventory-api_to_orders-api)✗ P1 orders.spec.yaml:62 orders-api_to_order-events carries PII data (OrderPlacedEvent) into storage but encryption.atRest is not set (options: SSE-SQS, SSE-KMS)✗ P8 orders.spec.yaml:175 S2a: orders-api writes to the DB (S2) and publishes directly in the same request; a crash between them loses or invents events. Write to an outbox table in the same transaction and relay it… 17 more · exit code 1
A VS Code extension with the same diagnostics, preview and visual editor is planned.
Who it's for
One source of truth, read the way each person needs it.
Architects
Design end to end, drill into contracts, and find the gaps before anything is built.
Business stakeholders
See who uses which system and what passes between them, without the technical detail.
Developers
Get an unambiguous contract for each step: payloads, mappings, tables and error handling.
Security reviewers
Every connection with port, protocol, encryption, auth, trust boundary and data sensitivity.
Coding agents
A spec that is code only: easy to parse, slice and validate, with a brief per step.
Platform teams
Own the shared catalogs: approved connection types, vendors, data labels and the rule pack.
Try it on your own system.
The playground opens with a complete example. Change it, break it, and watch the findings.