architecture-boundaries

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

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.
适用场景: 分层与边界、Web与公开API、应用布局(客户端、路由、日志)、端口/适配器、可跨运行时的领域/共享/工具代码、多租户、DDD布局以及反模式。

App boundaries (
apps/*
)

应用边界(
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/*
)

应用布局(
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 for the full tracing rules.
  • Configuration values: Read env through
    parseEnv
    /
    parseEnvOptional
    — see env-configuration.
  • 客户端:在
    apps/*/clients.ts
    中初始化集成,并从边界导入——避免分散原始客户端实例。
  • 路由:使用
    apps/*/routes/
    目录,并采用
    registerRoutes()
    (或等效)模式,确保HTTP接口保持模块化。
  • 日志:使用
    @repo/observability
    中的
    createLogger()
    ,为每个应用指定稳定的服务名称。
  • 链路追踪:每个
    Effect.runPromise
    调用点必须在管道链中包含
    @repo/observability
    withTracing
    ,以将Effect跨度连接到OTel管道。完整的追踪规则请参阅effect-and-errors
  • 配置值:通过
    parseEnv
    /
    parseEnvOptional
    读取环境变量——请参阅env-configuration

Web vs public API (
apps/web
and
apps/api
)

Web与公开API(
apps/web
apps/api

  • apps/api
    is the stable public API surface. Treat its routes/contracts as externally consumed and evolve them carefully.
  • 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
    apps/api
    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 —
    defineApiEndpoint
    ,
    createXxxRoutes
    factories,
    pnpm openapi:emit
    /
    pnpm mcp:emit
    , schema-description rules that fan out to the TS SDK and MCP tools — see api-endpoints.
  • apps/api
    是稳定的公开API接口。需将其路由/契约视为外部消费内容,谨慎进行版本演进。
  • apps/web
    不得为内部产品功能调用或代理
    apps/api
  • 对于Web产品开发,应通过直接组合领域用例和平台适配器,在
    apps/web
    的服务器函数中实现后端行为。
  • 在保持
    apps/api
    稳定性的同时,通过添加Web私有服务器函数/存储来提升
    apps/web
    的迭代速度。
  • 共享业务规则仍属于领域包;
    apps/web
    apps/api
    都应编排领域用例,而非重复实现策略。
  • Latitude产品功能应同时支持通过Web UI供人类访问,以及通过MCP/API接口供其他LLM Agent访问。
  • 不得将产品行为局限于仅UI可用的流程。遵守上述边界规则,同时设计模式、用例和公开功能,以便无需重新设计即可支持机器端访问。
  • 具体实现方案——
    defineApiEndpoint
    createXxxRoutes
    工厂、
    pnpm openapi:emit
    /
    pnpm mcp:emit
    、可生成TS SDK和MCP工具的模式描述规则——请参阅api-endpoints

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.
  • 公开请求/响应模式应保持边界特定;可复用共享领域模式或更窄的投影,而非强制所有接口使用完整领域实体。
  • 当某项功能属于产品契约的一部分时,需保留面向机器的MCP/API接口,而非仅提供Web端访问。

Domain layer (
packages/domain/*
)

领域层(
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<typeof entitySchema>
in
src/entities/<entity>.ts
. See
dev-docs/domain-entities.md
and
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/<entity>.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
    .
  • 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.
领域实体采用Zod优先原则:在
src/entities/<entity>.ts
中定义
entitySchema
+
z.infer<typeof entitySchema>
。请参阅
dev-docs/domain-entities.md
docs/adr/0001-domain-entity-schema-style.md
  • 将标准领域实体模式视为事实来源。同一领域内其他位置的模式和类型,以及应用/平台边界模式,应尽可能从实体结构派生或复用,而非重新声明相同字段。
  • 当边界模式必须与实体结构存在实质性差异时,仍需复用相关领域常量、字段模式和字面量联合类型,而非再次硬编码重复的长度或标记值。
  • 标准实体模式及其推导的实体类型应放在
    packages/domain/*/src/entities/<entity>.ts
    中。
  • 领域包常量放在
    packages/domain/*/src/constants.ts
    中。
  • 领域包错误放在
    packages/domain/*/src/errors.ts
    中。完整的包级错误清单及导入规则请参阅
    dev-docs/domain-errors.md
  • 关于如何构建这些错误(标记类、HTTP字段、按流程划分的联合类型、命名规则),请以
    packages/domain/issues
    为参考:查看
    packages/domain/issues/src/errors.ts
    以及
    dev-docs/issues.md
    中的*领域错误(
    @domain/issues
    参考模式)*章节。
  • 小型领域范围共享助手(如谓词或生命周期助手)放在
    packages/domain/*/src/helpers.ts
    中。
  • 仅作为单个领域用例输入的类型和模式应放在该用例文件中,而非通用侧边模块,除非多个用例确实共享完全相同的契约。
  • 应用层和平台层应尽可能通过复用或派生领域实体/用例模式来构建边界特定模式,而非从头重新定义相同契约。

Infrastructure (
packages/platform/*
)

基础设施层(
packages/platform/*

Infrastructure details live here only. Platform packages implement adapters for domain ports.
仅存放基础设施相关细节。平台包实现领域端口的适配器。

Platform adapters: Effect-based clients

平台适配器:基于Effect的客户端

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<Client, E, never>
    (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.
参考实现:
packages/platform/db-weaviate/src/client.ts
——
createWeaviateClientEffect
(以及脚本使用的轻量包装器
createWeaviateClient
)。
当平台包管理外部SDK客户端时,使用此模式,以便组合根保持在Effect中,且错误保持类型化。
  1. 主构造函数为Effect —— 导出
    createXClientEffect(...): Effect.Effect<Client, E, never>
    (若不可避免则包含依赖
    R
    )。脚本和一次性CLI仅可在需要Promise的边界处导出
    async function createXClient()
    ,实现为
    Effect.runPromise(createXClientEffect(...))
  2. 类型化错误 —— 使用
    Data.TaggedError
    (或
    @platform/env
    中的共享环境错误)建模连接、验证和引导失败。将它们合并为单个
    CreateXClientError
    (或类似名称)并与构造函数一起导出。
  3. 配置 —— 在Effect管道内使用
    @platform/env
    parseEnv
    /
    parseEnvOptional
    解析设置,而非在客户端模块外分散读取
    process.env
  4. 互操作性 —— 将基于Promise的SDK调用包装在
    Effect.tryPromise
    中,并将失败映射为标记错误。使用
    Effect.pipe
    Effect.flatMap
    Effect.map
    组合步骤。
  5. 管道内引导 —— 如果客户端在使用前必须应用模式/迁移/健康检查,则在同一管道中作为Effect运行这些操作(参考Weaviate:连接后执行
    migrateWeaviateCollectionsEffect
    ),以便调用者获得就绪的客户端或单一错误通道。
  6. Live层 —— 为外部SDK客户端暴露轻量的
    XClientLive(client, scope...)
    层,并将存储库适配器保留为依赖该客户端服务的
    Layer.effect
    Layer.succeed
    值。组合根通过
    createXClientEffect
    获取客户端,并在需要时通过小助手提供,例如
    withWeaviate(IssueProjectionRepositoryLive, client, organizationId)
并非所有遗留适配器都已迁移;新开发和修改客户端构造时优先采用此结构。

Shared utilities (
packages/utils
)

共享工具(
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.
可在任何包(领域、平台或应用)中共享的通用工具函数存放在
@repo/utils
中。此包应包含纯无状态助手函数,无领域或基础设施依赖。
示例:
formatCount
formatPrice
、字符串助手、数字格式化工具。
当编写不特定于单个领域或包的工具函数时,应将其放在
@repo/utils
中,而非本地保留。

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
    .
@domain/shared
@repo/utils
职责不同,不应合并。
  • 使用
    @domain/shared
    存放跨限界上下文使用的领域级共享契约、类型、错误和ID。
  • 使用
    @repo/utils
    存放可在任何地方复用的全局纯无状态助手。
  • 如果助手具有领域/业务含义,则属于
    @domain/shared
    ;否则使用
    @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 (
    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).
  • 领域仅依赖接口/标签(如
    Repository
    CacheStore
    Publisher
    等端口)
  • 平台包实现适配器
  • 应用中的组合根提供Live层
  • 领域绝不能导入具体的数据库/缓存/队列/对象存储客户端
  • 存储库方法命名:使用dev-docs/repositories.md中的标准动词(
    findById
    、针对唯一键的
    findByXxx
    、针对集合的
    listByXxx
    /
    list
    save
    delete
    vs
    softDelete
    等)。
  • 可靠性异步契约应同时支持项目范围和组织范围:默认在事件/任务/工作流负载中包含
    organizationId
    projectId
    MagicLinkEmailRequested
    UserDeletionRequested
    domain-events
    magic-link-email
    user-deletion
    负载除外)。

Web standards first (domain, utils, shared)

Web标准优先(领域、工具、共享包)

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.
packages/domain/*
packages/utils
@domain/shared
或任何可能在Node外运行的代码(浏览器、边缘计算、隔离环境)中,优先使用Web标准API而非Node专属模块,以保持这些层的可移植性。
  • 使用
    crypto.subtle
    /
    crypto.getRandomValues
    替代
    node:crypto
  • 使用
    fetch
    替代Node专属HTTP客户端
  • 使用
    TextEncoder
    /
    TextDecoder
    替代
    Buffer.from(…, 'utf-8')
  • 在公开接口中使用
    Uint8Array
    处理二进制数据
  • 使用
    ReadableStream
    替代
    node:stream
    /
    node:fs
  • 使用全局作用域的
    URL
    URLSearchParams
    Headers
    Request
    Response
  • 使用
    structuredClone
    替代JSON往返进行深度克隆
Node专属API可用于构建工具、脚本、CLI工具和测试基础设施。如果在这些范围外需要使用Node API,请添加简短注释说明原因。

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 and database-clickhouse-weaviate.
  • Postgres:控制平面和关系型数据(用户、组织、成员身份、配置)
  • ClickHouse:高容量遥测存储和分析读取
  • Weaviate:用于嵌入存储和语义相似度搜索的向量数据库
  • Redis:缓存和BullMQ后端
  • 对象存储:持久化原始摄入负载缓冲
访问模式、模式和迁移相关内容,请参阅database-postgresdatabase-clickhouse-weaviate

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
  • 每个请求都属于特定组织范围
  • 一个用户可属于多个组织
  • 组织成员身份检查在边界处执行,之后再进行领域逻辑处理
  • 所有遥测持久化和查询路径均包含
    organizationId
  • 组织范围的Redis或缓存键必须以
    org:${organizationId}:...
    开头;将组织ID放在键的首位

Domain design (DDD)

领域设计(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)
  • 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)
  • 无明确所有权的跨领域逻辑
  • 无核心功能契约的新供应商集成
  • 未使用
    LAT_
    前缀的应用环境变量(请参阅env-configuration
  • 使用
    "use client"
    "use server"
    指令——这些是Next.js专属指令;Web应用使用TanStack Start
  • 从包的主入口导出测试工具(请参阅testing