> For AI agents: the complete documentation index is available at /llms.txt, the full documentation bundle is available at /llms-full.txt.

# Backend 架构 Pattern

> **读者**：新成员、跨 repo 切换的工程师、需要解释 retaintive backend 如何工作的 PM / founder / AI Agent。
>
> 本文讲 **backend 运行机制和 repo 内代码分层**。系统级 repo 地图见 [系统架构总览](/architecture/0-system-overview.md)，通话/SMS/Lead 数据管道见 [Backend 数据管道](/architecture/2-backend.md)。

***

## TL;DR

retaintive 有两类 backend，不应该混成一个东西：

| Backend                             | 面向谁                             | 触发方式                | 架构 pattern                                                                                | 主要职责                                 |
| ----------------------------------- | ------------------------------- | ------------------- | ----------------------------------------------------------------------------------------- | ------------------------------------ |
| `studio-website-monorepo/apps/api`  | `apps/web` / 浏览器                | HTTP request        | **Layered**：routes → services → repositories；部分 v3 read route 仍 inline SQL                | 鉴权、store scope、查询/少量写入、返回 JSON       |
| `callytics-infrastructure/lambda/*` | SQS / EventBridge / 内部 pipeline | async event         | **Hexagonal where complexity warrants it**：handler → core ports → infrastructure adapters | 转录、AI 分析、短信持久化、联系人分析、Lead 下游处理、报告    |
| `callytics-common/src/db`           | 多 repo                          | compile-time import | shared schema                                                                             | Drizzle schema + TypeScript types 单源 |

两个 backend 都遵守同一个方向：**业务规则不应该依赖具体 vendor / framework / database client**。差别是抽象深度不同。

***

## 1. 基础词汇

### Web vs API

`apps/web` 是前端应用。它被 build 成 HTML/JS/CSS，部署在 Cloudflare Pages。用户浏览器下载这些静态文件后，JavaScript 在浏览器里运行。

`apps/api` 是 backend API。它被 build 成 Lambda bundle，部署在 AWS Lambda，通过 API Gateway 暴露成 `studio-api*.retaintive.ai`。

前端调用后端的本质是 HTTP：

```text
Browser JavaScript
  → Hono RPC fetch client（preferred）或 legacy axios client
  → https://studio-api*.retaintive.ai/v3/...
  → API Gateway
  → Lambda running Hono app
  → JSON response
```

### API backend vs processing backend

`apps/api` 是**前端查询 API**。它不负责从 RingCentral 下载录音，也不负责调用 Deepgram / OpenRouter 做 AI 分析。

这些后台处理在 `callytics-infrastructure/lambda/*`：

```text
RingCentral Webhook / SQS / EventBridge
  → Lambda processors
  → Neon / DynamoDB / S3
  → apps/api later reads the result
```

### Route / Service / Repository

在 server-side API 里，不建议用 MVC 描述 backend。更准确的是：

| 层                  | 职责                                             | retaintive 例子                                             |
| ------------------ | ---------------------------------------------- | --------------------------------------------------------- |
| Route / Controller | 解析 HTTP input、调用 auth/context、返回 JSON          | `routes/v3/contacts.ts`                                   |
| Service / Use case | 业务规则、权限判断、transaction boundary、跨 repository 协调 | `getStoreContext()` 当前承担一部分 service 职责                    |
| Repository / SQL   | 和数据库对话                                         | raw parameterized SQL、`infrastructure/neon-repository.ts` |

***

## 2. studio-api request lifecycle

一次用户访问 Dashboard 的 API 请求，大致是这个链路：

```mermaid
sequenceDiagram
    participant Browser
    participant Web as apps/web
    participant Cognito
    participant APIGW as API Gateway
    participant Lambda as apps/api Lambda
    participant Hono
    participant Route as Hono route
    participant Auth as auth/shared helper
    participant DB as Neon / DynamoDB

    Browser->>Web: 用户打开页面，Vue component mounted
    Web->>Cognito: 登录或刷新 session
    Cognito-->>Web: JWT tokens
    Web->>Web: API client 读取 access token
    Web->>APIGW: HTTP request + Authorization: Bearer access token
    APIGW->>Lambda: Invoke Lambda handler
    Lambda->>Hono: Hono app receives Request
    Hono->>Hono: middleware chain
    Hono->>Auth: verify JWT / attach auth context
    Hono->>Route: matched route handler
    Route->>Route: Zod parse query/body
    Route->>Auth: getStoreContext(userId, storeId)
    Auth->>DB: check store owner/member + store phones
    Route->>DB: parameterized SQL / DDB query
    DB-->>Route: rows/items
    Route-->>Web: JSON response
    Web-->>Browser: component re-render
```

