v0.5.4 · first npm release: the package is @ts-atlas/core is out →

Micro-framework for Agents: the new way to build a backend

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.

~/shop · zsh
$ 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

The cost of a change, for an agent

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.

01

Computed context

atlas map and atlas context <name> replace exploration. No decorators, no DI container, no reflection: wiring is data the tool prints.

02

Check, don’t guess

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.

03

The capability is the unit

One file owns its records, its pure decisions and the examples that specify them. Understanding it costs exactly one read.

Architectural foundations

Standing on forty years of computer science

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.

Parnas (1972) · Screaming Architecture

Information hiding by construction

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/.

Functional Core, Imperative Shell · Decider

Pure decisions, zero mocks

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.

Consumer-Driven Contracts · Subtyping

Checked without cross-imports

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.

Transactional Outbox · Snapshot Order

Event-driven on one Postgres

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

Conventional backend vs. Atlas

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.

DimensionConventional stack (NestJS / Express + ORM + Queue)Atlas (@ts-atlas/core/runtime + Postgres)
Context to change a featureO(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 contractsShared 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 consistencyDual-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 recordsSilent 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 indexesHand-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 & diagnosticsSlow 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.
When a rule breaks, atlas check prints the why and the exact fix
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

Decisions are pure. The runtime does all I/O.

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.

src/capabilities/orders.tsCapabilities →
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

Commands become facts. Facts feed everything else.

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.

commandPlaceOrdercapabilityordersfactOrderPlacedcapabilityorder-summarycapabilitystockviewcustomer-ordersqueryMyOrderscommandPlaceOrdercapabilityordersfactOrderPlacedcapabilityorder-summarycapabilitystockviewcustomer-ordersqueryMyOrders
  1. PlaceOrderPOST /commands/PlaceOrder
  2. ordersdecide: pure, versioned reads
  3. OrderPlacedoutbox, same commit
  4. order-summary.project (exactly once)
  5. stock.on reaction (exactly once)
  6. customer-ordersatlas_view_customer-orders
  7. MyOrdersGET /queries/MyOrders

Built into the runtime

Not into each capability

atlas context

Computed context

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.

atlas check

Errors are instructions

ATLAS000–028: every diagnostic names the rule, the place, why it matters and the exact fix line. Exit 1 on errors.

examples

Examples are the spec

Each handler carries state + input → expected outcome. Documentation for people, tests for machines: atlas test runs them all.

outbox

Atomic commit

Writes and emitted facts commit in one transaction with an outbox. No dual write between store and bus.

delivery

Exactly-once reactions

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

Read models as data

Views are real tables built from facts. Queries declare filters and order, and the tool proves an index serves them.

evolution

Facts never rewritten

Version facts and records with pure upcasters (t.fact, t.versioned). Old rows lift step by step on every read.

leases

Scale out safely

Consumer leases, a fair share across worker instances, automatic failover, and API-only instances with start({ workers: false }).

adapters

Zero runtime dependencies

A Store port with a memory store, a Postgres adapter and an Express router. atlas schema exports JSON Schema and OpenAPI 3.1.

Production

Many instances, one database

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 instances
  • workerStore: delivery on its own pool
  • Retries with jittered backoff, a lost connection never kills the process

Atlas runs the MallFeed social-commerce backend: more than a hundred capability files in one folder.

Production guide →
API · atlasRouterstart({ workers: false })API · atlasRouterstart({ workers: false })Worker instancestart() · leasesWorker instancefair share · failoverPostgres · one databaseatlas_recordsatlas_facts (outbox)atlas_cursorsatlas_leasesatlas_scheduleatlas_view_*

Benchmarks & trade-offs

Where Atlas wins—and what it trades off

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.

Reads · GET /queries/:nameAtlas wins on complex lists

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.

Writes · POST /commands/:typeAtlas wins on multi-domain actions

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.

Background queue fan-outPartitioned queues win on raw parallel throughput

~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.

Engineering & agent velocityAtlas wins by an order of magnitude

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.

What you give up (the cost)

  • Eventual consistency across capabilities: transactions are atomic inside one capability; cross-domain flows react through the outbox.
  • Pure projections without reads: .project sees only the emitted fact, never external state or read-time JOINs.
  • Bounded query DSL: q supports indexed operational filters; ad-hoc OLAP, GROUP BY and full-text search belong in SQL read-replicas or edge subscribers.

What you buy in return (the payoff)

  • Month-12 speed equals Month-1 speed: adding the 120th capability costs the same tokens and context as adding the 2nd.
  • Zero dual-write or unindexed queries: facts commit with state, and ATLAS024 refuses any sorted query without an index.
  • Deterministic safety for AI agents: atlas check acts as an automated senior architect reviewing every boundary before code runs.

Releases

Every version, with its migration notes

Get started

Install it in an app

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