portolan

module
v0.0.0-...-ff9ef69 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Sep 9, 2026 License: MIT

README

A software architecture landscape mapped by Portolan

Portolan

Your architecture, read from the code.

Turn code, contracts, schemas, traces and ADRs into a validated, navigable map of your software estate.

View the product tour · Explore the example catalog

Make architecture visible — and keep it honest

Architecture documentation loses value when it becomes another system teams must remember to maintain. Portolan starts with the evidence your repositories already contain and turns it into one coherent, searchable view of the system.

See how bounded contexts, services, APIs, events, data stores, flows and decisions fit together. Follow any relationship back to its source. Surface missing contracts, ownership conflicts and architectural drift before they become production surprises.

  • Understand the whole estate. Move from the landscape to a single flow, service, schema or decision without losing context.
  • Trust what you see. Portolan merges and validates facts from code, specifications and observed traces instead of relying on a second hand-built inventory.
  • Publish anywhere. The catalog and site are static end to end, require no hosted backend and fit naturally into pull requests and CI.

DDD enriches the model when a repository uses it; it is never a prerequisite. Optional branch comparison and source previews read immutable files from GitHub or GitLab at runtime, while local development uses a localhost-only control plane.

What it does

flowchart TB
  code["Go · TypeScript · Rust<br/>Java · Django"]
  specs["OpenAPI · AsyncAPI · GraphQL<br/>proto · SQL"]
  traces["OTel traces"]
  frag["catalog fragments<br/>beside each service<br/>*/portolan/*.json"]
  merged["merge + validate"]
  site["the site<br/>React SPA"]
  docs["docs/<br/>markdown, llms.txt"]
  exports["exports/<br/>Backstage + Mermaid"]
  c4["likec4/<br/>C4 + one view per flow"]

  code -- extract --> frag
  specs -- extract --> frag
  traces -- verify --> frag
  frag --> merged
  merged --> site
  merged -- generate --> docs
  merged -- generate --> exports
  merged -- likec4:gen --> c4 --> site

There is no master catalog file. Every fragment the manifest's sources globs find — what each service publishes beside its own code, plus the estate's hand-written facts in data/ (shared types, ADRs, .flow.md walkthroughs) — is merged, then validated as one estate: referential integrity is a property of the union, so a fragment naming a peer it does not own is normal. Validation happens at startup; if it fails the shell renders the error instead of a blank page.

Facts carry a status: declared (a fragment says so), verified (a recorded trace showed it happening), unresolved (nothing in the catalog answers the reference).

What the site shows

  • Entity pages — context, service, aggregate (entities, value objects, lifecycle, events, commands, queries), event, store, schema module, ADR.
  • Flows — step-by-step walkthroughs with a step rail, cross-protocol chains that continue across contexts, and per-step detail. Source locations open an inline code window around the exact line; private forge tokens stay in tab memory, and the external GitHub/GitLab link remains available.
  • Diagrams — LikeC4 C4 views (estate landscape, every container in the estate with its technology and the protocol on each edge, one per context, two per service, one dynamic view per flow), an ELK-routed dependency graph, and a context map. The app never draws these itself; npm run likec4:gen writes the model from the catalog.
  • ER canvases — per store: tables, views, keys and crow's feet, plus column lineage (from) drawn dashed; hovering a column lights the whole chain back to where the value came from.
  • API specs — OpenAPI via Scalar, AsyncAPI via its React component, a GraphQL schema as the SDL it was written as.
  • Navigation — ⌘K palette over everything the catalog names (e: events, vo: value objects, …), sidebar tree, breadcrumbs, "what links here", a trail of recent pages, pins, keyboard shortcuts, light/dark and density.
  • Ask the catalog — a chat that answers from the generated pages: the model gets llms.txt and opens pages one at a time, names things by their ids (which become links), and can end an answer with a card drawn from the catalog — a service, a flow's sequence diagram, what runs between two contexts, an aggregate's state machine. The demo answers through the worker in proxy/; a reader can bring their own OpenAI-compatible endpoint and key from the panel's settings, kept in their browser.

What it checks