这条链路解释了几个常见疑问：

- **为什么前端能传数据给后端？** 因为浏览器发 HTTP request，不是因为 frontend/backend 在同一个 repo。
- **为什么后端能读数据库？** 因为 API Lambda 部署时配置了环境变量、secret、network、IAM 和 database connection。
- **auth 在哪里发生？** Cognito 发 token；前端 API client 附上 access token；API middleware 验证 token；route/helper 再检查 store-level 权限。
- **API 和业务处理 Lambda 的关系是什么？** API 通常读 processing Lambda 写好的结果；processing Lambda 不由浏览器直接调用。

本地代码里当前同时存在两套前端 API client：

| Client                | 文件                                                 | 状态                                                    |
| --------------------- | -------------------------------------------------- | ----------------------------------------------------- |
| Hono RPC fetch client | `apps/web/src/api/hono-client.ts` → `typed-api.ts` | 新代码 preferred，端到端复用 `AppType` 类型，内置 401 refresh/retry |
| legacy axios client   | `apps/web/src/api/index.ts` → `ot-api/base.ts`     | 旧 `ot-api/*.ts` 仍在用，后续迁移掉                             |

***

## 3. studio-api 的 Layered 架构

`studio-website-monorepo/apps/api` 选择 layered，是因为它主要是 read-heavy API：复杂查询、筛选、排序、分页、少量写入。目标结构是 `routes/ → services/ → repositories/`，但迁移仍在进行：简单 v3 Neon read route 仍会直接在 route 里写 parameterized SQL。

```mermaid
flowchart TB
    req["HTTP Request"]
    route["Route / Controller<br/>Hono handler"]
    service["Service / Shared helper<br/>auth context / store scope / business rules"]
    repo["Repository / SQL<br/>parameterized SQL / DDB client"]
    db["Neon / DynamoDB"]
    res["JSON Response"]

    req --> route --> service --> repo --> db
    db --> repo --> service --> route --> res
```

### 3.1 Route 层负责什么

Route 是 HTTP 边界，应该处理：

- 读取 `c.req.query()` / `c.req.json()`
- 用 Zod 做 input validation
- 从 middleware 读取 `auth`
- 调用 service/helper
- 把结果转成 JSON response
- 把错误交给统一 error handler

Route 不应该承载复杂业务规则。读查询很薄时可以 inline SQL；一旦规则开始变多，就应该抽 service。

### 3.2 Services 和 Auth / Store context

代码里已经有明确的 `src/services/` 和 `src/repositories/`：

- `src/services/*-neon.ts`：Neon-backed business/data helpers，例如 stores、members、phone numbers、OAuth states。
- `src/services/ringcentral/*`、`token-access.ts`、`subscription-client.ts`：外部服务和 cross-service 调用。
- `src/repositories/*`：仍保留的 DynamoDB data access，例如 `call-analysis.ts`、`phone-store-assignments.ts`。
- `src/utils/authorization.ts` / `store-authorization-neon.ts`：store-level authorization helper。

`getStoreContext(userId, storeId)` 本质上是 service 层逻辑，因为它做了业务判断：

```text
1. store 是否存在
2. 当前 user 是 owner 还是 member
3. user 是否有权限访问这个 store
4. 这个 store 下有哪些 phone numbers
```

它现在放在 `routes/v3/shared.ts` 是务实选择：多个 v3 route 共用，且这些 endpoint 是 read-heavy Neon SQL。语义上仍要把它当 service boundary 看，而不是“普通 util”。其他 v2/v3 route 也可能用 `getAuthorizedStore()` 或 `getAuthorizedStoreNeon()` 做同类 authorization。

### 3.3 SQL / Repository 层负责什么

