--- name: architecture-boundaries description: Layering and boundaries, web vs public API, app layout (clients, routes, logging), ports/adapters, runtime-portable domain/shared/utils code, multi-tenancy, DDD layout, or anti-patterns. --- # Architecture and layer boundaries **When to use:** Layering and boundaries, web vs public API, **app layout** (clients, routes, logging), ports/adapters, **runtime-portable domain/shared/utils code**, multi-tenancy, DDD layout, or anti-patterns. ## App boundaries (`apps/*`) Apps only handle: - Input validation - Authentication and authorization - Organization access enforcement - Routing to domain use-cases No business logic in handlers, controllers, or jobs. ## Application layout (`apps/*`) - **Clients**: Initialize integrations in `apps/*/clients.ts` and import from boundaries — avoid scattering raw clients. - **Routes**: Use `apps/*/routes/` with a `registerRoutes()` (or equivalent) pattern so the HTTP surface stays modular. - **Logging**: Use `createLogger()` from `@repo/observability` with a stable service name per app. - **Tracing**: Every `Effect.runPromise` call site must include `withTracing` from `@repo/observability` in the pipe chain to connect Effect spans to the OTel pipeline. See [effect-and-errors](../effect-and-errors/SKILL.md) for the full tracing rules. - **Configuration values**: Read env through `parseEnv` / `parseEnvOptional` — see [env-configuration](../env-configuration/SKILL.md). ## Web vs public API (`apps/web`, `apps/api`, `@repo/operations`) - The public API's operation definitions (route config + transport-neutral `execute` logic) live in **`packages/operations` (`@repo/operations`)** — the boundary-contract layer between apps and domain. One definition fans out to the HTTP route, OpenAPI, MCP tool, SDK methods, CLI command, and in-process agent tools. - `apps/api` is the transport shell: middleware (auth, org context, rate limiting), the MCP HTTP transport, mounting `operationModules`, and the manifest emit scripts. Treat the operation contracts as externally consumed and evolve them carefully. - `@repo/operations` sits above domain: operations validate input, map to public schemas, and orchestrate `@domain/*` use-cases — the same boundary responsibilities apps own, factored into a package so non-HTTP consumers (worker-side agents) can run `execute` in-process. - `apps/web` must not call or proxy through `apps/api` for internal product features. - For web product development, implement backend behavior in `apps/web` server functions by composing domain use-cases and platform adapters directly. - Keep iteration velocity in `apps/web` by adding web-private server functions/stores while preserving `apps/api` stability. - Shared business rules still belong in domain packages; `apps/web` and `@repo/operations` should both orchestrate domain use-cases rather than duplicating policy. - Latitude product capabilities should be equally accessible to humans through the web UI and to other LLM agents through MCP/API surfaces. - Do not dead-end product behavior into UI-only flows. Preserve the boundary rules above, but design schemas, use-cases, and public capabilities so machine-facing access can exist without redesign. - For the concrete recipe — `defineOperation`, `OperationModule` manifests, `group`/`sdkMethod`/`access`/`rateLimitTier`, `pnpm openapi:emit` / `pnpm mcp:emit`, schema-description rules that fan out to the TS + Python SDKs, MCP tools, and the `latitude` CLI, the required declarative `access` field, and `defineToolset` (with its access ceiling) for internal agents — see [api-endpoints](../api-endpoints/SKILL.md). ## Cross-cutting implementation constraints - Public request/response schemas should remain boundary-specific; they may reuse shared domain schemas or narrower projections rather than forcing full domain entities onto every surface. - When a capability is part of the product contract, preserve a machine-facing MCP/API surface instead of making it web-only. ## Domain layer (`packages/domain/*`) Business logic lives here. Domain packages expose: - Use-cases - Canonical entity schemas and inferred entity types - Domain types and errors - Dependency ports (interfaces/tags) ## Domain package layout Domain **entities** are **Zod-first**: `entitySchema` + `z.infer` in `src/entities/.ts`. See [`dev-docs/domain-entities.md`](../../../dev-docs/domain-entities.md) and [`docs/adr/0001-domain-entity-schema-style.md`](../../../docs/adr/0001-domain-entity-schema-style.md). - Treat canonical domain entity schemas as the source of truth. Schemas and types elsewhere in the same domain, plus app/platform boundary schemas, should derive from or reuse the entity shapes whenever practical instead of re-declaring the same fields. - When a boundary schema must differ materially from the entity shape, still reuse the relevant domain constants, field schemas, and literal unions rather than hardcoding duplicated lengths or sentinel values again. - Canonical entity schemas and their inferred entity types belong in `packages/domain/*/src/entities/.ts`. - Domain package constants belong in `packages/domain/*/src/constants.ts`. - Domain package errors belong in `packages/domain/*/src/errors.ts`. A full package-by-package inventory and import rules live in [`dev-docs/domain-errors.md`](../../../dev-docs/domain-errors.md). - For **how** to structure those errors (tagged classes, HTTP fields, unions per flow, naming), treat `packages/domain/issues` as the reference: see `packages/domain/issues/src/errors.ts` and the section *Domain errors (`@domain/issues` reference pattern)* in `dev-docs/issues.md`. - Small domain-scoped shared helpers such as predicates or lifecycle helpers belong in `packages/domain/*/src/helpers.ts`. - Types and schemas that exist only as inputs to one domain use-case belong in that use-case file rather than a generic side module, unless several use-cases truly share the exact same contract. - App and platform layers should build boundary-specific schemas by reusing or deriving from domain entity/use-case schemas whenever practical rather than redefining the same contract from scratch. ## Infrastructure (`packages/platform/*`) Infrastructure details live here only. Platform packages implement adapters for domain ports. ### Platform adapters: Effect-based clients **Reference implementation:** `packages/platform/db-weaviate/src/client.ts` — `createWeaviateClientEffect` (and the thin `createWeaviateClient` wrapper used by scripts). Use this pattern when a platform package owns an external SDK client so composition roots can stay in Effect and errors stay typed. 1. **Primary constructor is an Effect** — Export `createXClientEffect(...): Effect.Effect` (or with requirements `R` if unavoidable). Scripts and one-off CLIs may export `async function createXClient()` as `Effect.runPromise(createXClientEffect(...))` only at the boundary that needs promises. 2. **Typed errors** — Model connection, validation, and bootstrap failures with `Data.TaggedError` (or shared env errors from `@platform/env`). Union them into a single `CreateXClientError` (or similar) exported next to the constructor. 3. **Configuration** — Resolve settings with `parseEnv` / `parseEnvOptional` from `@platform/env` inside the Effect pipeline, not ad hoc `process.env` reads scattered outside the client module. 4. **Interop** — Wrap promise-based SDK calls in `Effect.tryPromise` and map failures to tagged errors. Compose steps with `Effect.pipe`, `Effect.flatMap`, and `Effect.map`. 5. **Bootstrap in the pipeline** — If the client must apply schema/migrations/health checks before use, run those as Effects in the same pipeline (see Weaviate: `migrateWeaviateCollectionsEffect` after connect) so callers get a ready client or a single error channel. 6. **Live layers** — Expose a thin `XClientLive(client, scope...)` layer for the external SDK client and keep repository adapters as `Layer.effect` or `Layer.succeed` values that depend on that client service as needed. The composition root acquires the client with `createXClientEffect` and provides it via a small helper when useful, for example `withWeaviate(IssueProjectionRepositoryLive, client, organizationId)`. Not every legacy adapter has been migrated; prefer this shape for new work and when touching client construction. ## Shared utilities (`packages/utils`) General-purpose utility functions that can be shared across any package (domain, platform, or app) live in `@repo/utils`. This package should contain pure, stateless helper functions with no domain or infrastructure dependencies. Examples: `formatCount`, `formatPrice`, string helpers, number formatters. When writing a utility function that is not specific to a single domain or package, place it in `@repo/utils` instead of keeping it local. ## Shared domain vs utils `@domain/shared` and `@repo/utils` have different responsibilities and should not be merged. - Use `@domain/shared` for domain-level shared contracts, types, errors, and IDs used across bounded contexts. - Use `@repo/utils` for global pure, stateless helpers that are reusable anywhere. - If a helper has domain/business meaning, it belongs in `@domain/shared`; otherwise, use `@repo/utils`. ## Ports and adapters - Domain depends on interfaces/tags only (ports like `Repository`, `CacheStore`, `Publisher`) - Platform packages implement adapters - Composition roots in apps provide live layers - Domain must never import concrete DB/cache/queue/object storage clients - **Repository method names:** Use the standard verbs in [dev-docs/repositories.md](../../../dev-docs/repositories.md) (`findById`, `findByXxx` for unique keys, `listByXxx` / `list` for collections, `save`, `delete` vs `softDelete`, etc.). - Reliability async contracts should stay project-scoped as well as organization-scoped: include both `organizationId` and `projectId` in event/task/workflow payloads by default (except `MagicLinkEmailRequested`, `UserDeletionRequested`, `domain-events`, `magic-link-email`, and `user-deletion` payloads). ## Web standards first (domain, utils, shared) In `packages/domain/*`, `packages/utils`, `@domain/shared`, or any code that may run outside Node (browser, edge, isolates), prefer **Web Standard APIs** over Node-only modules so those layers stay portable. - Use `crypto.subtle` / `crypto.getRandomValues` instead of `node:crypto` - Use `fetch` instead of Node-specific HTTP clients - Use `TextEncoder` / `TextDecoder` instead of `Buffer.from(…, 'utf-8')` - Use `Uint8Array` for binary data in public interfaces - Use `ReadableStream` instead of `node:stream` / `node:fs` streams - Use `URL`, `URLSearchParams`, `Headers`, `Request`, `Response` from the global scope - Use `structuredClone` instead of JSON round-trips for deep cloning **Node-only APIs are acceptable** in build tooling, scripts, CLI utilities, and test infrastructure. If you need Node outside those scopes, add a brief comment explaining why. ## Data and infrastructure (overview) - **Postgres**: Control-plane and relational data (users, organizations, memberships, config) - **ClickHouse**: High-volume telemetry storage and analytical reads - **Weaviate**: Vector database for embeddings storage and semantic similarity search - **Redis**: Cache and BullMQ backend - **Object storage**: Durable raw ingest payload buffering For access patterns, schema, and migrations, see [database-postgres](../database-postgres/SKILL.md) and [database-clickhouse-weaviate](../database-clickhouse-weaviate/SKILL.md). ## Multi-tenancy - Every request is organization-scoped - A user may belong to many organizations - Organization membership checks happen at boundaries before domain execution - All telemetry persistence and query paths include `organizationId` - Organization-scoped Redis or cache keys must start with `org:${organizationId}:...`; keep the org id first in the key ## Domain design (DDD) - Organize by bounded context (e.g. telemetry, organizations, identity, alerts) - Domains should be single-responsibility and focused on policy/rules - Use in-memory adapters for fast tests where possible ## Anti-patterns to reject - Cross-domain logic without clear ownership - New provider integrations without a core capability contract - Introducing application env vars without the `LAT_` prefix (see [env-configuration](../env-configuration/SKILL.md)) - Using `"use client"` or `"use server"` directives — these are Next.js-specific; the web app uses TanStack Start - Exporting test utilities from a package's main entry point (see [testing](../testing/SKILL.md))