One page for "what may import what", and how features that may not import
each other still work together. The rules come from PLAN §9 and CLAUDE.md;
pnpm check:boundaries enforces them (it is also the last step of
pnpm lint), so this page and the code cannot quietly drift apart.
@kwtech/web-ui @kwtech/module-kit
(no @kwtech deps) (no @kwtech deps)
▲ ▲
optional peer │ │ dependency
│ │
┌────────────────────┴─────────────────────┴───────────────────┐
│ module-auth module-permissions module-chat │
│ module-queuing-window module-notification module-app-hub │
│ module-note module-task module-basic-pos │
│ module-basic-bookkeeping module-jobs module-booking │
│ module-print-studio │
│ │
│ Each depends on module-kit ONLY, with web-ui, React and │
│ Nest as OPTIONAL peers. None imports another module. │
└──────────────────────────────▲────────────────────────────────┘
│ dependency (all of them)
apps/web-server · apps/web-app
the ONLY place modules meet
Peers are listed as each package.json has them; "Nest" is @nestjs/common,
@nestjs/graphql and reflect-metadata.
| Package | @kwtech dependencies |
Peers | Imported by |
|---|---|---|---|
web-ui |
none | react, react-dom, tailwindcss, lucide-react, AG Grid, Radix dropdown | modules' /react, web-app |
module-kit |
none | react, graphql-ws | every module, both apps |
module-auth |
module-kit | web-ui, Nest, React, Next, jsonwebtoken | both apps |
module-permissions |
module-kit | web-ui, Nest, React, read-excel-file | both apps |
module-chat |
module-kit | web-ui, Nest, React, react-dom | both apps |
module-queuing-window |
module-kit | web-ui, Nest, React | both apps |
module-notification |
module-kit | web-ui, Nest, React, react-dom | both apps |
module-app-hub |
module-kit (and @dnd-kit/*) |
web-ui, Nest, React | both apps |
module-note |
module-kit | web-ui, Nest, React | both apps |
module-task |
module-kit (and @dnd-kit/*) |
web-ui, Nest, React | both apps |
module-basic-pos |
module-kit | web-ui, Nest, React | both apps |
module-basic-bookkeeping |
module-kit | web-ui, Nest, React | both apps |
module-jobs |
module-kit | web-ui, Nest, React | both apps |
module-booking |
module-kit | web-ui, Nest, React | both apps |
module-print-studio |
module-kit (and pdf-lib, pdfjs-dist, heic2any, all used in the browser only) |
web-ui, Nest, React | both apps |
web-server |
every module, module-kit | — | nothing |
web-app |
every module, module-kit, web-ui | — | nothing |
⚠ Nothing depends on module-auth except the apps. Chat, the queue and
notifications do not know what a session is: the app tells each of them who
is calling (resolveActorId). Switch any feature module off — or leave it out
of an app — and the others still build and run.
src/index.ts, src/domain/, types.ts, the pure core: no framework at all
feature-keys.ts, operations.ts (no Nest, React, Next, Prisma)
▲ ▲
src/server/ (→ /server) src/react/ (→ /react)
Nest, GraphQL React, web-ui
✗ /react never imports /server — server code stays out of the browser
A module that needs something another module knows DECLARES a port (a small
interface and a DI token). The app, which depends on both, implements it in
apps/web-server/src/<module>/ and binds it in app.module.ts. An unbound port
has a documented, fail-closed meaning — see each module's README.
| A module needs… | Declared as | The app answers with |
|---|---|---|
| Who is calling | resolveActorId / resolvePrincipal option |
auth/resolve-principal.ts — where auth and permissions meet |
| May this caller do it (feature keys) | the registry's bindings |
FeatureGuard from module-permissions, fed by seed/registry.ts |
| How many may there be (caps) | CHAT_LIMIT_CHECKER, QUEUE_LIMIT_CHECKER, NOTE_LIMIT_CHECKER, TASK_LIMIT_CHECKER, POS_LIMIT_CHECKER, BOOKING_LIMIT_CHECKER, STUDIO_LIMIT_CHECKER |
PermissionsLimitChecker |
| People's names / emails | CHAT_USER_DIRECTORY, QUEUE_STAFF_DIRECTORY, NOTIFICATION_USER_DIRECTORY, NOTE_AUTHOR_DIRECTORY, TASK_MEMBER_DIRECTORY (also who may be assigned, and who has left), POS_MEMBER_DIRECTORY (names for "by staff"), BOOKS_MEMBER_DIRECTORY (who recorded and voided each entry), BOOKING_MEMBER_DIRECTORY (also who works the desk: who a staff resource may be, and who is reminded), STUDIO_MEMBER_DIRECTORY (who owns a shared layout, who printed what) |
adapters reading auth_user (and permissions, for tasks' members) |
| Is this person staff here | QUEUE_STAFF_CHECK |
queue/staff-check.ts (permissions) |
| Does this person hold a key the module checks itself | NOTE_ACCESS_CHECK (note:manage_all), TASK_ACCESS_CHECK (task:assign, task:manage_all), POS_ACCESS_CHECK (who sees costs; whose edit keeps a fixed discount), BOOKS_ACCESS_CHECK (who may void an investor's entry), STUDIO_ACCESS_CHECK (studio:manage_all: change somebody else's shared layout, read everybody's print history) |
note/access-check.ts, task/access-check.ts, pos/access-check.ts, books/access-check.ts, studio/access-check.ts (permissions) |
| A workspace's time zone (which day a sale belongs to; the TV board's clock) | POS_WORKSPACE_TIME_ZONE, BOOKS_WORKSPACE_TIME_ZONE, BOOKING_WORKSPACE_TIME_ZONE (what 9:00 means there); QueueWorkspaceLocation.timeZone; in the browser, useWorkspaceTimeZone() from module-kit |
pos/workspace-time-zone.ts and queue/workspace-locator.ts (read perm_workspace); the shell's WorkspaceTimeZoneProvider |
| A point of sale's takings for some days (the books' sales and cost of goods) | BOOKS_SALES_SOURCE |
books/sales-source.ts, calling the POS's own PosReportService.takings (the books import the POS's Nest module instance) |
| Live events | CHAT_PUBSUB, QUEUE_PUBSUB, PERMISSIONS_PUBSUB, NOTIFICATION_PUBSUB, NOTE_PUBSUB, TASK_PUBSUB, POS_PUBSUB, BOOKS_PUBSUB, BOOKING_PUBSUB |
ONE realtimePubSub() engine for all |
| A database | X_PRISMA, X_PRISMA_WRITE |
prisma/module-clients.ts, checked by satisfies-modules.ts |
| Email a person | CHAT_NOTIFIER, auth's and permissions' mail callbacks |
chat/notify-mail.ts, auth/*-mail.ts, permissions/invitation-mail.ts |
| Tell a person something (in the app) | X_NOTIFIER declared by the producing module — the first is TASK_NOTIFIER (task/notifier.ts), then BOOKING_NOTIFIER (booking/notifier.ts) |
an adapter calling module-notification's NotificationSender — see its README |
| Run something on a schedule | processes on the module's server descriptor (ProcessContribution, module-kit) — the first is task.due_today, then booking.upcoming_sessions, then studio.prune_logs | composeProcesses(…) handed to jobsServerModule in app.module.ts; the declarations again in seed/registry.ts for db:sync |
| Which workspaces a background process may reach (the organization's plan) | JOBS_ENTITLED_WORKSPACES | jobs/entitled-workspaces.ts (reads permissions' plans and subscriptions) |
| Who paused, forced or rescheduled a background process, by name | JOBS_ACTOR_DIRECTORY | jobs/actor-directory.ts (reads auth_user) |
Shared shapes (a pub/sub port, a directory entry) are COPIED structurally in
each module, not imported — a duplicate interface is cheaper than a coupling.
Something moves into module-kit only when a second module needs the same
code, not just the same shape (PLAN §9 rule 8).
- A
module-*package imports only@kwtech/module-kitand@kwtech/web-uifrom@kwtech/*(and itself). module-kitandweb-uiimport no other@kwtechpackage.- No package imports an app, or reaches into another package's
src/— public entry points only. - A module's
/reactnever imports its/server. - A module's pure core imports no framework.
- A module's
package.jsonlists onlymodule-kitandweb-uiamong@kwtech/*.
Imports inside comments are ignored, because doc comments quote imports as examples. A failure names the file, the line and the rule.