`studio-api` 里大量查询是 analytical read：CTE、多 JOIN、动态 filters、pagination、sort。用 raw SQL 更直接，也更容易看执行计划。

纪律是：

- 必须用 parameterized query：`$1`, `$2`, `$N`
- 用户输入只能进入 params array，不能拼进 SQL string
- `store_id` / tenant scope 必须在 SQL 条件里明确出现
- 同一查询逻辑复制到 3 个以上 endpoint 时，抽 repository/helper

### 3.4 什么时候必须抽 service

下面情况不要继续把逻辑塞进 route：

| 信号                                                | 为什么                                   |
| ------------------------------------------------- | ------------------------------------- |
| 出现业务规则                                            | 规则要可测试、可复用，不能藏在 HTTP handler          |
| 多表写入 / transaction                                | transaction boundary 需要集中控制           |
| 调外部 vendor                                        | vendor client 应该是 adapter，不应散落在 route |
| 同一段 SQL/逻辑 3+ 处复用                                 | 会产生行为漂移                               |
| route 超过“parse input + call helper + return JSON” | 可读性和测试性开始下降                           |

目标不是教条地建立 `services/` 和 `repositories/` 目录，而是让 boundary 清楚。

***

## 4. Authentication 和 store-level authorization

认证和授权是两层：

```mermaid
flowchart LR
    login["用户登录"] --> cognito["Cognito<br/>签发 JWT"]
    cognito --> web["apps/web<br/>保存 token"]
    web --> api["apps/api request<br/>Authorization: Bearer access token"]
    api --> jwt["Auth middleware<br/>验证 token"]
    jwt --> user["得到 userId / claims"]
    user --> store["getStoreContext<br/>检查 store owner/member"]
    store --> scoped["生成 store-scoped query context"]
    scoped --> sql["SQL 必须带 store_id / allowed phones"]
```

| 层              | 回答的问题                                     |
| -------------- | ----------------------------------------- |
| Authentication | “这个请求是谁发的？”                               |
| Authorization  | “这个 user 能不能访问这个 store / org / resource？” |
| Data scoping   | “SQL/DDB 查询有没有被限制在允许的数据范围内？”              |

不要把“有 token”理解成“能访问所有数据”。token 只证明身份；store-level authorization 决定数据范围。

本地 `test` 环境还有一个专用旁路：`X-Test-User-Id` header 可以跳过 JWT，用于 integration/dev test；prod 不允许。

***

## 5. callytics-infrastructure 的 Hexagonal 架构

`callytics-infrastructure/lambda/*` 的复杂 processor 选择 hexagonal，是因为后台处理业务复杂、外部依赖多、需要单元测试时 mock vendor/AWS。`transcribe-processor` 和 `ai-analysis-processor` 是最明确的 reference；`message-processor` 也拆成 `core/` 与 `infrastructure/`；`lead-processor` 这类较薄的 SQS consumer 则更接近 thin handler + core function。

- Deepgram / AWS Transcribe
- DeepSeek V4 Flash via OpenRouter
- SQS
- DynamoDB
- S3
- Neon
- Secrets Manager

核心思想：**core 定义自己需要什么能力（ports），infrastructure 提供具体实现（adapters）**。

```mermaid
flowchart TB
    subgraph driving["Driving adapters：外部触发 core"]
        sqs["SQS Lambda handler"]
        cron["EventBridge cron"]
        manual["manual invoke"]
    end

    subgraph core["Core / Domain"]
        usecase["pipeline / use case<br/>业务规则和步骤编排"]
        ports["protocols.ts<br/>ports / interfaces"]
    end

    subgraph driven["Driven adapters：core 调用外部世界"]
        ai["AI adapter<br/>OpenRouter"]
        transcribe["Transcription adapter<br/>Deepgram / AWS Transcribe"]
        neon["Neon repository"]
        ddb["DynamoDB repository"]
        s3["S3 repository"]
        sqspub["SQS publisher"]
        secrets["Secrets Manager adapter"]
    end

    driving --> usecase
    usecase --> ports
    ports --> ai
    ports --> transcribe
    ports --> neon
    ports --> ddb
    ports --> s3
    ports --> sqspub
    ports --> secrets
```

典型目录：