The Problems page lists every edge that leaves the chart, grouped errors first:

  • calls and consumers no service in the catalog answers, and RPC methods a known provider does not have;
  • a foreign key or a column's lineage crossing a service boundary, a database with a second writer, a table that no longer holds the aggregate it claims, a column whose type has drifted from its field's, an outbox with no payload;
  • a channel with a second publisher, an event on a channel its service does not declare, a declared channel no event names, a subscription nothing publishes.

Where the facts come from

Plugins, one JSON message in and one out (plugins/README.md), declared in portolan.json and run in three phases:

phase plugins
extract extract-project, extract-go, extract-ts, extract-rust, extract-java, extract-django, extract-celery, extract-python-kafka, extract-openapi, extract-wsdl, extract-http-clients, extract-redis, extract-asyncapi, extract-graphql, extract-proto, extract-river, extract-watermill, extract-go-nats, extract-csr, extract-sql, extract-flows, extract-adr, extract-glossary, extract-commands
verify verify-otel — reads traces, marks the hops they show as verified; verify-codeowners — reads CODEOWNERS, says who to ask about each service
generate gen-markdowndocs/, gen-mermaid — standalone flow diagrams, gen-backstage — Backstage entities

fetch-git, fetch-bsr and fetch-csr bring in sources from other repositories, the Buf Schema Registry and a Confluent Schema Registry, against a lock, so a later build can reproduce them without a socket.

Each plugin describes its own options; npm run schema asks all of them and composes schema/portolan.schema.json, which editors complete against and gen checks before running anything.

Use it in your project

Run the setup once from the root of a repository:

npx @shortlink-org/portolan init
npm install --save-dev @shortlink-org/portolan
npx portolan generate
npx portolan dev

init looks at the repository the way the site's Settings page does when a project is added: it finds the directories that hold a build file, the domain layouts, API specifications, schemas, migrations, ADRs and glossaries it can read, and proposes a portolan.json with an extractor for each. In a terminal it asks which directories are projects, what to read in each, and whether to run portolan generate straight away; every question has the detected answer as its default. With --yes, or without a terminal, it takes those defaults and asks nothing. A plugin whose toolchain is not on PATH is pointed out before anything is written.

init never overwrites an existing portolan.json. It adds .portolan/ to .gitignore and, when the repository has a package.json, adds these scripts without replacing scripts that are already there:

{
  "scripts": {
    "architecture": "portolan dev",
    "architecture:gen": "portolan generate",
    "architecture:check": "portolan check",
    "architecture:build": "portolan build"
  }
}

The generated fragments, Markdown, and exports are ordinary reviewable files and should be committed. .portolan/ is local build state and dist/ is the deployable static site.

command purpose
portolan init inspect the repository and write the first manifest; --yes takes every detected default
portolan dev run the local site and setup UI
portolan generate update fragments, documentation, and exports
portolan check fail when committed generated files are stale, without writing them
portolan build build the static site into dist/
portolan diff BASE describe the architecture change from a branch, tag, or commit
portolan doctor show which toolchains the manifest's plugins need and which are on PATH

Node.js 24 is required, and for a repository the built-in extractors can read on their own it is the only requirement: every Go plugin runs as one wasm module over the workspace (adr/0006), the fetchers run inside the host (adr/0008), and the package ships no Go at all. A Go, TypeScript, OpenAPI, proto, SQL or GraphQL tree is read, and another repository or a schema registry is vendored, with Node and git alone. An extractor that runs in its own runtime still needs it: Python 3 for Django and Celery, Java 21 for Java, Cargo for Rust. portolan doctor reports what the manifest asks for against what is on PATH. The Docker image contains all of them.

Add delivery automation

While portolan dev is running, open Settings → Delivery presets. Portolan detects GitHub or GitLab from the repository's origin, previews the exact CI changes, and generates only the jobs selected there. Architecture checks and static catalog publishing are selected by default; pull-request architecture diffs and GitHub SARIF annotations are opt-in. Check, review, and publication live in separate GitHub workflows, while SARIF augments the review workflow, so comment and code-scanning permissions are granted only when those capabilities are enabled. Existing unmanaged workflow files are never overwritten, and disabling a capability removes only files managed by Portolan.

Run without installing Node or language toolchains

The same CLI is published at ghcr.io/shortlink-org/portolan. On Linux, pass the host uid and gid so generated files remain owned by the developer:

