← Back to docs

Modular architecture

Built-in modules, external modules, registries and contract checks

Functionality is organized into modules that can be switched on per deployment. apps/server and apps/client are thin assembly layers; the logic lives in module packages.

What a module contains

Part Holds
shared/ Cross-end contracts: types, validation, pure logic
server/ Routes, business logic, that module's Prisma schema
client/ Pages, routes, translations

Each module implements ServerAppModule / ClientAppModule in module.ts / module.tsx, registering its routes, navigation, permissions and copy with the kernel.

Built-in modules

Under packages/builtin/*, shipped with the platform:

Module Responsibility
user Users and JWT authentication
rbac Roles and permissions
platform Platform console: tenants, plans, quotas
marketing Site CMS: page layout, docs library, member entrances
site-member Site member identity
billing Subscriptions and payments
audit Audit log
notification In-app notifications
background-job Job centre
error-log / slow-query Observability
dashboard Workspace card aggregation

External modules

Third-party and bespoke features live in modules/* at the repo root (the shipped examples are note, todo and bookmark):

  • They talk to the kernel only through the @rewindom/module-sdk facade
  • Import boundaries are enforced by verify-module.mjs
  • They bring their own Prisma schema; migrations stay with apps/server

See Installing an external module.

Modules never import each other

A module cannot import another module's code. Cross-module work goes through kernel extension points: the event bus, providers, slots. requires declares dependency and load order only — it does not grant code-level access.

The reason is one-directional layering: the kernel and infrastructure don't know about business modules, business modules don't know about each other, and any one of them can be deleted without disturbing the rest.

Registries

Two registries decide what is enabled:

Registry Path
Server enabled modules apps/server/src/enabled-modules.ts
Client enabled modules apps/client/src/enabled-modules.ts

Order is infrastructure → shell → business, so dependencies load before their users. The external-module section is generated by pnpm gen:external-modules — don't hand edit it.

The route prefix is a hard rule

A new module's renderRoutes / nav.path / mobileTabPaths must use the /app/<module> prefix (/app/site, /app/notes). Routes mounted at the top level get swallowed by the CMS on tenant domains. The reasoning is in Host-based routing.

Generating and checking

pnpm gen:module <spec.yaml>          # scaffold a module from a spec
pnpm check:modules                   # registries, tenant columns, toggles, permissions, order, shell, nav, import boundaries
pnpm check:deps                      # circular dependency detection
node scripts/verify-module.mjs <id>  # check a single module

The standard path is: write the spec → gen:module → fill in the logic → check:modules.