github.com/Panevschi-Ruslan/flowatlas command reference in CI fixtures

flowatlas · a walkthrough in eleven steps

One map of a project that lives in several repositories

A command-line tool that reads several TypeScript repositories with the compiler's own checker, without running them, and joins them into one graph you can query β€” from a terminal or from a coding agent. It knows NestJS and Angular, React and Next.js, SvelteKit and Remix, Express, Fastify, Koa and Hono, Medusa, tRPC and Telegraf; AWS Lambda, API Gateway, Step Functions, EventBridge, SQS and SNS as Terraform deploys them; TypeORM, Prisma, Drizzle, Mongoose, Sequelize, Kysely, Knex, MongoDB and node-postgres; Redis, Kafka, RabbitMQ, BullMQ and socket.io — and it can join to a service it cannot read at all, from its OpenAPI or AsyncAPI document.

A request made in one service is matched to the route that answers it in another, a message published in one to whatever handles it, a button in a browser to the endpoint it calls. madge, dependency-cruiser and nx graph draw the imports inside one repository, which is what a compiler already sees. This joins the repositories. If your project is one repository, you do not need it.

npm install -g @flowatlas/cli
flowatlas init --dir .   # find the repositories under here
flowatlas build          # read them all and join them

Node 22.12 or later. Nothing is sent anywhere: the graph, the traces it is given and the page it writes stay on your machine.

what it reads

ways in

  • NestJS controllers, message handlers and schedules, with their guards and pipes
  • Express, Fastify, Koa and Hono, where a route is registered by a call
  • Next.js, Medusa, SvelteKit and Remix / React Router, where a route is the path of a file
  • Lambda functions, API Gateway routes and Step Functions, read from the Terraform that deploys them
  • tRPC procedures, and Telegraf bot commands, callbacks and events
  • what each one takes and answers, failures included, field by field

browsers

  • Angular templates, services and router, lazy routes included
  • React components and hooks
  • fetch, axios, a wrapper or a client class of your own — each joined to the route that answers it

data

  • TypeORM, Prisma, Drizzle, Mongoose, Sequelize, Kysely, Knex, MongoDB and node-postgres
  • read with nothing installed, even a Prisma client that was never generated
  • a repository base of your own, with the table each class states
  • Redis and cache-manager, and every settings key a request depends on

messages

  • Kafka, RabbitMQ, BullMQ, Redis pub/sub and socket.io, from both ends
  • an in-house bus, described in the configuration
  • the payload and the reply of a request, compared across the boundary

across repositories

  • a monorepo's services with the workspace packages they use, on a fresh clone
  • a service you have no source for, from its OpenAPI or AsyncAPI document
  • every contract compared where one repository meets another

what really ran

  • OpenTelemetry, Zipkin and Jaeger traces, @nestjs/observe, and the HAR files a Playwright run saves
  • AWS X-Ray, Step Functions histories, API Gateway and Lambda logs
  • counts, p50 and p95, statuses and payload shapes laid over the map; never an edge drawn from a trace
  • what was seen and never declared, and what the tests never reached

before a merge

  • diff: a verdict, and every changed flow as a sequence diagram
  • check --env: can this be deployed against what an environment runs
  • rules: who may call, write, publish to or consume what
  • spec and export: OpenAPI, AsyncAPI, Backstage and Structurizr from the graph
  • SARIF for GitHub code scanning; in CI has the recipes

what it cannot read

  • a row saying why, and the one change that would read it
  • an address or a table decided at run time is reported, never guessed
  • test code left out, and every test directory named so you can see it

Every shape above is held by a fixture — a small repository written to prove it, compared on every push. The fixtures page lists all of them.

measured on the demo project

The demo project is fixtures/multi-repo, the same one the walkthrough below is recorded against: four repositories, three NestJS services and an Angular frontend, with a shared package of types between them. It is synthetic, written for the test suite, and holds every kind of join exactly once, including the ones that fail, so the figures are small and the rows that did not join are there on purpose. Anyone can rebuild them from a checkout; the README has the commands.

962lines of TypeScript, over 27 files
130nodes, joined by 114 edges
12of those edges cross a repository boundary
1.2sto read all four; 0.3s when nothing changed

The third figure is the one that matters. Twelve connections that no compiler in any of those four checkouts can see, because each one only ever reads its own. 104 of the edges were read from the code, 5 were inferred and marked heuristic, and 5 were declared by an annotation. Every edge says which of the three it is. Measured with 0.6.0.

ways in

HTTP routes15
message handlers2
routes something in the project reaches7
routes nothing it can see calls8

joined β€” first build, then configured

browser requests matched to their route4 β†’ 6 / 13
calls between services matched3 β†’ 3 / 7
routes something reaches6 β†’ 7
edges across a repository boundary10 β†’ 12

found once they were joined

contract errors, over 7 boundaries0
names declared more than one way1
channels handled nowhere1
front-end requests for a route nothing serves2

what it says it cannot see

gaps and findings to act on11
places the reader missed, and findings9 + 4
of those, places static reading cannot reach2
edges inferred, and marked so5