```text
lambda/ai-analysis-processor/src/
├── handler.ts                         # driving adapter + composition root
├── core/
│   ├── models.ts                      # domain types
│   ├── protocols.ts                   # ports
│   └── stages/                        # pipeline / use cases
└── infrastructure/
    ├── ai/                            # model provider adapter
    ├── config-repository.ts           # S3/config adapter
    ├── neon-repository.ts             # Postgres adapter
    ├── persistence-repository.ts      # DDB/S3 adapter
    ├── secrets-manager.ts             # Secrets Manager adapter
    └── sqs-publisher.ts               # SQS adapter
```

### 5.1 `handler.ts` 是 composition root

`handler.ts` 允许做这些事：

- import AWS SDK
- 初始化 AWS clients
- 读取 env vars
- 创建 infrastructure adapters
- 把 adapters 注入 core pipeline
- 处理 SQS batch response / retry / partial failure

`core/` 不应该 import AWS SDK，也不应该知道 Lambda/SQS/EventBridge 的存在。

### 5.2 Core 只依赖 ports

Core 应该写成：

```typescript
async function runPipeline(input: PipelineInput, deps: {
  configRepo: ConfigRepository;
  aiClient: AiClient;
  analysisRepo: AnalysisRepository;
  taskPublisher: TaskPublisher;
}) {
  const config = await deps.configRepo.load(input.clientId);
  const result = await deps.aiClient.analyze(input.transcript, config);
  await deps.analysisRepo.save(result);
  await deps.taskPublisher.publish(result.followUpTasks);
}
```

这样测试时可以传 fake deps，不需要真的调用 AWS / AI vendor / Neon：

```typescript
await runPipeline(input, {
  configRepo: fakeConfigRepo,
  aiClient: fakeAiClient,
  analysisRepo: inMemoryAnalysisRepo,
  taskPublisher: fakeTaskPublisher,
});
```

### 5.3 Adapters 负责技术细节

Adapter 可以知道具体 vendor：

- `NeonAnalysisRepository` 知道 SQL/Drizzle
- `S3ConfigRepository` 知道 bucket/key
- `OpenRouterAiClient` / `invokeAI` wrapper 知道 API endpoint/model name
- `SqsTaskPublisher` 知道 queue URL/message shape

但这些细节不能倒灌进 core。

***

## 6. Layered vs Hexagonal 怎么选

| 场景                                           | 选                               | 理由                           |
| -------------------------------------------- | ------------------------------- | ---------------------------- |
| 前端读数据、筛选、分页、返回 JSON                          | Layered                         | 抽象成本低，读查询直观                  |
| 复杂 SELECT / report / CTE                     | Layered + raw parameterized SQL | SQL 表达力和性能更可控                |
| 多步骤业务处理、状态机、重试、幂等                            | Hexagonal                       | core 规则需要独立测试                |
| 调 OpenAI / Deepgram / Stripe / Vapi 等 vendor | Hexagonal                       | vendor 必须通过 port/adapter 隔离  |
| SQS/EventBridge Lambda processor             | Hexagonal                       | handler 适合作 composition root |
| 多表写入 transaction                             | Layered + 显式 service            | transaction boundary 要集中     |
| 小型 CRUD endpoint                             | Layered                         | 不要为了形式上高级增加目录和 class         |

**判断标准**：不是“哪个 pattern 更高级”，而是“业务规则是否值得从 runtime/vendor 细节里剥离出来”。

***

## 7. ORM 策略：Drizzle vs raw SQL

retaintive 混用 Drizzle 和 raw SQL 是有意选择。

| 用法                    | 在哪                                    | 为什么                                        |
| --------------------- | ------------------------------------- | ------------------------------------------ |
| Drizzle schema        | `callytics-common/src/db/schema/*.ts` | schema 单源，生成 TS types                      |
| Drizzle insert/update | processing Lambda repositories        | write path 要 type-safe，字段漂移成本高             |
| Raw parameterized SQL | `apps/api/src/routes/v3/*.ts`         | 复杂 read query、CTE、JOIN、window function 更清楚 |

安全边界：

- raw SQL 必须 parameterized
- table/column 名不要从用户输入动态拼接
- sort field 必须 whitelist
- tenant/store scope 必须显式进 where clause
- write path 优先复用 Drizzle schema/type

