RyTask docs

Architecture overview

A modular monolith with hard, machine-enforced boundaries — one codebase, one image, many clients.

View as MarkdownOpen in ChatGPTOpen in Claude

A modular monolith, on purpose

RyTask's backend is a single NestJS application split into modules with hard boundaries — identity, orgs, projects, work-items, comments, views, search, notifications, slack, and time-tracking. It deploys as one process and one database, which keeps self-hosting simple (the whole point of the product), while the boundaries keep each module extractable into its own service later without a rewrite. You get microservice discipline without microservice operations.

What a module is here

A module exposes exactly two public surfaces:

  • a contract — a *.contract.ts file declaring the service interface other modules may call, and
  • its published events — domain events under events/ that other modules may subscribe to.

Everything else — providers, repositories, domain policies, internal services — is private. A module never imports another module's repositories and never queries another module's tables directly. Cross-module reads go through the owning module's contract or a read-model it publishes.

Boundaries are enforced, not promised

The rules above are not a convention to remember in code review. They are checked by dependency-cruiser in CI (pnpm check:boundaries, configured in packages/config/dependency-cruiser.cjs), with three rules:

  • no-cross-module-internals — a module may only reach another module through its *.contract.ts or its events/; importing another module's services, providers, repositories, or domain code fails the build.
  • no-raw-db-outside-repositories — direct imports of the database package are only allowed inside repositories/ and the tenancy machinery, so every query flows through a tenant-scoped repository (see multi-tenancy).
  • no-circular — circular dependencies make modules non-extractable, so they are an error anywhere.

A boundary violation is a red build, the same as a failing test.

One codebase, one image, two roles

The API server and the background worker are the same codebase and the same Docker image. The entrypoint in apps/api/src/main.ts checks one environment variable: with WORKER=1 it boots the BullMQ background processors, otherwise it boots the HTTP server. Domain logic lives once; request handling and background processing scale independently by running more of either role.

Async work — notifications, Slack delivery, scheduled scans — goes through BullMQ on Redis. Jobs are idempotent and safe to retry, and every queue processor has an integration test that enqueues, processes, and asserts, including on replay.

API-first, no back doors

The REST API and the domain events are the contract. The web app, the Slack bot, and the MCP server are all clients of the same services — none of them gets a private side channel into the database or a special-cased code path. An MCP tool dispatches to the same service its REST sibling calls, under the same permissions and the same tenant context. This is what makes full MCP parity sustainable: there is only one surface to keep in parity with.

Ports and adapters at the edges

External I/O sits behind interfaces in apps/api/src/common: Clock, IdGenerator, PasswordHasher, Mailer, Crypto, and the Slack and GitHub ports, among others. Domain logic depends on the port, never the vendor library, so policies and providers are pure and testable without infrastructure — and swapping an adapter (a different mailer, a fake clock in tests) touches one file.

The monorepo at a glance

PathWhat lives there
apps/apiThe NestJS backend — API and worker, selected by WORKER=1.
apps/webThe Next.js web app.
apps/docsThis documentation site.
packages/dbThe Drizzle schema (src/tables.ts is the single source of truth), migrations, and seed.
packages/contractsShared DTOs, the MCP tool registry, and API schemas — one contract, no drift.
packages/uiShared React components and the design tokens.
packages/configShared tsconfig, Biome, Vitest, and boundary-rule presets.
packages/sdkThe generated TypeScript client.
infra/dockerDockerfiles and the Compose stack.
scriptsThe CI gates — required tests, MCP parity, design tokens.

For how these pieces are kept honest in CI, see testing and quality gates.

On this page