Monorepo Conventions
Where new code goes
apps/*— anything with a UI a human loads in a browser. Next.js. One app per audience (marketing, docs, merchant dashboard, exchange, internal ops, status page) — do not add audience-specific routes to an existing app instead of a new one; do not create a new app for a feature that belongs inside an existing audience's app.services/*— anything that's an independently deployable backend process with its own database access and its own health check. NestJS. One bounded context per service (seeservice-catalog.mdfor the current list and each one's responsibility). If you're adding an endpoint that doesn't fit any existing service's stated responsibility, that's a signal to either widen a service's stated responsibility (update its README) or scaffold a new one (npm run new:service) — don't bolt unrelated logic onto an unrelated service.packages/*— anything published/shared and not independently deployable on its own: types, utils, config, SDKs, widgets, ecommerce plugins,core-*shared domain logic consumed by multiple services. Acore-*package holds logic two or more services need identically (e.g. the Trade Compatibility Score calculator incore-matching, consumed by bothmatching-engineandrecommendation-service) — it is not a place to dump code you're unsure where else to put.infra/*— how things run. No application code.tools/*— repo-local developer tooling that isn't shipped anywhere (codegen, seeding, the new-service scaffolder).docs/*— narrative and reference documentation. Update the relevant doc in the same PR that changes the behavior it describes — a stale doc here is treated as a bug.
Naming
- Service/app/package folder names are kebab-case and match their
package.jsonnamefield exactly (packages are scoped@tbbn/<folder-name>). - Database tables are
snake_case, plural (trade_sessions,seller_merchant_links). - Event names are
noun.past_tense_verb(listing.created,trade.completed) — seeevent-webhook-catalog.md. - Environment variables are
SCREAMING_SNAKE_CASE, prefixed by service where ambiguous (AUTH_SERVICE_JWT_SECRET, not justJWT_SECRET, once more than one service needs a secret with that shape). - API routes are versioned in the path (
/v1/...) — seedocs/api/api-conventions.md. - UUIDs are UUIDv7 (time-ordered), generated via
@tbbn/utils'generateId()— never rely on Postgresgen_random_uuid()(v4) directly, since v7 sorts chronologically and keeps b-tree indexes dense.
WORKING vs STUB
Every folder's README states its status. WORKING means real business logic exists and is
tested. STUB means the folder boots, has a health check (services) or exports its documented
public interface (packages), and has a README describing what it will do and which roadmap
phase builds it out — but contains no real business logic yet. service-catalog.md is the
single source of truth for current status; keep it in sync when a stub graduates to working.
Cross-workspace imports
Services and apps depend on packages/* purely through normal Node module resolution: an
npm workspace dependencies entry (e.g. "@tbbn/types": "*") plus the npm-created
node_modules/@tbbn/<name> symlink, resolving to that package's main/types fields (its
built dist/, not raw src/) — never via relative ../../../packages/... imports, and not via
a tsconfig.json paths shortcut either (an earlier version of tsconfig.base.json had one;
it was removed because pointing paths at another package's src/ pulls that package's source
files across the compiling package's rootDir boundary, which breaks tsc's per-package
rootDir/outDir isolation). This means a package must be built
(npm run build -w packages/<name>) before anything consuming it will typecheck — the repo
root's build script builds the Phase 0-3 working set in explicit dependency order before
falling back to --workspaces --if-present for everything else. Services never import from
other services' src/ directly (only their published HTTP/event contract) — cross-service logic
sharing goes through a core-* package instead.