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-sdkfacade - 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.