***

## 8. 不要混淆的三组边界

### 部署边界

```text
apps/web  → Cloudflare Pages
apps/api  → AWS Lambda + API Gateway
lambda/*  → AWS Lambda processors
```

同一个 repo 里的目录可以部署到不同平台；不同 repo 的 Lambda 也可能通过 SQS 在同一个 runtime pipeline 里协作。

### 请求边界

```text
Browser request → apps/api
SQS/EventBridge event → callytics-infrastructure/lambda/*
RingCentral webhook → ringcentralSubscriptionService
IMAP polling → lead-tracking
```

看到“后端”二字时先问：这是 HTTP API backend，还是 async processing backend？

### 数据边界

```text
Neon: business primary SoT for contacts/messages/leads/tasks/calls
DynamoDB: call-analysis + config/legacy
S3: files/config artifacts
callytics-common: schema/types, not storage
```

***

## 9. 新增功能时怎么落位

### 新增前端页面需要读数据

1. 在 `apps/web` 加 page/component/query hook。
2. 在 `apps/api` 加 route。
3. Route 做 Zod validation + auth context。
4. 需要 store 权限时调用 `getStoreContext()` 或同类 helper。
5. SQL 必须带 store/org scope。
6. 复杂逻辑超过 route 职责时抽 service/helper。

### 新增后台处理步骤

1. 先确认触发源：SQS、EventBridge、manual invoke、还是 webhook。
2. 在 `lambda/<processor>/src/core` 写 use case / pipeline step。
3. 在 `core/protocols.ts` 定义需要的 ports。
4. 在 `infrastructure/` 实现 vendor/AWS/DB adapters。
5. 在 `handler.ts` 装配 adapters。
6. 用 fake adapters 测 core；用 integration test 覆盖 adapter 关键路径。
7. CDK 里声明 queue/event/env/permissions。

### 新增共享字段或表

1. 先改 `callytics-common` schema/type。
2. 明确 writer：哪个 Lambda/API 写这个字段。
3. 明确 reader：哪个 API/processor 读这个字段。
4. 更新写入矩阵或对应 system-design 文档。
5. 跨 repo 发布顺序要避免 “reader 先读不存在字段” 或 “writer 写了旧 schema”。

***

## 10. 和业界 vocabulary 对照

| retaintive 说法             | Spring / Java        | .NET                 | 解释               |
| ------------------------- | -------------------- | -------------------- | ---------------- |
| Hono route handler        | `@RestController`    | Controller           | HTTP 入口          |
| `getStoreContext()`       | `@Service`           | Application service  | 鉴权/上下文/业务规则      |
| raw SQL / repository file | `@Repository`        | Repository           | DB 访问            |
| `core/`                   | Domain layer         | Domain layer         | 业务规则             |
| `infrastructure/`         | Infrastructure layer | Infrastructure layer | 外部技术实现           |
| `handler.ts`              | `main()` / DI config | `Program.cs`         | Composition root |
| `protocols.ts`            | Ports / interfaces   | Interfaces           | core 需要的能力契约     |

面试或外部沟通可以这样说：

> The frontend-facing API uses a pragmatic layered architecture: Hono routes validate input, shared service helpers enforce auth and store scope, then routes call parameterized SQL or repositories. The async processing Lambdas use hexagonal architecture: `handler.ts` is the composition root, `core/` contains business rules and ports, and `infrastructure/` implements adapters for AWS, Neon, S3, SQS, and AI providers.

***

## 11. 相关文档

- [系统架构总览](/architecture/0-system-overview.md) — repo 间职责、runtime、部署边界
- [Backend 数据管道](/architecture/2-backend.md) — 通话 / SMS / Lead / 报告 pipeline
- [Store-Level 隔离设计](/system-design/store-level-isolation.md) — 多租户隔离和 repository/query scope
- [Call Lifecycle Tracking](/system-design/call-lifecycle-tracking.md) — call timeline 写入职责
- [Phone Normalization Strategy](/system-design/phone-normalization-strategy.md) — phone/store assignment repository 逻辑
- [AI Voice Agent 可行性](/ai/product/voice-agent-feasibility.md) — 未来 voice agent 为什么应走 hexagonal
