Tech stack and code structure¶
How BizOS code is organised and which technologies it uses. Rationale for the core choices is in ADR-0002, ADR-0003, ADR-0004, ADR-0006, ADR-0007, ADR-0008, ADR-0009, ADR-0010, ADR-0011 (superseded), and ADR-0012.
Stack¶
Decided items are agreed. Proposed items are the default until an ADR accepts or replaces them.
| Layer | Choice | Status |
|---|---|---|
| Design approach | Domain-Driven Design: modules (level 1) contain features (level 2) | Decided |
| Frontend | React | Decided |
| Backend | NestJS | Decided |
| Language | TypeScript (strict) for frontend, backend, and shared packages | Proposed |
| Deployment shape | Modular monolith: one API app and one worker app, split by module inside; every module can be extracted into its own service (see Module communication and extraction) | Proposed |
| Frontend build | Vite | Proposed |
| Frontend data and forms | TanStack Query (server state), React Router, React Hook Form with Zod | Proposed |
| API style | REST with OpenAPI generated from NestJS; typed client generated for the frontend | Proposed |
| Commands, queries, events | @nestjs/cqrs inside each module; integration events between modules |
Proposed |
| Database | PostgreSQL | Decided |
| Tenancy model | Single PostgreSQL database with shared schema multi-tenancy (ADR-0012) | Decided |
| Background jobs | Redis with BullMQ (@nestjs/bullmq) |
Proposed |
| File storage | S3-compatible object storage | Proposed |
| Repository layout | Nx monorepo with pnpm workspaces | Decided |
| Backend code generation | NestJS CLI 11 (root dev dependency, nest-cli.json at the root) for elements inside a module; Nx generators for apps and libraries |
Decided |
| Module granularity | One Nx library per module and platform (libs/api/<module>, libs/web/<module>), plus one contract library per backend module once another module uses it (libs/api/contracts/<module>) |
Decided |
| Testing | Jest (backend), Vitest and Testing Library (frontend), Playwright (end-to-end); see Testing | Decided |
| ORM and migrations | Drizzle ORM with node-postgres (pg); drizzle-kit for migrations |
Decided |
| Local database | PostgreSQL 17 in Docker Compose (docker-compose.yml) |
Proposed |
| CI and delivery | Gitea Actions: ci.yml (lint, test, build) on every push and pull request; deploy.yml started by hand builds Docker images for api, web, admin, docs (MkDocs specs site), pushes them to the Gitea registry, migrates, and switches the environment. No automatic deploys |
Decided |
| UI components and styling | shadcn/ui (Radix base, Nova preset, Lucide icons) on Tailwind CSS 4, themes astrovista (default, dark) and twitter, in libs/shared/ui |
Decided |
Levels¶
| Level | Name | DDD meaning | Example |
|---|---|---|---|
| 1 | Module | A bounded context: owns its domain model, its tables, its API, and its UI routes | crm |
| 2 | Feature | A vertical slice inside a module: one capability with its use cases, endpoints, and screens | crm/opportunities |
A module is the unit of ownership: one owner, one folder, one public API. A feature is the unit of delivery: small enough for one person in a few days. Specs follow the same split: a spec area maps to a module and a spec file to a feature (for example specs/admin-portal/02-tenant-management.md maps to admin/tenant-management).
Modules¶
| Module | Scope | Example features |
|---|---|---|
iam |
Identity and access for tenant users | sign-in, sessions, password-reset, my-profile, roles-permissions, user-management, audit-log, notification-center |
system |
Tenant-level system administration done by tenant admins inside their workspace | company-profile, settings, reference-data |
tenancy |
Tenant registry, tenant resolution from subdomain, per-tenant databases and migrations | tenant-registry, tenant-resolution, tenant-migrations |
onboarding |
Sign-up and automated tenant provisioning | sign-up, provisioning, default-configuration |
billing |
Plans, entitlements, subscriptions, invoices, payments | plan-catalog, entitlements, subscriptions, invoices |
admin |
Internal admin portal for operators (see specs) | operator-access, tenant-management, provisioning-monitor, backup-restore, system-health |
crm |
Customers and sales | customers, contacts, opportunities, products, transactions |
contracts |
Contract tracking | contracts, expiry-alerts |
tasks |
Task management | task-board, assignments |
data-import |
Excel/CSV import | file-upload, column-mapping, import-jobs |
integrations |
External systems | api-keys, public-api, inbound-webhooks |
dashboard |
Executive KPIs | kpi-calculation, dashboard |
assistant |
AI assistant | ai-gateway, chat, draft-and-confirm |
Adding a module needs an ADR; adding a feature inside a module does not.
Libraries created so far: libs/api/ iam, system, tenancy, crm, admin (all imported by apps/api) and shared; libs/web/ iam, admin. The other modules get a library when their first feature starts. Contract libraries: libs/api/contracts/tenancy (@bizos/api-tenancy-contracts), whose TenantDirectory port (tenant counts and tenants by state) is bound by tenancy and used by the admin dashboard.
Repository layout¶
apps/ Thin shells: bootstrap, configuration, wiring of module libraries
api/ NestJS HTTP API
worker/ NestJS standalone process for background jobs (no HTTP server)
web/ React app for tenant users (<tenant>.<root-domain>)
admin/ React app for the admin portal (admin.<root-domain>)
<app>-e2e/ Playwright end-to-end tests for one React app, e.g. apps/web-e2e
libs/
api/<module>/ One NestJS library per backend module, e.g. libs/api/tenancy
api/contracts/<module>/ What other modules may use from a module: ports, their types, integration events
api/shared/ Backend shared kernel: tenant context, auth guards, base types, event bus
web/<module>/ One React library per frontend module, e.g. libs/web/admin
shared/ui/ Shared React components and design tokens
shared/api-client/ Typed client generated from the OpenAPI spec
db/ drizzle-kit configs, generated migrations, local database init
architecture/ This folder
specs/ Functional specs
Every module is an Nx library with its own package.json, named @bizos/<platform>-<module> (for example @bizos/api-admin); a contract library is named @bizos/api-<module>-contracts. Apps import modules by that name only; deep imports into a library are rejected by lint.
Backend module (Clean Architecture)¶
Every backend module (libs/api/<module>) follows Clean Architecture (Hexagonal Ports and Adapters) with strict dependency inversion (ADR-0010):
flowchart TD
subgraph Presentation["1. Presentation Layer (features/)"]
ctrl["Controllers (REST endpoints)"]
guard["Guards & Middlewares"]
end
subgraph Application["2. Application Layer (features/)"]
service["Use Case Services"]
cqrs["Command & Query Handlers"]
dto["Validated Request & Response DTOs"]
end
subgraph Domain["3. Domain Core (domain/)"]
entity["Entities & Value Objects"]
rules["Pure Business Rules"]
port["Repository Interfaces (Ports)"]
end
subgraph Infrastructure["4. Infrastructure Layer (infrastructure/)"]
drizzle["Drizzle ORM Schemas & Repositories"]
db["Database Connection & Pools"]
client["External Adapters"]
end
Presentation --> Application
Application --> Domain
Infrastructure -. "implements" .-> port
Application -. "injects interface" .-> port
libs/api/crm/
package.json @bizos/api-crm, Nx tags
src/
index.ts Exports only the NestJS module, for apps to import
lib/
api-crm.module.ts NestJS module, wires everything below and binds the module's ports
domain/ Pure core: Entities, value objects, domain events, repository interfaces
features/
opportunities/ Vertical slice per feature
opportunities.controller.ts Presentation: REST endpoint, HTTP status, auth guard
opportunities.service.ts Application: Use case orchestration
dto/ Validated request & response shapes
opportunities.spec.ts Pure unit tests with mocked repository ports
customers/
infrastructure/ Adapters: Drizzle schemas, repository implementations, external clients
Layer Responsibilities & Invariants:¶
- Domain (
domain/): Pure business logic with zero framework or database dependencies. Entities and value objects enforce business rules; repository interfaces specify storage contracts. - Application (
features/<feature>/): Use cases orchestrate domain logic and call repository interfaces. Injects tokens (@Inject(USER_REPOSITORY)), never concrete infrastructure classes. - Presentation (
features/<feature>/): Receives HTTP requests, parses payloads via DTOs, executes auth guards, and delegates to application use cases. - Infrastructure (
infrastructure/): Implements repository interfaces with Drizzle ORM, executes SQL, and handles database connections.
Dual Identity and Tenancy Architecture¶
BizOS enforces clear separation between platform operations and customer business data (ADR-0012, USR spec):
flowchart LR
subgraph Realm1["System & Operations Realm"]
opUser["Operator Accounts<br/>(Owner, Operator, Support)"] --> adminHost["admin.<root-domain><br/>apps/admin"]
adminHost --> adminApi["libs/api/admin"]
adminApi --> controlData[("bizos_dev<br/>Platform & Control Data")]
end
subgraph Realm2["Customer & Tenant Realm"]
custUser["Tenant Users<br/>(Admin, Manager, Member, Guest)"] --> tenantHost["{tenant}.<root-domain><br/>apps/web"]
tenantHost --> tenantApi["libs/api/iam, tenancy, crm"]
tenantApi --> tenantData[("bizos_dev<br/>Tenant Data (tenant_id)")]
end
- Credential Isolation: Operator accounts are global platform records; tenant user accounts are scoped by
tenant_id. Customer credentials cannot authenticate the admin portal. An authenticated operator can open a tenant Customer Portal without a customer password (ambient SSO): the API provisions a matching IAM platform user if needed, issues a tenant-realm session, and tenant APIs may accept an operator JWT whenx-bizos-operator-accessis set. - Routing & Subdomains: The hostname determines identity realm (
admin.<root-domain>vs<tenant>.<root-domain>).
Contract library¶
libs/api/contracts/crm/
package.json @bizos/api-crm-contracts, Nx tags
src/
index.ts Everything other modules may use from crm
lib/
customer-directory.port.ts Abstract class: the port and its injection token
customer-directory.types.ts Serializable request and response types
events/
customer-created.event.ts Integration event: type name, version, payload
Frontend module (Feature-Based Architecture)¶
Frontend code for both the Tenant Web App (apps/web) and the Admin Portal (apps/admin) follows a Feature-Based Architecture (Feature-Driven structure). Instead of organizing files by technical type (controllers/, views/, models/), every business capability is organized as a self-contained feature folder under libs/web/<module>/src/lib/features/<feature>/.
libs/web/<module>/
package.json @bizos/web-<module>, Nx tags (platform:web, type:module, scope:<module>)
src/
index.ts Public API: exports module routes and root views
lib/
routes.tsx Module router linking feature pages
features/
<feature-name>/ Self-contained feature slice (e.g. user-management, sign-in)
pages/ Route-level screen components (e.g. UserManagementPage)
components/ UI components used exclusively by this feature (e.g. UserTable, AddUserDialog)
api/ API client fetch calls and server state mutations for this feature
hooks/ Custom React hooks encapsulating feature state and data workflows
types/ Feature-specific interfaces and DTOs (e.g. ManagedUser)
<feature>.spec.tsx Co-located component and integration tests
Core Principles for Frontend:¶
- Self-Contained Vertical Slices: All assets required for a feature (page, components, API calls, types, tests) live together inside that feature folder. Deleting or refactoring a feature is clean and contained.
- Thin App Shells:
apps/webandapps/adminare thin application shells. They configure the HTML entry point, root CSS, router context, and mount module routes (<IamRoutes>,<AdminRoutes>). They contain zero business logic or feature components. - Design System Boundary (
libs/shared/ui): Reusable design system primitives (Button, Input, Dialog, Table, Badge, Sidebar) live inlibs/shared/ui. They are stateless, presentation-only, and completely decoupled from business domain models. Feature components insidelibs/web/<module>compose these UI primitives with domain logic. - Co-location over Separation by Type: Keep tests, styles, and API clients as close to where they are consumed as possible.
Nx tags and dependency rules¶
Each project has three tags, and eslint.config.mjs at the root enforces the rules below.
| Tag | Values | Rule |
|---|---|---|
type |
app, module, contract, shared, e2e |
app may use module, contract, and shared; module may use contract and shared, never another module; contract may use only contract; shared may use only shared; e2e may use only shared |
platform |
api, web |
A project may depend only on projects of the same platform |
scope |
module name or shared |
Identifies the module; used for ownership and future rules |
Adding a module¶
# Backend
npx nx g @nx/nest:library libs/api/<module> --name=api-<module> --importPath=@bizos/api-<module> \
--unitTestRunner=jest --strict --tags="platform:api,type:module,scope:<module>"
# Backend contracts, when another module first needs something from <module>
npx nx g @nx/js:library libs/api/contracts/<module> --name=api-<module>-contracts \
--importPath=@bizos/api-<module>-contracts --bundler=none --unitTestRunner=none \
--tags="platform:api,type:contract,scope:<module>"
# Frontend
npx nx g @nx/react:library libs/web/<module> --name=web-<module> --importPath=@bizos/web-<module> \
--bundler=none --unitTestRunner=vitest --tags="platform:web,type:module,scope:<module>"
Then create domain/, features/, and infrastructure/ under src/lib/, add the library to the consuming app's (or module's) package.json as "@bizos/<name>": "workspace:*", and run pnpm install and npx nx sync.
Adding code inside a backend module¶
Use the NestJS CLI with the target folder as the last argument. Paths are relative to the repository root, and the element is registered in the nearest NestJS module.
npx nest g controller tenant-management libs/api/admin/src/lib/features
npx nest g service tenant-management libs/api/admin/src/lib/features
npx nest g guard tenant-state libs/api/tenancy/src/lib/infrastructure --dry-run # preview first
Do not use nest g application, library, or sub-app; create apps and libraries with Nx as above.
Module communication and extraction¶
Every backend module is written so it could run as its own service later, without changing its callers or its business code (ADR-0008). The module, not the feature, is the unit of extraction.
flowchart LR
subgraph api["apps/api (one process today)"]
tasks["tasks module"] -- "injects" --> port["CustomerDirectory port<br/>@bizos/api-crm-contracts"]
crm["crm module"] -- "binds" --> port
end
crm -- "crm.customer-created" --> bus[("Integration events<br/>outbox + BullMQ")]
bus -- "delivers at least once" --> tasks
| Need | Use | Never |
|---|---|---|
| Read data owned by another module | Its port (batch reads), or a local copy kept up to date from its events | Import its module library, join or read its tables |
| React to something that happened in another module | Subscribe to its integration event | Be called from inside its transaction |
| Change data owned by another module | A command on its port, or an event it reacts to | Write its tables |
Contracts¶
- A contract library holds ports (abstract classes), their request and response types, and integration event types. It imports only other contract libraries: no NestJS, Drizzle, or entity types, and no functions or classes with behaviour.
- Types are serializable: plain objects, strings, numbers, booleans, arrays; dates as ISO 8601 strings; ids as UUID strings.
- Ports offer batch reads (
getByIds(ids)) so callers never loop over remote calls. - Change contracts compatibly: add optional fields; for a breaking change add a new port method or a new event version and keep the old one until no consumer uses it.
- The module that implements ports is decorated
@Global()and exports only its port bindings, so other modules inject the port without importing the module. The app chooses the implementation by importing the module; after extraction it imports a module that binds the same ports to HTTP adapters instead.
// libs/api/contracts/crm: the port
export abstract class CustomerDirectory {
abstract getByIds(ids: string[]): Promise<CustomerSummary[]>;
}
// libs/api/crm: the binding
@Global()
@Module({
providers: [{ provide: CustomerDirectory, useClass: CustomerDirectoryService }],
exports: [CustomerDirectory],
})
export class ApiCrmModule {}
// libs/api/tasks: the caller
constructor(private readonly customers: CustomerDirectory) {}
Integration events¶
- Domain events from
@nestjs/cqrsstay inside their module. A module publishes an integration event only when another module needs to react. - Envelope:
id(UUID),type(<module>.<event>in kebab case, e.g.crm.customer-created),version(integer),occurredAt,tenantId(null for control-plane events),correlationId,payload. - An event is written in the same transaction as the change that caused it (transactional outbox) and delivered at least once by
apps/worker. Consumers are idempotent (deduplicate onid) and do not rely on order across event types.
HTTP, jobs, and configuration¶
- All controllers of a module are served under
/api/<module>/:@Controller('<module>/<feature>')behind the globalapiprefix. - BullMQ queue names start with the module name, e.g.
data-import.rows. - Module settings are environment variables prefixed with the module name in upper snake case (
CRM_,DATA_IMPORT_). A module never reads another module's settings; shared infrastructure settings (database URLs, Redis) come from@bizos/api-shared.
Extracting a module¶
Extract only with a concrete reason: independent scaling (for example assistant), heavy background load (data-import), fault isolation or a separate release cadence (integrations), or a separate team. Then:
- Create
apps/<module>-servicethat imports the module and its job handlers. - Give the module its own database (one per tenant if it uses tenant data): move its PostgreSQL schema and start a separate migration folder from a baseline of that schema.
- In
apps/apiandapps/worker, replace the module import with a module that binds the same ports to HTTP adapters. Callers do not change. - Move the module's event consumers to the new app.
- Route
/api/<module>/to the new service at the reverse proxy.
Frontend UI¶
- shadcn/ui components live in
libs/shared/ui/src/components/ui/, configured bylibs/shared/ui/components.json. Themes are:root[data-theme="<name>"](and:root[data-theme].dark) blocks of light and dark CSS variables inlibs/shared/ui/src/styles/globals.css, selected bydata-themeand thedarkclass on<html>. Light tokens must use that higher-specificity selector so they are not overwritten by the AstroVista:rootfallback that follows.index.htmlsets the default, and each app callsapplyStoredTheme()from@bizos/shared-ui/lib/themebefore rendering so the user's choice applies on load; theThemeSwitchercomponent (@bizos/shared-ui/components/theme-switcher) in the portal headers and the sign-in screens changes it. The choice is stored in thebizos-themecookie on the parent domain (<root-domain>), soadmin.<root-domain>and<tenant>.<root-domain>share it; on localhost both dev servers share it because cookies ignore the port. A new theme also needs an entry inTHEMESin that file. Available themes:astrovista(Outfit, Merriweather, Fira Code, the default in Dark mode when no preference is set) andtwitter(Open Sans). Theme fonts are Fontsource packages imported inlibs/shared/ui/src/lib/fonts.ts. Add a theme from tweakcn by copying its registrycssVarsinto a new block rather than runningshadcn add, which overwrites the default:rootvariables. - Fonts load from JavaScript: each app's
main.tsximports@bizos/shared-ui/lib/fonts. Do not@importfont packages in CSS; on Windows the Tailwind Vite plugin rewrites theirurl()s into paths the dev server cannot serve. - Each React app's
src/styles.cssimports@bizos/shared-ui/globals.cssand adds@source "../../../libs"so Tailwind sees classes used in libraries. Apps load Tailwind through@tailwindcss/vite. - Import components by subpath, for example
import { Button } from '@bizos/shared-ui/components/ui/button'; class names are merged withcnfrom@bizos/shared-ui/lib/utils. - Table row Action columns use
RowActionsfrom@bizos/shared-ui/components/row-actions: outline black pencil (Edit) and outline red trash (Delete). Do not use text links or filled icon buttons for those actions. - Add a component with
pnpm dlx shadcn@latest add <name> -c libs/shared/ui. Generated files may be edited; keep edits small so later updates can be compared with--diff. - Components used by one module only go in that module's
libs/web/<module>; move them tolibs/shared/uiwhen a second module needs them. - A React app renders the routes its module exports.
apps/adminrendersAdminRoutesfrom@bizos/web-admin.apps/webrendersIamRoutesfrom@bizos/web-iam. An admin seesAdminRoutesafter sign-in. A customer sees a shell with a header, sidebar, and footer, and a note that the customer screen will be built later. Both roles can open change password from the signed-in screen. Success deletes every session for that user and returns them to sign-in. The admin portal frame (shadcnsidebar, header, page not found) is thelayoutfeature oflibs/web/admin; each feature's pages are added as child routes of that layout. The layout provides theTooltipProviderthe sidebar's tooltips need. tsconfig.base.jsonhas no DOM types; React projects add"lib": ["es2022", "dom"]to theirtsconfig.lib.json,tsconfig.app.json, ortsconfig.spec.jsonwhen their code toucheswindowordocument.- The
@nx/enforce-module-boundariesrule is off forlibs/shared/ui/src/components/ui/only, because shadcn writes self-imports through the@bizos/shared-ui/...alias. - Dev server ports:
web4200,admin4201. Both dev servers proxy/apito the API on port 3000 so the session cookie stays on the app's origin.
Testing¶
| Level | Tool | Where | Run |
|---|---|---|---|
| Backend unit and integration | Jest with @swc/jest |
*.spec.ts next to the code, in libs/api/<module> and the NestJS apps |
pnpm test, or npx nx test <project> |
| Frontend unit and component | Vitest with Testing Library in jsdom | *.spec.tsx next to the component, in libs/web/<module>, libs/shared/ui, and the React apps |
pnpm test, or npx nx test <project> |
| End-to-end | Playwright (Chromium) | apps/<app>-e2e/src/*.spec.ts, one project per React app |
pnpm e2e, or npx nx e2e <app>-e2e |
- Each Vitest project loads
src/test-setup.ts, which adds the@testing-library/jest-dommatchers (toBeInTheDocument,toHaveValue, ...). Projects that render the shared sidebar also stubwindow.matchMediaandResizeObserverthere, because jsdom has neither. - Query the screen the way a user sees it:
getByRole,getByLabelText,getByText. Use a test id only when an element has no accessible name. Simulate input with@testing-library/user-event, notfireEvent. - End-to-end tests start the app's dev server unless one is already running on its port; set
BASE_URLto run them against a deployed environment. Install the browser once withpnpm exec playwright install chromium. - End-to-end tests cover critical flows that cross apps or the API, such as sign-in and tenant provisioning; everything else is tested at unit or component level. They drive the browser and never import app code.
- Rule 8 applies at every level: a test that checks a spec requirement starts its name with the requirement ID.
Database access¶
There are two kinds of database, each with its own schema set and migrations:
| Database | Used by | Schema file in a module | Migrations |
|---|---|---|---|
| Control plane (one) | tenancy, onboarding, billing, admin |
src/lib/infrastructure/database/control-plane.schema.ts |
db/migrations/control-plane/ |
| Tenant (one per tenant) | All other modules | src/lib/infrastructure/database/tenant.schema.ts |
db/migrations/tenant/ |
- drizzle-kit finds schema files by these names (see
db/*.config.ts), so a module adds tables by creating the file; no central list. - Each module puts its tables and enums in a PostgreSQL schema named after the module, declared with Drizzle's
pgSchema('<module>')(dashes become underscores, e.g.data_import). The existingtenants(tenancy) andsettings(system) tables are still inpublic; see open questions. - Tables belong to the module that defines them. Schema files are not exported from a module's
index.ts; other modules use its contracts. - No foreign keys, joins, views, or transactions across module schemas. A row in another module is referenced by its UUID only.
- Primary keys are UUIDs, so rows keep their identity when a module moves to its own database.
DatabaseModulefrom@bizos/api-sharedprovidesCONTROL_PLANE_DB(a Drizzle instance) andTenantDatabaseManager, which keeps one connection pool per tenant (get(tenantId, connectionString)). A pool idle longer thanTENANT_POOL_IDLE_MSis closed, andrelease(tenantId)closes one pool on demand.- The
tenancymodule appliesdb/migrations/tenant/to one tenant or to every tenant that is notdeleted.pnpm db:migrate:tenantmigrates only the local development database. - Column names are written explicitly in
snake_case; timestamps usetimestamp with time zone. - Change a schema, then run
pnpm db:generate:control-planeorpnpm db:generate:tenant, review the generated SQL, and commit it with the code. Never edit an applied migration.
Rules¶
- Domain is pure.
domain/imports nothing from NestJS, the ORM, or other modules. Business rules live here, not in controllers or handlers. - Modules talk through contracts. A module never imports another module library. Synchronous calls go through ports in the other module's contract library (
@bizos/api-<module>-contracts); side effects go through integration events. No cross-module database joins, foreign keys, or transactions. See Module communication and extraction. - Features stay inside their module. A feature may use its module's domain and shared code, but not another feature's internals. Code needed by two features moves to the module's
domain/or toshared/. - Tenant context is implicit and mandatory. Every tenant request resolves the tenant from the subdomain once; repositories get the tenant database connection from that context. No handler passes a tenant ID by hand, and no query can run without a tenant context except in
tenancy,onboarding,billing, andadmin, which use the control-plane database. Across a module boundary that may become remote, the tenant travels explicitly: in the event envelope, or in request headers. - Validate at the edge. DTOs are validated in the controller; the domain assumes valid input and enforces invariants.
- Boundaries are checked by tooling. Nx project tags and the
@nx/enforce-module-boundarieslint rule fail the build when rule 1, 2, or 3 is broken. - Naming. Folders and files in
kebab-case; module and feature names are the same in backend, frontend, and specs. - Tests trace to specs. A test that checks a spec requirement starts its name with the requirement ID, e.g.
it('TEN-03 suspends the tenant', ...), so drift between code andspecs/can be found with a search.
Open questions¶
- Authentication: build on Passport within NestJS, or use an external identity provider. Either way, credentials must be verifiable by any service without a synchronous call to
iam. - Integration events for IAM are rows in
iam.outbox. The API andapps/workerpoll that table and deliver each event at least once. BullMQ remains the proposed transport when Redis is added. Tenant request context on AsyncLocalStorage is in@bizos/api-shared. tenantsandsettingsare still in thepublicschema; move them totenancyandsystemwith a migration before more tables are added.- The API has one OpenAPI document; split it per module (
/api/<module>/) when the typed client is generated, so a module's client can follow it to its own service.