Computed context
atlas map and atlas context <name> replace exploration. No decorators, no DI container, no reflection: wiring is data the tool prints.
Context should be computed, not discovered. Atlas is a TypeScript backend where the system is its own map: capabilities declare themselves as data, and a tool answers what an agent would otherwise learn by reading thirty files.
$ atlas context ordersorders — src/capabilities/orders.ts (112 lines)DOWNSTREAM BLAST RADIUS (who breaks if what this produces changes) OrderPlaced.customerId <- order-summary OrderPlaced.lines[].sku <- stock OrderPlaced: unused by consumers (safe to change): lines[].unitPriceCents orders.status <- cancel-unfulfillable (foreign read)To change this capability, edit only: src/capabilities/orders.ts$ atlas check✓ system is consistent (0 errors, 0 warnings)$ atlas test28 passed, 0 failed, 28 examples in 4 capabilities (13 ms)
Why Atlas
cost = find + understand + write + verify
An agent’s most common failure is not bad code. It is correct code built on a wrong picture of the system: a consumer it did not know existed, a field someone else relied on, a convention nobody wrote down. Atlas attacks each term.
atlas map and atlas context <name> replace exploration. No decorators, no DI container, no reflection: wiring is data the tool prints.
Every boundary has a runtime shape, so atlas check proves one rule everywhere: the producer’s shape satisfies the consumer’s. Contracts are checked, not imported.
One file owns its records, its pure decisions and the examples that specify them. Understanding it costs exactly one read.
Architectural foundations
Atlas does not invent new distributed-systems theory. It turns five proven software architecture principles into a single TypeScript protocol enforced by a tool—so neither humans nor agents can drift into spaghetti.
Each capability in src/capabilities/ is a true bounded context. It never exports helpers to siblings and never imports another capability (ATLAS006); the folder screams the business domain, not controllers/ or repositories/.
decide and .project are pure functions of (input, state, now) → effects (ATLAS007). Because they perform no I/O, the runtime retries concurrency conflicts for free and runs dozens of spec examples in milliseconds.
Every boundary has a runtime shape (t.*). Capabilities stay decoupled while atlas check (ATLAS003) proves statically that every emitted fact and owned record satisfies every downstream consumer’s needs.
State writes, outbox facts and cursor advances commit in one ACID transaction ordered by Postgres xid8 snapshots. You get exact delivery, FOR SHARE / FOR UPDATE OCC, leases and shadow-swapped views without Kafka or Redis.
Why it feels different
In a conventional layered stack, every new feature multiplies the files to trace, the mocks to maintain, and the infrastructure to keep in sync. In Atlas, the cost of a change stays constant whether you have 4 capabilities or 140.
| Dimension | Conventional stack (NestJS / Express + ORM + Queue) | Atlas (@ts-atlas/core/runtime + Postgres) |
|---|---|---|
| Context to change a feature | O(N) exploration across controllers, DTOs, services, repositories and event listeners (10–30 files). | O(1): atlas context <name> prints upstream shapes and the field-level blast radius. Edit one file. |
| Cross-domain contracts | Shared imports that tangle modules, or untyped event payloads that break consumers at runtime. | Zero cross-capability imports. atlas check (ATLAS003, ATLAS016) proves producer shapes satisfy every consumer and blocks sensitive leaks. |
| Writes + events consistency | Dual-write race between database and message broker, or extra CDC / Debezium / Kafka infrastructure. | Atomic transaction: record writes, outbox facts and scheduled timers commit together in Postgres. |
| Concurrency on hot records | Silent lost updates, or manual SELECT FOR UPDATE locks that queue readers and stall the connection pool. | Automatic version checks, FOR SHARE on read-only records, 200 ms lock timeout and jittered retries of the pure decision. |
| Read models & query indexes | Hand-written SQL migrations and unindexed WHERE / ORDER BY clauses discovered under production load. | Declarative views rebuilt via shadow-table swap (__shadow); ATLAS024 refuses any query order without a matching index. |
| Verification & diagnostics | Slow integration suites with Docker, seed scripts and brittle mocks; stack traces that require investigation. | Inline examples verified in ~15 ms with no database, plus compiler-grade diagnostics that print why: and the exact fix: line. |
error ATLAS003 stock: consumes OrderPlaced.lines[].sku, but orders does not emit it why: a reaction cannot read a field the fact never carries; at runtime it would be undefined. fix: add `sku: t.string` to emits.OrderPlaced (inside `lines[]`) in src/capabilities/orders.ts, or remove it from needs error ATLAS024 order-summary: query MyOrders orders by placedAt, but no index of view customer-orders starts with it (after its eq filters) why: an ordered page without a matching index sorts the whole table on every request. fix: add `['customerId', 'placedAt']` to views.customer-orders.indexes in src/capabilities/order-summary.ts.
One file, one capability
A decision receives input, state and time, and returns reject(code) or accept({ write, emit, send }). Because it is pure, the runtime retries it on a concurrency conflict for free.
import { capability, t } from '@ts-atlas/core/runtime';
const Line = t.object({ sku: t.string, quantity: t.int, unitPriceCents: t.int });
const ada = { id: 'c1', roles: [] };
export default capability({
name: 'orders',
intent: 'Customers place orders for themselves and cancel their own, at most once. The customer is the actor, never the payload.',
owns: {
orders: t.object({
orderId: t.string, customerId: t.string, status: t.enum('placed', 'cancelled'), lines: t.array(Line),
totalCents: t.int, placedAt: t.string, cancelReason: t.nullable(t.string),
}),
},
emits: { OrderPlaced: t.object({ orderId: t.string, customerId: t.string, lines: t.array(Line), totalCents: t.int }) },
})
.command('PlaceOrder', {
access: 'user',
input: t.object({ orderId: t.string, lines: t.array(Line) }),
reads: { existing: { from: 'orders', key: (input) => input.orderId } },
decide({ input, state, now, actor }, d) {
if (state.existing !== null) return d.reject('order-exists', { orderId: input.orderId });
if (input.lines.length === 0) return d.reject('order-empty');
const totalCents = input.lines.reduce((sum, line) => sum + line.quantity * line.unitPriceCents, 0);
const order = { ...input, customerId: actor.id, totalCents };
return d.accept({
write: [d.write('orders', input.orderId, { ...order, status: 'placed', placedAt: now, cancelReason: null })],
emit: [d.fact('OrderPlaced', order)],
});
},
examples: [
{ name: 'rejects an anonymous caller', actor: null, input: { orderId: 'o1', lines: [] }, expect: { rejects: 'unauthenticated' } },
{ name: 'rejects an empty order', actor: ada, input: { orderId: 'o1', lines: [] }, expect: { rejects: 'order-empty' } },
// …
],
});How it works
A command is decided and committed with its facts in one transaction. Each consumer reads the outbox through its own cursor and commits its effects with the cursor advance: exactly once, in order.
PlaceOrderPOST /commands/PlaceOrderordersdecide: pure, versioned readsOrderPlacedoutbox, same commitorder-summary.project (exactly once)stock.on reaction (exactly once)customer-ordersatlas_view_customer-ordersMyOrdersGET /queries/MyOrdersBuilt into the runtime
The file, every upstream shape it relies on, and the field-level blast radius of what it produces. The minimal context for a safe change, in milliseconds.
ATLAS000–028: every diagnostic names the rule, the place, why it matters and the exact fix line. Exit 1 on errors.
Each handler carries state + input → expected outcome. Documentation for people, tests for machines: atlas test runs them all.
Writes and emitted facts commit in one transaction with an outbox. No dual write between store and bus.
A consumer’s writes and its cursor advance commit together, in order and in batches. A refusal settles; one bad fact never blocks the rest.
Views are real tables built from facts. Queries declare filters and order, and the tool proves an index serves them.
Version facts and records with pure upcasters (t.fact, t.versioned). Old rows lift step by step on every read.
Consumer leases, a fair share across worker instances, automatic failover, and API-only instances with start({ workers: false }).
A Store port with a memory store, a Postgres adapter and an Express router. atlas schema exports JSON Schema and OpenAPI 3.1.
Production
Commands are safe by construction (row versions), deliveries commit with a cursor compare-and-set, and timers are claimed with SKIP LOCKED. Consumer leases run each consumer in one instance at a time, split fairly across worker instances.
start({ workers: false }) for API-only instancesworkerStore: delivery on its own poolAtlas runs the MallFeed social-commerce backend: more than a hundred capability files in one folder.
Production guide →Benchmarks & trade-offs
In software architecture nothing is free: every guarantee is bought by giving up a degree of freedom. Here is how Atlas measures against a conventional stack on Postgres 17 (Apple M4 Pro, Docker), and the trade-offs behind those numbers.
O(log N) keyset · 0 JOINs
Conventional ORMs run multi-table JOINs and OFFSET pagination on every request. Atlas reads a single pre-projected table (atlas_view_*), enforces a composite btree index at build time (ATLAS024), and pages by keyset cursor.
3,225–6,165 cmd/s · p50 4.7–9.7 ms
Instead of locking rows across 4 tables and waiting on an external broker inside the HTTP request, a command locks only its own records (FOR SHARE / FOR UPDATE, 200 ms timeout) and appends its facts in one short Postgres transaction.
~825 facts/s per consumer (batched)
Each capability processes its stream sequentially under one lease to guarantee exact order and cursor atomicity (20,000 facts drained in 24.2 s; 26 statements/s idle across 106 consumers). Unordered BullMQ/Kafka workers can run N consumers on the same queue in parallel.
28 specs in ~15 ms · 1 file per change
Pure decisions need no database or mocks to test. Month 12 moves at the speed of Month 1 because atlas context computes the exact blast radius and atlas check catches contract drift before runtime.
.project sees only the emitted fact, never external state or read-time JOINs.q supports indexed operational filters; ad-hoc OLAP, GROUP BY and full-text search belong in SQL read-replicas or edge subscribers.ATLAS024 refuses any sorted query without an index.atlas check acts as an automated senior architect reviewing every boundary before code runs.Releases
Get started
Requires Node 24 (it runs .ts natively). The core has zero runtime dependencies.
$ npm install @ts-atlas/core
$ npx atlas new loyalty-points --dir src/capabilities
$ npx atlas check --dir src/capabilities