docker run --rm \
  --user "$(id -u):$(id -g)" \
  -e HOME=/tmp \
  -v "$PWD:/workspace" \
  -w /workspace \
  ghcr.io/shortlink-org/portolan:0.2.0 generate

Use immutable versions in CI. latest is intended for trying the CLI, not for a reproducible build.

One repository or an estate repository

For one application or a monorepo, keep portolan.json at its root and write fragments beside each component. An organization-wide catalog can instead live in a dedicated architecture repository: fetch-git pins the service repositories at immutable commits and the normal merge, check, diff, and build commands operate on the combined estate.

Develop Portolan itself

npm install
npm run dev
npm run gen          # run extract → verify → generate over portolan.json
npm run gen:check    # fail if what is committed no longer follows from the catalog
npm run diff         # what this branch changes about the architecture
npm run schema       # recompose the manifest schema from the plugins
npm test             # vitest; npm run test:go for the Go catalog mirror
npm run build        # likec4:gen + tsc --noEmit + vite build

Generated output is committed, so a change to it shows up in a diff. CI builds the site (npm run build); the --check variants and the test suites are run locally before a change lands, since they need the Go, Java, Rust and Python toolchains the plugins are written in.

Directories

Path Synopsis
Package catalog is the Go side of the contract in src/catalog.ts.
Package catalog is the Go side of the contract in src/catalog.ts.
internal
commands
Package commands reads what a developer types against a checkout - the make targets, npm scripts, just recipes and task-runner tasks a repository declares - into catalog.Command values.
Package commands reads what a developer types against a checkout - the make targets, npm scripts, just recipes and task-runner tasks a repository declares - into catalog.Command values.
gohttp
Package gohttp reads outbound HTTP-shaped calls from Go source.
Package gohttp reads outbound HTTP-shaped calls from Go source.
goscan
Package goscan reads a Go source tree the way the extractors need it: every non-test, non-generated file parsed once, each with the import path of its package and its imports by local name, and the string constants of the tree indexed so that a topic, a queue or a subject named by a constant resolves to the literal behind it.
Package goscan reads a Go source tree the way the extractors need it: every non-test, non-generated file parsed once, each with the import path of its package and its imports by local name, and the string constants of the tree indexed so that a topic, a queue or a subject named by a constant resolves to the literal behind it.
wsdl
Package wsdl reads local WSDL 1.1 contracts and the XML Schemas they reach.
Package wsdl reads local WSDL 1.1 contracts and the XML Schemas they reach.
Package plugin is the wire contract between the host and a generator.
Package plugin is the wire contract between the host and a generator.
schematest
Package schematest checks that a plugin's options schema still describes its options struct.
Package schematest checks that a plugin's options schema still describes its options struct.
plugins
cmd/portolan-go command
Command portolan-go is every built-in Go plugin in one binary.
Command portolan-go is every built-in Go plugin in one binary.
extract-adr
Command extract-adr reads decision records written by hand, as the MADR markdown they already are, and answers with a catalog fragment.
Command extract-adr reads decision records written by hand, as the MADR markdown they already are, and answers with a catalog fragment.
extract-asyncapi
Package extractasyncapi is portolan-extract-asyncapi: an AsyncAPI document in, a catalog fragment out.
Package extractasyncapi is portolan-extract-asyncapi: an AsyncAPI document in, a catalog fragment out.
extract-commands
Command extract-commands reads what a developer types against a checkout - the make targets, npm scripts, just recipes and task-runner tasks the repository declares - and answers with a catalog fragment holding them on the service they belong to.
Command extract-commands reads what a developer types against a checkout - the make targets, npm scripts, just recipes and task-runner tasks the repository declares - and answers with a catalog fragment holding them on the service they belong to.
extract-csr
Package extractcsr is portolan-extract-csr: schemas vendored out of a Confluent Schema Registry in, a catalog fragment out.
Package extractcsr is portolan-extract-csr: schemas vendored out of a Confluent Schema Registry in, a catalog fragment out.
extract-flows
Command extract-flows reads flows written by hand, in a text form that reads like the sequence diagram it becomes, and answers with a catalog fragment.
Command extract-flows reads flows written by hand, in a text form that reads like the sequence diagram it becomes, and answers with a catalog fragment.
extract-glossary
Command extract-glossary reads the vocabulary a bounded context speaks, as the GLOSSARY.md it is already written in, and answers with a catalog fragment.
Command extract-glossary reads the vocabulary a bounded context speaks, as the GLOSSARY.md it is already written in, and answers with a catalog fragment.
extract-go
Package extractgo is portolan-extract-go: a Go service in, a catalog fragment out.
Package extractgo is portolan-extract-go: a Go service in, a catalog fragment out.
extract-go-nats
Package extractgonats is portolan-extract-go-nats: nats.go and JetStream calls in a Go repository in, the subjects the service listens on and publishes to out.
Package extractgonats is portolan-extract-go-nats: nats.go and JetStream calls in a Go repository in, the subjects the service listens on and publishes to out.
extract-graphql
Package extractgraphql is portolan-extract-graphql: a GraphQL schema in, a catalog fragment out.
Package extractgraphql is portolan-extract-graphql: a GraphQL schema in, a catalog fragment out.
extract-http-clients
Package extracthttpclients is portolan-extract-http-clients: outbound HTTP and SOAP calls in a Go repository in, source-backed dependencies and flows out.
Package extracthttpclients is portolan-extract-http-clients: outbound HTTP and SOAP calls in a Go repository in, source-backed dependencies and flows out.
extract-openapi
Package extractopenapi is portolan-extract-openapi: an OpenAPI document in, a catalog fragment out.
Package extractopenapi is portolan-extract-openapi: an OpenAPI document in, a catalog fragment out.
extract-project
Package extractproject is portolan-extract-project: a repository component in, a neutral catalog fragment out.
Package extractproject is portolan-extract-project: a repository component in, a neutral catalog fragment out.
extract-proto
Package extractproto is portolan-extract-proto: .proto files in, a catalog fragment out.
Package extractproto is portolan-extract-proto: .proto files in, a catalog fragment out.
extract-redis
Package extractredis is portolan-extract-redis: source-backed Redis client construction in a Go repository in, a Redis store owned by the service out.
Package extractredis is portolan-extract-redis: source-backed Redis client construction in a Go repository in, a Redis store owned by the service out.
extract-river
Package extractriver is portolan-extract-river: River job declarations, producers and registered workers in a Go repository in, work queues and job flows out.
Package extractriver is portolan-extract-river: River job declarations, producers and registered workers in a Go repository in, work queues and job flows out.
extract-sql
Package extractsql is portolan-extract-sql: the migrations of a service in, a catalog fragment describing where its state lives out.
Package extractsql is portolan-extract-sql: the migrations of a service in, a catalog fragment describing where its state lives out.
extract-watermill
Package extractwatermill is portolan-extract-watermill: Watermill router declarations and publications in a Go repository in, channels and source-backed flows out.
Package extractwatermill is portolan-extract-watermill: Watermill router declarations and publications in a Go repository in, channels and source-backed flows out.
extract-wsdl
Package extractwsdl is portolan-extract-wsdl: WSDL contracts and their local XSD graph in, structured SOAP interfaces in the catalog out.
Package extractwsdl is portolan-extract-wsdl: WSDL contracts and their local XSD graph in, structured SOAP interfaces in the catalog out.
gen-markdown
Package genmarkdown is portolan-gen-markdown: a catalog in, a directory of markdown out.
Package genmarkdown is portolan-gen-markdown: a catalog in, a directory of markdown out.
openapi
Package openapi is what two extractors agree on about an OpenAPI document: how its interfaces are named in the catalog, and which operation answers on which route.
Package openapi is what two extractors agree on about an OpenAPI document: how its interfaces are named in the catalog, and which operation answers on which route.
verify-codeowners
Command verify-codeowners says who to ask about each service, by reading the CODEOWNERS file the repository already keeps.
Command verify-codeowners says who to ask about each service, by reading the CODEOWNERS file the repository already keeps.
verify-otel
Command verify-otel reads OpenTelemetry traces and says which hops of the catalog have been seen running.
Command verify-otel reads OpenTelemetry traces and says which hops of the catalog have been seen running.
render
mermaid
Package mermaid renders catalog flows as Mermaid sequence diagrams.
Package mermaid renders catalog flows as Mermaid sequence diagrams.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL