Turborepo monorepo, pnpm workspaces. Toolchain and conventions follow
../masterdb-mgt-tool; the full plan lives in docs/PLAN.md, and the reasoning behind
it in docs/DESIGN-NOTES.md. Building a feature? Start
with CLAUDE.md: the working guide and checklist, which Claude
Code also loads on its own.
- Node >= 22
- pnpm >= 11 —
corepack enablepicks it up from thepackageManagerfield..npmrcsetsengine-strict=true, so an older pnpm fails the install rather than producing a subtly different one. - Docker —
pnpm devruns the local Postgres in a container. - On Windows, WSL2.
New computer? Follow docs/SETUP.md, from git clone
to signed in, with the database, seeding, optional services and
troubleshooting.
apps/
web-server/ NestJS — REST today, GraphQL + WS in Phase 3 :8080
web-app/ Next.js — web frontend :8081
packages/
web-ui/ React + Tailwind 4 + AG Grid Community
module-kit/ the module contract every app composes
module-auth/ sign-in, sessions, MFA, password reset
module-permissions/ organizations, workspaces, roles, features, plans, limits
module-chat/ conversations and messages
module-queuing-window/ walk-in queue: windows, lines, a live TV board
module-notification/ system notifications: bell, toasts, an inbox
No module imports another; only the apps depend on them. The map, and the
ports modules use instead, are in docs/DEPENDENCIES.md,
enforced by pnpm check:boundaries.
Feature modules ship as one package with a dependency-free core plus adapters
behind subpath exports — @kwtech/module-permissions, .../server, .../react.
Apps list their modules once and compose the rest:
export const WEB_MODULES = [permissionsWebModule, usersWebModule];
export const ROUTES = composeRoutes(WEB_MODULES);See packages/module-kit/README.md for the contract and docs/PLAN.md §9 before adding a module.
pnpm install
pnpm env:new local # envs/local/, with fresh secrets
# fill in SEED_USER_* (your sign-in) in envs/local/web-server.env — BEFORE the first start
pnpm dev # database created, migrated, filled and seeded; both apps runningEvery step, and what each one does, is in docs/SETUP.md.
Each environment is a profile: envs/<name>/web-server.env and
envs/<name>/web-app.env, gitignored. The active one is symlinked to
apps/web-server/.env.local and apps/web-app/.env.local (both apps use the
same file name), so switching is one
command and every tool (Nest, Next, Prisma, the seeders) follows it.
pnpm env:show # active profile, its database and API (no secrets printed)
pnpm env:new staging # create a profile from the templates, with fresh secrets
pnpm env:use staging # switch both apps
pnpm db:deploy # …now runs against staging
pnpm env:use local # back
pnpm env:check # templates hold no secrets; profiles lack no variable- Development defaults to
local.pnpm devactivates it and creates it the first time. A non-local profile prints a warning banner on everypnpm dev. APP_ENVin each profile says what it is, and the guards trust it:db:migrateanddb:snapshotrun only on local, anddb:restorenever touches production.- Deployed hosts don't use profile files. Set the same variables in the host
(Vercel, the container host), including
APP_ENV. A production build refuses to boot without it. .env.examplefiles are templates committed to a public repo. A value for any*_PASSWORD,*_SECRET,*_KEYor*_EMAILfails the commit.- Keep each profile in a password manager too. That's how a new machine gets them.
On another machine, including one with an old apps/web-server/.env: git pull && pnpm install && pnpm env:show moves the old files into envs/local/
without losing anything, and pnpm env:check lists what's new. The full steps
for each case are in
envs/README.md → Setting up another machine.
pnpm dev starts everything — every package in watch mode plus both apps:
| API | http://localhost:8080/api/v1 |
| API docs (Swagger) | http://localhost:8080/api/v1/docs |
| Web | http://localhost:8081 |
| Sign in | http://localhost:8081/auth/signin |
Editing a packages/* file rebuilds it and the running apps pick the change up;
there is no separate build step while developing.
pnpm dev # everything: package watchers + both apps
pnpm dev:api # just the API, and the packages it needs
pnpm dev:web # just the frontend, and the packages it needs
pnpm build # everything, in dependency order
pnpm start # run the built apps
pnpm typecheck
pnpm test
pnpm lint # biome — one tool, no eslint, no prettier
pnpm check:fix # lint + format, writing fixesDatabase (all scoped to apps/web-server, which owns the schema — PLAN §12.2):
pnpm --filter @kwtech/web-server db:compose # copy module prisma fragments in
pnpm --filter @kwtech/web-server db:generate # compose + generate the client
pnpm --filter @kwtech/web-server db:migrate # compose + migrate dev
pnpm --filter @kwtech/web-server db:seed # idempotent first user
pnpm --filter @kwtech/web-server db:snapshot # dev data → seed-data/snapshot.json
pnpm --filter @kwtech/web-server db:restore # seed-data/snapshot.json → empty database
pnpm --filter @kwtech/web-server db:studioPostgres is expected where DATABASE_URL points, localhost:5432 by
default. pnpm dev and pnpm dev:api run scripts/dev-db.mjs first: if
nothing answers on that port it starts a kwtech-postgres Docker container
(creating it the first time, with its data in the kwtech-pgdata volume), and
on a brand-new volume applies the migrations and seeds. A native Postgres
already on the port is used as-is.
pnpm db:up # start (or create) the container without starting the apps
pnpm db:down # stop it; the data stays in the volumeShared dev data. apps/web-server/seed-data/snapshot.json is a committed
copy of a dev database's rows, so a second machine starts where the first left
off. A brand-new container restores it automatically; against a native
Postgres, or to catch up with a newer snapshot, run it yourself:
pnpm db:snapshot # this database → snapshot.json (commit it)
pnpm db:restore # snapshot.json → an empty, migrated database, then db:seed
pnpm db:restore --force # the same, emptying the database firstThe repository is public, so the snapshot carries no credentials: no
password hashes, MFA secrets, sessions or reset tokens. After a restore the
SEED_USER_* and SEED_DEMO_USER_* accounts sign in with the passwords in your
.env; any other account uses forgot-password, whose link is printed in the
API console while SMTP_URL is unset. Everything else in it — names, emails,
chat messages — is readable by anyone, so keep real customer data out of dev.
Docker Engine inside WSL2, once (/etc/wsl.conf needs systemd=true):
sudo apt update && sudo apt install -y docker.io
sudo usermod -aG docker $USER && sudo systemctl enable --now docker
# then open a new terminal so the group membership applies- Biome is the only linter/formatter — no ESLint, no Prettier.
- Packages extend the root
tsconfig.base.jsondirectly; there is notypescript-configpackage. - Shared dependency versions go in the pnpm
catalog:inpnpm-workspace.yaml. turbo.jsonandbiome.jsoncare JSONC — comment anything non-obvious.- Conventional commits, enforced by lefthook + commitlint.