Skip to content

Architecture

Nexul is one Go module (github.com/otal-labs/nexul) that builds a server binary and a runner binary, with a cli binary planned for the same module. Around them sit a React web app, a thin Electron desktop shell, a TypeScript SDK, and a small automations host — all in the same repository so the seams between them stay explicit instead of accidental.

Business logic lives under internal/<domain>/, one directory per bounded context — tickets, deploy, docs, topology, and so on. A domain owns its model, its storage interface, its use-cases, and the events it produces and consumes, and it never imports another domain directly. Most domains follow a five-file shape: model.go, repo.go, usecase.go, handler.go, events.go. Domains with no events yet, or a thinner shape, are exempt until they need it. See Repository Layout for the full directory map.

Every capability the product offers is a use-case function. Two adapters call it, and neither duplicates it:

  • HTTP/JSON gateway — what the browser talks to, and what a third-party integration talks to with a scoped API token instead of a session.
  • MCP server — the Model Context Protocol adapter LLM agents use to drive the product: search docs, create tickets, read topology, trigger deploys, replay a dead letter.

If a capability exists in the UI, it exists in MCP by construction, and vice versa. Neither adapter carries business logic of its own — each parses input, calls the use-case, and formats the output.

One SQLite file, one writer, WAL mode for concurrent readers, FTS5 for full-text search over docs and tickets. Queries are written by hand in .sql files under internal/platform/storage/queries/, and sqlc compiles them into internal/platform/storage/sqlcgen/ against the migrations directory — no Postgres, no dual-backend abstraction.

Domains that need to react to each other’s changes do it through the EventBus interface in internal/platform/eventbus/, never through direct imports. Today the bus is in-process and channel-backed — one process, zero network hops. If a domain ever needs to become its own service, the bus implementation swaps to NATS via Watermill and the domain code doesn’t change: it still calls Publish/Subscribe on the same interface. Critical events go through a transactional outbox, written in the same SQLite transaction as the domain change, so an event can never be lost or published for a change that rolled back.

web/ is React 19 + Vite + Tailwind, built with shadcn/ui components. It talks to the server over the HTTP/JSON gateway for requests and a WebSocket for live events — never over MCP; the browser never speaks JSON-RPC.

desktop/ is a thin Electron shell over the same served web app. It exists for consistent rendering on Linux, not to host a second UI: it bootstraps a connection token and otherwise stays out of the way.

sdk/ is the TypeScript package automations are written against — a client, an API client, generated event types, a config schema, and a testing surface. automations/ is the small container bundled with every instance that runs Default automations and small Custom ones; bigger Custom automations run wherever their owner deploys them.