The first column is what a first build gives you. The configuration behind the second, the fixture's own, says which settings key addresses a service, where a frontend's key points and which package holds the shared types: a string in one repository and a route in another are joined by a fact only the person who deployed them knows, and flowatlas doctor names each with the line that wants it. The four calls that stay unjoined are the fixture's deliberate ones.

Nothing in that last table is dropped or rounded away. Each row carries a file, a line, a reason and the one thing to change. flowatlas stats prints these figures for your own project, and flowatlas doctor prints the list behind them.

The rest of this page is a walkthrough, recorded against fixtures/multi-repo, the demo project measured above: four services that do not share a repository, small enough to read in a sitting, and already carrying the kinds of problem a real project has. A scene is a shell script, so what is on screen is what actually ran.

01Point it at the repositories

Put yourself in a directory with the repositories under it, or beside them, and run init. It reads each manifest, works out the name and the kind, and writes flowatlas.config.json. It also registers the graph server in every repository, so an agent working in any one of them can ask about all of them.

npm install -g @flowatlas/cli

flowatlas init          # find the repositories and ask
flowatlas init --dir .  # look under this one, not beside it

flowatlas init with no --dir scans the parent directory, which is what you want when the configuration lives inside one of the repositories, and not what you want when it lives above them.

02Build

build reads every repository in its own process and joins the readings. A first build is rarely all green, and it is not supposed to be.

lsflowatlas init --dir . --yesflowatlas build
flowatlas init finding four repositories, then flowatlas build joining them

Three of the seven calls between services joined; one goes to PAYMENTS_URL, which no service claims. Four of thirteen browser requests found their route. The last line counts what is left: missed places the reader could not join, notes on joins it made from a name alone, and findings about the code itself. Nothing was guessed; all of it was recorded, with a file and a line.

03Ask what it could not read

doctor turns those counts into a list of things to do, grouped by cause. Every row names the reason, the file, the line, the call, and what to change.

flowatlas doctor --unresolvedflowatlas doctor --reason base-url-from-name --section unresolved
flowatlas doctor grouping what it could not join by cause, and the two settings it asks for

Three browser calls could go to two services, so it asks which one the frontend's apiUrl names. PAYMENTS_URL is a setting no service claims, and doctor --missing names the repository it probably belongs to. And ORDERS_URL was joined to orders only because of its name, which is kept as a note until the configuration says so outright.

flowatlas doctor --reason <code>  # one kind of gap
flowatlas doctor --missing          # the repository missing
flowatlas accept route-unguarded --service orders
flowatlas doctor --strict           # exit 1 past the baseline

04Answer it in the configuration

Two settings join two more browser requests, reach one more route, and turn both notes into facts.

baseUrlEnv says which settings keys other services use to address a service. Without it, a key named after a service is still joined to it, and doctor keeps a note saying only the name chose it; a key named after nothing is joined to nothing. apiTarget does the same for a browser, saying which service a frontend's settings key points at.

git diffflowatlas build
the two settings added to the configuration, as a diff, and the build that follows
first buildafter two settings
calls between services linked3 of 73 of 7
browser requests joined to a route4 of 136 of 13
routes something reaches67
places missed · notes11 · 29 · 0

What survives is no longer missing configuration. It is the project disagreeing with itself, which is the thing worth knowing.

05Follow one request

flow takes any way in and follows it until it stops.

flowatlas flow 'ui_action:web#…' --format tree --depth 20flowatlas config 'ui_action:web#…'
a click in an Angular template followed through the gateway into the orders service

The first trace starts at a (click) in an Angular template and ends at a row in a table in a different repository, crossing two service boundaries on the way, with unresolved on this path: 0 to say nothing along it was guessed.

The second shows the other case. BillingClient builds its address at run time from a registry, so nothing can be read from the source; an annotation on the method declares where it goes, and the edge is recorded with confidence: marker rather than static, so it is always clear which edges were told rather than found.

The second also shows billing calling back into orders for the order it invoices: three repositories again, one of them twice.

An entry can be named however you would say it aloud. These three are the same route:

flowatlas flow "POST /orders/12345/archive"
flowatlas flow "POST /orders/:param/archive"
flowatlas flow "entry:orders:http:POST:/orders/:param/archive"

06Ask the other direction

impact starts at a symbol and walks backwards to every way in that reaches it. Give it a table and it tells you which routes would have to be retested.

flowatlas impact 'table:orders#orders' --format treeflowatlas hotspots --type entry --max 5flowatlas channel order.created --format tree
impact walking backwards from a table, hotspots, and both ends of a channel

hotspots ranks what the most things point at, and channel shows both ends of a message channel — here order.created, published in orders and handled in billing, whose handler calls back into orders for the order it was told about.

07Compare what crosses a boundary

Two services agree about a shape until one of them changes. contracts compares what each side of every crossing declares, field by field.

flowatlas contracts --severity error --max-nodes 4flowatlas types --drift --format tree
no contract errors, the boundaries that could not be compared and why, and two types declared two ways

No boundary in the demo disagrees, and the unchecked list is why that is worth believing: it says which boundaries could not be compared and what to do about each — a literal where a typed value would do, a call that says nothing about what it expects back — rather than reporting them as agreement.

types --drift finds what a compiler in one checkout never can: InvoiceDto declared one way in billing and another in the gateway, and OrderDto in the shared package and again in the web app. contracts compares the two field by field.

08See the shape of the whole thing

flowatlas stats --format treeflowatlas dead --kind channelsflowatlas cycles
what the graph is made of, what nothing reaches, and no cycles

dead is the one to read carefully. It says so itself: nothing in it is proof, every row is a heuristic with the reason it fired attached.

visualise writes one self-contained page: no server, nothing to fetch, nothing to install. It opens on a Map of the services, lists every way in, and follows any one of them across service boundaries, marking each crossing with what it was joined by and how much to trust it.

flowatlas visualise --title Shop
the generated page: Steps on a gateway route followed into billing and on into orders, with the time each step took in the traces

It is a report you can click, not a viewer you keep running. Attach it to a review, or keep it beside a decision.

It opens on a Map: one box per service, queue, table and outside host, laid out in the order a request travels, joined by what passes between them and counted. A check marks what to look at — routes nothing calls, channels with one end, services that reach each other — and, once traces are laid over it, a heat colours each link by its traffic or its p95. Start here offers the busiest way in and the worst gap.

Steps shows one way in as a list in call order: each table, address and channel inline, calls side by side or once per item of a loop, what it can fail with, and — where traffic went through — how often it ran and how long it took; Waterfall draws the same steps on a time axis. Copy as prompt hands it to an assistant as Markdown. Not joined lists what did not join, grouped by cause, with the risks found in the code and what was accepted. Busiest ranks the methods most things lean on, Traces draws real requests one by one, and the Graph tab shows any node with what it takes and gives back, each type opened field by field. --diff <ref> adds Changes and --observed-diff adds Run time changes. One search box above the tabs finds a route, channel, table, type, gap or class, and goes to it.

Mapheat p95Start hereWaterfallNot joinedsearchGraph · Types
the Map of the four services coloured by p95, the busiest way in opened in Steps and as a waterfall, Not joined grouped by cause, the search, and one route in the Graph with its types

The page above is the demo project with its sample traces laid over it (scene 10): scripts/demo/page-project.sh builds it and scripts/demo/page-tour.mjs takes the pictures.

09Ask what a branch changes

diff builds the graph at two revisions and reports what moved between them.

git -C orders diffflowatlas diff HEAD --section impactflowatlas diff HEAD --section flows
one renamed route, who reaches it, and two flows it cuts, step by step

One route is renamed in the orders service. flowatlas names every way in that reached it — the gateway route, billing's handlers and the browser button above them, in repositories the change does not touch — and shows each flow it cuts as the steps before and after, drawn as a sequence diagram in the Markdown. The output is Markdown because its destination is a pull request comment; --fail-on-contract-break and --fail-on-rule-break make it a build failure instead.

flowatlas diff main               # this branch against main
flowatlas diff main --section impact  # only who would notice
flowatlas check --env production  # safe to deploy there?
flowatlas rules                   # who may call what

10Lay what really ran over it

Everything so far was read from code. observe adds what real traffic did — from OpenTelemetry, Zipkin, Jaeger, @nestjs/observe, a test run's HAR files or AWS logs — and lays it over the map: how often each join ran, how long it took, how it answered. A trace never draws an edge; what it shows that the code does not is listed as a question.

ls traces/*.jsonflowatlas observe traces/…flowatlas doctor --reason failure-seen-undeclared
three trace files read, the joins they ran with their timings, and a status traffic got that the code never declares

Twenty-nine spans from three files: the joins they ran, the slowest ways in by p95, and three things traffic reached that no reader drew — a collection, a metrics route and a queue. One route answered 500 that nothing in its code says it can; doctor names it, and the page puts the same counts and timings on every step.

flowatlas observe test-results  # a Playwright run's HAR files
flowatlas observe --listen      # OTLP on 127.0.0.1:4318

Run time in five minutes goes from an end-to-end test run to that picture.

11Give it to an agent

init already registered the server in each repository. An agent in any one of them gets the whole project over the Model Context Protocol.

cat orders/.mcp.jsontools/listfind_symboldescribe_for_prompt
the registered server, its tools listed, a symbol found by its initials, and a table described for a prompt

Twenty-six tools in all, and a session lists the ones its project can answer: nineteen here, until traces are laid over it or rules are written (flowatlas mcp --all-tools lists every one). Only one of them returns source code. A trace never drags in the body of every method along it; the agent asks the graph which name to look at, then asks for that one name.

flowatlas mcp --install   # register everywhere, merging
flowatlas mcp --install --dry-run

After that, claude mcp list shows flowatlas. docs/mcp.md is the whole thing: setup for Claude Code, Cursor, VS Code and Claude Desktop, what each tool takes, the order to call them in, and what to check when a client will not list the server.