Mortar — architecture

Status: 已实装;公开 launch 进行中。这份文档跟随 代码演进;过往决策见 git history。

Mortar 是 Backend-as-a-Service:HTTP API 在前,Postgres + Redis + 对象存储 + SSE 在后。给国内 mobile / web app 用,单 VPS 起步, swap to 阿里云 RDS / OSS / 腾讯云 CDB / COS 不改一行代码。

跟 Supabase / Firebase 走同一种 shape,AI agent 在 chat 里看一眼就懂 —— 这些 API 形状在它训练数据里到处都是。客户端用 host 子域名寻址某个 project(https://<tenant>.api.mortar.appunvs.com/v1/{capability},即 Supabase 的 <ref>.supabase.co 模型);内部由 SubdomainTenant 中间件 把它折回 router 和后续整条栈所用的 /v1/{tenant}/{capability} 路径 (见 § “Per-project tenant_id 隔离”)。


设计原则

Mortar 内部走 7 primitive × (5 core feature + 3 extension feature) 两层结构。这是核心设计原则,是整个产品的脊椎。5 coreauth / data / files / realtime / usage(已实装)。 3 extensioncron / customdomain / queue(规划中, opt-in via env / flag)。

7 primitive(成本维度)

分两类计费:3 baseline(含在 tier 月费里)+ 4 metered(配额 + 超量计费 + 硬 cap)。

Primitive类型是什么对应的云账单
computebaselinepool ECS 实例时间(per-tenant 切片)ECS instance hours
databasebaselinePostgres 切片(与 mortar binary co-located on pool ECS)折算进 ECS hours(self-hosted PG),或 RDS instance baseline
cachebaselineSelf-hosted Redis 槽位(co-located on pool ECS)折算进 ECS hours(self-hosted Redis)
storagemetered对象存储 GB-月OSS / COS / S3 storage GB-月
networkmetered出向带宽(egress)公网下行 GB(OSS request 费 Mortar 替吃,不入账单)
functionmetered用户 FaaS 函数 CPU-minFC / SCF / Lambda CPU-min + GB-second
commsmetered出站事务消息(email + sms 两条 ledger resource,单价独立)Aliyun DirectMail per-message + Aliyun SMS per-message(或 Tencent SES/SMS)

7 个 primitive 同形于公有云账单的主类别。让自家成本结构跟云成本 结构对齐,意味着:

  • 给客户的账单(7 个 cost dimensions)可以直接溯源到我们自己 付给云厂的钱
  • 任何 primitive 都可以替换底层 backend(阿里云 → 腾讯云 → AWS)而不影响上层 feature
  • 监控告警按 primitive 切(function 慢 ≠ database 慢 ≠ network 慢), on-call 不混淆
  • baseline 与 metered 两类的 enforcement 不同:baseline 是 tier 选了 就预算固定;metered 三段(quota → overage → cap)。详见 pricing.md

5 feature(用户能力)

Feature用户能干什么组合了哪些 primitive
auth用户注册 / 登录(email+password、phone-OTP) / API key / email-verify / password-resetdatabase + comms(sms / email 发码)+ cache(OTP / token + 发送侧限频)
data表 + 行 CRUD + 全文检索database + cache(查询缓存)
files文件上传 / 下载 / 签名 URLstorage + database(metadata)
realtimeSSE 行变更订阅 + 广播 broadcast + 在线状态 presencedatabase(LISTEN)+ cache(pub/sub fan-out + presence roster)
usage用量 ledger + 余额database

每个 feature 组合多个 primitive 来实现一个用户能直接调的能力。 比如:

  • data = database + cache。行存 Postgres,热表查询走 Redis 缓存(TTL ~5s,写穿透 invalidate)。
  • realtime = database + cache。三件套(对齐 Supabase Realtime):
    • 行变更 postgres_changes:Postgres trigger 发 pg_notify,Go listener 收到后通过 cache (Redis pub/sub) 跨副本 fan out 到所有 Mortar 实例的 SSE 订阅者。
    • 广播 broadcast:客户端→客户端的瞬时消息(光标 / 拖拽中)。走 ChannelBroker,同样用 cache pub/sub fan out;收走 SSE、发走 POST .../broadcast(保持单 SSE 传输,不引入 WebSocket)。
    • 在线状态 presence:谁在线 + 各自的瞬时 state(光标位置 / 选区 / 颜色)。roster 存 cache KV(单进程则用进程内 store),TTL 由 SSE 连接的心跳续期、断连即清并广播 leave。 这三者让 Mortar 能直接撑住协作型 UI(如简版 Figma)。详见 realtime-multi-region.md
  • files = storage + database。文件 blob 在 OSS / COS / LocalDisk 里,metadata(owner / mime / size / created_at)在 Postgres 里。

Extension features(规划中)

5 core feature 之外,规划了 3 个 opt-in extension feature。每个 都是一个独立 Go 包(internal/feature/<name>/),可单独开关,不影响 core 路径。

Feature一句话开关 / 默认
cron周期任务调度 → compute Runner默认 onMORTAR_CRON=off 关闭
customdomain项目自定义域名 + ACME TLS 证书签发项目级配置,无 ENV gate
queuePostgres-backed job queue + worker SDKMORTAR_QUEUE_BACKEND=<postgres|off>(默认 postgres)

为什么把两层分开

跟 marketing / SDK 用户看到的 shape 故意不一样

看 Mortar 的角度用什么 shape
用户 SDK / docs9 capability modules on MortarClientauth db storage realtime compute usage cron queue comms)+ customDomains + tokens on AccountClient(控制平面)
内部代码 + 账单7 primitive × (5 core + 3 extension) feature
Marketing 文案”7 cost dimensions on your bill: 3 baseline + 4 metered”

跨语言 SDK 状态:TS SDK 是 reference 实现,覆盖全部 8 modules

  • customDomains。Swift / Kotlin / Dart SDK 当前只覆盖原始 6 个 (auth / db / storage / realtime / compute / usage)+ account;规划中的 cron / queue / customDomains 尚未跨语 言铺开。需要时按 TS 实现照抄即可——HTTP 接口已稳定,schema 见 openapi.yaml

这是两个正交关注点

  • Feature shape 给用户:他们问”怎么上传文件?“,找 mortar.storage.upload(),不关心底下走哪些 primitive。
  • Primitive shape 给账单 + 运维:每个 primitive = 一条云账单 = 一份用量监控 = 一个可替换的 backend。

试图把两层强行合并 = 两边都难受。保留两套 schema,每个受众读 对的那套。详细论证见 § “SDK schema vs internal schema”


架构图

              ┌──────────────────────────────────────────────┐
              │  SDK(TS / Swift / Kotlin / Dart)             │
              │   @mortar-ai/client / mortar-swift / etc.        │
              └────────────────────┬──────────────────────────┘
                                   │  HTTPS

              ┌──────────────────────────────────────────────┐
              │  Mortar HTTP API(Go, cmd/mortar)             │
              │  /v1/{tenant}/{auth | data | files | realtime |   │
              │           compute | usage}                     │
              │  Middleware: tenant + JWT + rate-limit         │
              └────────────────────┬──────────────────────────┘

              ┌────────────────────┼────────────────────┐
              ▼                    ▼                    ▼
        ┌──────────┐         ┌──────────┐         ┌──────────┐
        │ feature/ │         │ feature/ │         │ feature/ │
        │  auth    │  ...    │  data    │  ...    │ realtime │
        └─────┬────┘         └─────┬────┘         └─────┬────┘
              │                    │                    │
              └────────────────────┼────────────────────┘

       ┌──────────┬──────────┬──────────┬──────────┬──────────┐
       ▼          ▼          ▼          ▼          ▼
    compute    database    network    storage     cache
    (FC/SCF/   (Postgres / (OSS-      (OSS / COS  (Tair /
     Lambda)    RDS / Neon  egress     / S3 /      ElastiCache /
                / local)   meter)      LocalDisk)  local-redis)


              ┌──────────────────────────────────────────────┐
              │  Cloud backend                                │
              │  当前: 单 VPS(local-redis + LocalDisk +      │
              │         single Postgres)                     │
              │  规划中: 阿里云 RDS / OSS / FC / Tair          │
              │       腾讯云 CDB / COS / SCF                  │
              │  规划中: 跨 region(cn-hangzhou / cn-shenzhen)│
              └───────────────────────────────────────────────┘

控制平面(账户 / 项目生命周期 / 计费)走 /v1/_accounts/*,跟 /v1/{tenant}/* 数据平面分流。控制平面入口是 mortar CLI(@mortar/cli), 走 AccountJWT 鉴权;end-user app 流量走 APIKey 鉴权。两套互不串。


Per-primitive

每个 primitive 一个 Go 包,定义一个 interface + 多个 backend impl。 代码路径:mortar/internal/primitive/<name>/

primitive/compute

是什么:用户函数执行 runtime。POST /v1/{tenant}/compute/{fn_name} 拿一段 JS / TS 代码加 input payload,返回 output。

Backend 选项

  • 当前: 本地 Node child-process(dev only,不可生产)
  • 规划中: 阿里云 FC(Function Compute)—— 国内首选
  • 规划中: 腾讯云 SCF(Serverless Cloud Function)—— 阿里云容灾备份
  • 规划中: AWS Lambda(海外用户)

何时用谁:production 默认阿里云 FC(v3 region 覆盖最广 + 冷启动 最快);海外 region 走 Lambda;test 走本地 child-process。

关键文件mortar/internal/primitive/compute/compute.go

primitive/database

是什么:关系数据存储。Mortar 用的是 Postgres(不是 SQLite), 所有 tenant-scoped 表走 RLS 隔离(SET LOCAL mortar.tenant_id = '<uuid>')。

Backend 选项

  • 当前: 本地 Postgres 15 单实例
  • 规划中: 阿里云 RDS PostgreSQL
  • 规划中: 腾讯云 CDB PostgreSQL
  • 规划中: Neon serverless Postgres(海外按需)
  • 规划中: 跨 region read replicas(详见 read-replicas.md

何时用谁:production 走 RDS;海外走 Neon;CI / dev 走本地 Postgres docker container。

关键文件mortar/internal/primitive/database/database.go(pgx pool wrapper)+ setup.go(schema migration)+ verify_rls_integration_test.go(RLS 隔离的 contract test)。

primitive/network

是什么:出向带宽 metering。不是用户能直接调的 API —— 它是每 个其他 primitive 的副作用。我们 wrap http.ResponseWriter 统计 bytes-out,再加上 OSS access log 把 egress 算到 project 头上。

Backend 选项

  • 当前: response-writer wrapping(in-process counter)+ Postgres buffer
  • 规划中: OSS access-log 拉日志 + 离线对账
  • 规划中: 阿里云 CDN egress 直接报量(real-time)

何时用谁:所有 region 都用 response-writer wrapping(cheap + 准确);OSS / CDN egress 单独拉云账单对账。

关键文件mortar/internal/primitive/network/network.go。 adapter 在 mortar/internal/feature/usage/network_adapter.go + network_buffered.go(buffered counter)。

primitive/storage

是什么:对象存储抽象。ObjectStorage interface 4 个方法 (Put / Get / Sign / Delete)。

Backend 选项

  • 当前: LocalDisk(写 MORTAR_STORAGE_LOCAL_ROOT 磁盘 + nginx serve)
  • 规划中: 阿里云 OSS(国内首选)
  • 规划中: 腾讯云 COS
  • 规划中: AWS S3(海外)
  • 规划中: Cloudflare R2(海外,零 egress 费)

何时用谁:production 国内走阿里云 OSS(生态最完整);海外走 R2 (零 egress 友好);dev / CI 走 LocalDisk。

关键文件mortar/internal/primitive/storage/storage.go

primitive/cache

是什么:KV + pub/sub。Mortar 内部用来 (a) rate-limit 计数 (b) realtime 跨副本 fan-out (c) 查询缓存。当前内部使用,不暴露给 SDK;规划中考虑加 mortar.cache.{get,set,publish} 给用户。

Backend 选项

  • 生产默认:self-hosted Redis 跟 Postgres + mortar binary 跑在同一 台 pool ECS 上,¥0 边际成本——这是 Mortar 多租户 cache 摊薄的前提
  • Dev / tests:in-process map (backend/local)
  • managed 备选(阿里云 Tair / 腾讯云 Redis / AWS ElastiCache / Upstash): 同 Redis 协议,把 MORTAR_CACHE_REDIS_URL 指过去就行。但 ~¥100-150/GB-月 直接破坏多租户摊薄,仅 dedicated tier 或 BYOC 才用。 专属 cluster-mode / IAM backend 不内置——客户真要再加。

关键文件mortar/internal/primitive/cache/cache.go

primitive/comms

是什么:出站事务消息(email + sms 两条 ledger resource,单价 独立)。一个 primitive、按渠道两条计费维度——比 “email/sms 各自独立 primitive” 简洁,比 “塞进 network” 真实(vendor 账单确实分开)。

Backend 选项

  • Devlocal(写到 data/comms/<tenant>/outbox.jsonl,可 inspect、零云依赖)
  • 生产aliyun(Dysmsapi for SMS,复用 signACS3 签名; DirectMail email TODO)/ tencent(TODO)
  • env knob:MORTAR_COMMS_BACKEND=local|aliyun,加 MORTAR_COMMS_ ALIYUN_{ACCESS_KEY_ID,ACCESS_KEY_SECRET,SIGN_NAME,ENDPOINT}

计费:每条 message 按渠道写一条 credit_ledger 行,resource = emailsms,单价 YuanPerEmail / YuanPerSMS

关键文件mortar/internal/primitive/comms/comms.go + mortar/internal/backend/{local,aliyun}/comms.go


Per-feature

每个 feature 一个 Go 包,组合多个 primitive 实现一个 user-facing 能力。代码路径:mortar/internal/feature/<name>/

feature/auth

HTTP 路由

  • POST /v1/_accounts/signup / signin / me — Mortar 账号自身
  • POST /v1/_accounts/tokens / GET / DELETE — 账号级长效 PAT (mtr_pat_*)CRUD
  • POST /v1/{tenant}/auth/signup / signin / signout — AppUser(end user of AI bundle)
  • POST /v1/{tenant}/auth/phone/{start,verify} — phone-OTP 登录 (composes comms.sms + cache
  • POST /v1/{tenant}/auth/email/verify/{start,confirm} — 邮箱验证 (composes comms.email + cache
  • POST /v1/{tenant}/auth/password/reset/{start,confirm} — 密码找回 (composes comms.email + cache
  • POST /v1/{tenant}/_admin/keys — mint API key

组合database(存 mortar_accounts + account_keys(PAT)+ app_users + api_keys)+ comms(OTP/token 发送)+ cache (OTP/token 存储 + 发送侧限频 cooldown / daily cap)。

鉴权 token 类型

  • Account JWT —— Mortar 账户登录,scope /v1/_accounts/*
  • Account PAT —— 长效 mtr_pat_*,与 Account JWT 同 scope, 支持 MORTAR_ACCOUNT_TOKEN env 用于 CI / headless
  • API Key —— project-scope,前缀 mtr_live_ / mtr_test_
  • AppUser JWT —— end-user-of-AI-bundle 登录,scope /v1/{tenant}/{auth,data,files,realtime,compute,comms}/*

关键文件mortar/internal/feature/auth/{apikey,jwt,middleware, password,users_dao,apikeys_dao}.go + JWT keys cache keys_cache.go。详见 auth.md

feature/data

HTTP 路由

  • POST /v1/{tenant}/db/tables / GET / DELETE — table admin
  • POST /v1/{tenant}/db/{table}/rows / GET / PATCH / DELETE — row CRUD
  • GET /v1/{tenant}/db/{table}/rows?q=... — 全文检索

组合database(行存 + 元数据)+ cache(查询缓存,规划中)。

Schema 设计:用户”表”是 app_tables + app_columns 元数据; 真实行在 app_rows.data JSONB 加 GIN 索引。AI 创建新表不需要 ops 介入(无 ALTER TABLE)。tradeoff 详见 §“key design decisions”。

关键文件mortar/internal/feature/data/{tables_dao,rows_dao, fts,models}.go

feature/files

HTTP 路由

  • POST /v1/{tenant}/storage/files?bucket=&key= — 上传(raw body)
  • GET /v1/{tenant}/storage/files/{bucket}/{key...} — 下载或 redirect to 签名 URL
  • DELETE /v1/{tenant}/storage/files/{bucket}/{key...}
  • GET /v1/{tenant}/storage/sign/{bucket}/{key...} — mint signed URL

组合storage(blob)+ database(metadata 行)。

关键文件mortar/internal/feature/files/{files_dao,models}.go

feature/realtime

HTTP 路由(对齐 Supabase Realtime 三件套):

  • GET /v1/{tenant}/realtime/{table} — SSE 长连,server 推行变更事件 (postgres_changes)
  • GET /v1/{tenant}/realtime/channel/{channel} — SSE 长连,收 broadcast + presence;?ref=&key= 把本连接绑定到一个 presence 成员(断连即 leave)
  • POST /v1/{tenant}/realtime/channel/{channel}/broadcast — 发广播
  • POST /v1/{tenant}/realtime/channel/{channel}/presence — 上报 / 更新 presence state(track)
  • DELETE /v1/{tenant}/realtime/channel/{channel}/presence?ref= — 显式 离开(untrack;SSE 断连也会自动 untrack)

组合databaseLISTEN mortar_row_change)+ cache(Redis pub/sub 跨副本 fan-out + presence roster KV)。

两层 broker 设计(行变更 + channel 各一套,结构对称):

  • 单进程:InMemoryBroker / InMemoryChannelBroker + InMemoryPresenceStore
  • 多副本:RedisBroker(pub/sub) / CacheChannelBroker + CachePresenceStore —— 跨副本 fan-out via cache pub/sub;presence roster 走 cache KV
  • 跨 region(规划中):NSBroker 占位,详见 realtime-multi-region.md

关键文件mortar/internal/feature/realtime/{realtime,listener, inmemory,redis}.go

feature/usage

HTTP 路由

  • GET /v1/{tenant}/usage/me — 当前 project 余额 + 用量 + tier cap

组合database(ledger 行)。本身是只读 + 写 ledger,不 是 user-facing cost dimension。

Ledger 模型:每次 primitive 使用 → 直接追加一行 credit_ledger(tenant_id, primitive, qty, yuan, ts))。Credit 余 额 = 充值总额 - ledger 累计成本。(当前没有 usage_event / usage_ledger 两表分层 —— 只有 credit_ledger。)

关键文件mortar/internal/feature/usage/{ledger,cost,ticker, noisy_neighbor}.go。Noisy-neighbor detection(一个 project 占用 率突增)见 noisy_neighbor.go


计费模型

Credit ledger

每次 metered primitive 使用 → credit_ledger 行追加。Ticker (feature/usage/ticker.go)every 1h by default (override via MORTAR_USAGE_TICKER) 聚合 tick-driven resources(tier baseline 累计

  • storage at rest) + 扣减 project credit balance。Balance ≤ 0 触发 plan-gate 拒绝写(读仍允许,给用户机会充值)。Cache 现在纯 baseline, 不进 ledger(LRU evict 即可)。

6 cost dimensions

3 baseline(含在 tier 月费里)+ 3 metered(quota → overage → cap)。

维度类型计价计量
Computebaselinetier 月费的一部分pool ECS 时间 × vCPU 切片
Databasebaselinetier 月费的一部分self-hosted Postgres 占 ECS RAM/disk
Cachebaselinetier 月费的一部分self-hosted Redis 占 ECS RAM
Storagemetered¥0.15 / GB-月OSS / COS bucket 用量(超 quota)
Networkmetered¥0.50 / GB egress公网下行(超 quota)
Functionmetered¥0.05 / CPU-minFC / SCF / Lambda 执行时长(超 quota)

每月账单展示 6 行,每一行对得上我们付给云厂的钱。透明 + 可 审计。详见 pricing.md

Plan gate(Tier 1-5)

每个 project 选一个 Tier(1=hobby / 2=starter / 3=pro / 4=growth / 5=enterprise)。Tier 决定:

  • 每月免费额度
  • 单一 primitive 的硬上限(防 noisy neighbor)
  • 高级 feature 开关(规划中增量,如 cron 等)

Plan gate 中间件在 internal/plan/gate.go,每个 endpoint 都过;超 tier cap 返回 402 Payment Required

支付 providers

Provider区域用途
WeChat Pay国内微信扫码 / H5 / 小程序支付
Alipay国内支付宝扫码 / H5
Stripe海外国际信用卡 / Apple Pay / Google Pay

实装:mortar/internal/backend/{wechatpay,alipay}/(Stripe 规划中)。 Webhook 端点 /v1/_webhooks/payment/{provider},回调签名校验后 → ledger credit +amount 行。详见 pricing.md


控制平面入口:mortar CLI

它在哪

代码路径:mortar/cli/。Node 实现,发布为 @mortar/cli。 唯一的控制平面客户端 —— CLI 优先是因为面向 AI 的产品需要可脚本化、 可被 agent 直接调用,不依赖浏览器。Web admin UI 暂不存在;如果以后 有需求,要么生成 GitHub-style 静态 dashboard,要么走 mortar projects info 这类 CLI 命令。

mortar/cli/
├── src/
│   ├── commands/        ← projects / keys / cron / queues / realtime / functions
│   ├── lib/             ← AccountClient + project APIKey 包装
│   └── config.ts        ← ~/.mortar/config.json (mode 0600)
└── package.json

它怎么跟 HTTP API 交互

跟 end-user 流量完全分流

  • mortar CLI 走 AccountJWTmortar login 拿的),调 /v1/_accounts/* + /v1/_projects/* + /v1/_webhooks/payment/* 控制 平面 API
  • End-user app 流量走 APIKey(项目内 mint 的),调 /v1/{tenant}/{auth,data,files,realtime,compute}/* 数据平面 API

两条路径走同一份 Go binary 但不同 middleware 链。控制平面挂 accountJWTMiddleware;数据平面挂 apikeyMiddleware + tenantMiddleware + rlsSetLocalMiddleware

CLI 不需要单独部署 —— 装在用户机器 / CI runner / agent 容器里,直接 调 api.mortar.appunvs.com


多租户

Per-project tenant_id 隔离

tenant_id 是数据隔离边界 key,其值就是 project 的 UUID(project = tenant)。所有 tenant-scoped Postgres 表都有 tenant_id UUID NOT NULL 列 + RLS 策略:

CREATE POLICY tenant_isolation ON app_rows
    USING (tenant_id = auth.tenant_id());

auth.tenant_id() 是 Mortar 版的 Supabase auth.uid():一个 STABLE helper,读 current_setting('mortar.tenant_id') GUC。policy 全部用它而不是内联 current_setting,可读性更好,且迁移自 Supabase 的项目可以把 auth.uid() 形状的 policy 直接映射过来(只是隔离粒度是 project tenant_id,不是 end-user)。定义见 internal/primitive/database/setup.gorlsAndTriggers

请求进来 → tenant middleware 从 APIKeyAppUserJWT 解出 tenant_id → SET LOCAL mortar.tenant_id = '<uuid>'auth.tenant_id() 随之返回该 UUID → 之后所有 query 自动按 tenant_id filter。忘了 SET LOCAL = 零行返回(不是漏数据,是噪声明显),单测覆盖每个 endpoint 的 SET LOCAL 调用。

tenant 在 wire 上从哪来:客户端把它放进 host 子域名https://<tenant>.api.mortar.appunvs.com,dev 为 http://<tenant>.localhost:8080),调的是 tenant-less 路径 (/v1/auth/signin/v1/db/tables/.../v1/storage/...)。一个 SubdomainTenant 中间件读取最左侧 host label,在 router 之前/v1/<capability>/… 改写成 /v1/<tenant>/<capability>/…,所以下游的 router、tenant middleware、handler、egress 计量全都照旧在内部 /v1/{tenant}/… 形状上工作,不受影响。base domain 由 MORTAR_TENANT_BASE_DOMAINS 驱动(默认 api.mortar.appunvs.com,localhost);泛域名 TLS 是运维侧的事(Caddy on-demand TLS),Mortar 不签发证书。旧的 path-based 调用(直接打 apex host 的 /v1/{tenant}/…)依然有效,向后兼容。Account-scope 路由 (/v1/_accounts/*)始终留在 apex host 上、始终 tenant-less。

JWT 3 层 scope

TokenScope用途
AccountAccountJWT一个 Mortar 账户控制平面(mortar login 登录)
ProjectAPIKey (mtr_live_*)一个 project(其 tenant_id)服务器端 SDK / CI
AppUserAppUserJWT一个 project + 一个 end usermobile / web app 里的最终用户

3 层中间件分别校验,不可互替(Account 不能调 /v1/{tenant}/db,APIKey 不能调 /v1/_accounts/me)。详见 auth.md


SDK schema vs internal schema {#sdk-schema-vs-internal-schema}

Mortar 有两套并行的代码组织方式,描述同一份产品:

  1. 内部架构 —— Go binary 关心的:成本维度 + 可替换 cloud backend
  2. SDK / API surface —— 用户关心的:用哪个 capability

两者不是同形。不同关注点 → 不同分组。强行合并 → 两边都难看。

内部:7 primitive + (5 core + 3 extension) feature

见上文 § “设计原则”

SDK:12 capability 模块(mirrors HTTP paths)

下面的 /v1/{tenant}/…内部路由形状。在 wire 上 MortarClientcreateClient({ url, apiKey }) 配置(没有 tenant 选项)——tenant 跑在 host 子域名里(https://<tenant>.api.mortar.appunvs.com),SDK 打的 是 tenant-less 形式(/v1/auth/*/v1/db/*……);由 SubdomainTenant 中间件折回 /v1/{tenant}/…AccountClient 始终打 tenant-less 的 apex host(/v1/_accounts/*)。

@mortar-ai/client
├── MortarClient (project tenant,API-key auth)
│   ├── auth        ─→ /v1/{tenant}/auth/*           (AppUser 注册/登录 + phone-OTP + email verify + 密码找回)
│   ├── db          ─→ /v1/{tenant}/db/*             (表 + 行 CRUD)
│   ├── storage     ─→ /v1/{tenant}/storage/*        (文件上传/下载)
│   ├── realtime    ─→ /v1/{tenant}/realtime/{table} + /realtime/channel/{channel} (SSE:行变更 + broadcast + presence)
│   ├── compute     ─→ /v1/{tenant}/compute/*        (函数 deploy / invoke)
│   ├── usage       ─→ /v1/{tenant}/usage/me         (dashboard)
│   ├── cron        ─→ /v1/{tenant}/cron/schedules   (定时函数)
│   ├── queue       ─→ /v1/{tenant}/queue/jobs       (后台任务)
│   └── comms       ─→ /v1/{tenant}/comms/send       (email + sms 出站)
└── AccountClient (account-scope JWT 或 PAT,控制平面)
    ├── projects       ─→ /v1/_accounts/projects/*
    ├── customDomains  ─→ /v1/_accounts/projects/{id}/domains/*
    └── tokens         ─→ /v1/_accounts/tokens/*    (`mtr_pat_*` PAT)

db 域细分两个 accessor —— mortar.from(table)(hot path row CRUD)+ mortar.tables.{...}(cold path table admin)。所以 MortarClient13 个 accessor / 12 个 domaincustomDomains / tokensAccountClient 因为路由在 _accounts 下(账号 JWT 或 PAT,不是项目 API key)。

为什么三个 primitive 不出现在 SDK

  • network:不是用户能调的。是所有其他调用的副作用。我们 wrap response-writer 计 bytes-out,用户从不写 mortar.network.send()
  • cache:当前内部用(rate-limit + realtime fan-out + 查询缓 存)。规划中考虑加 mortar.cache.{get,set,publish} 给用户。
  • compute:是 ECS instance 时间分摊,没运行时 API 可调。 现身于 tier baseline 月费 + noisy-neighbor monitor 的 per-tenant CPU 归因(internal/primitive/compute/compute.goTracker interface)。

为什么 auth + usage 不是 primitive

它们是 feature(组合 primitive 的能力),不是 cost dimension。

  • auth 用 database(存 user 行)+ network(egress on signin response),但自己不是一条账单线
  • usage只读 ledger meta-layer,本身不产生 billable work。

跨产品定位

Mortar vs Fabric

核心区别:Mortar 是给 AI-generated app 的 backend;Appunvs AI Editor 是生成 app 的编辑器

MortarFabric
角色Backend-as-a-ServiceAI-powered IDE
谁的客户AI bundle 的 end users(AppUser)编辑器的开发者用户
自己用什么 backend自己的 Postgres + Redis + OSSfabric-server 的 Postgres + Redis + LocalFS
数据库里有什么AppUser 注册 / app data / filesUser 账号 / project metadata / ai_turns / 构建产物

⚠️ Fabric 自己不跑在 Mortar 上。User accounts / project metadata / ai_turns / project builds / bundle storage 都在 fabric-server 自己的 Postgres + Redis + LocalFS 里。同理,AI 构建器 自己的编辑器也不跑在它对外提供的 BaaS 上。只有 AI bundle 自己的 end-user-facing data 走 Mortar(via host bridge)。

详见 fabric/docs/architecture.md § “Fabric 自身基础设施”。

Mortar vs Keel

完全独立。Keel works with 任何 backend(Supabase / Firebase / 自家 / Mortar);Mortar works with 任何 runtime(Keel / Expo / RN 裸 / web / Flutter / native)。

  • 没有 @keel-ai/mortar plugin —— 用户接 Mortar 用标准 @mortar-ai/client,跟接 Supabase 用 @supabase/supabase-js 同一种方式
  • Mortar 文档不提 Keel;Keel 文档把 Mortar 当”众多 BaaS 选项之 一”提一句

为什么:让两边各自能独立卖给非交叉客户。Mortar 的 ICP 是”想要 国内合规 BaaS 的开发者”,Keel 的 ICP 是”想要国内 RN pipeline 的开 发者”,这两群人有交集但不必绑定

详见 keel/docs/architecture.md § “跟 Mortar 的关系”。


Key design decisions(回顾)

1. Postgres from day 1(不是 SQLite)

SQLite 适合单 binary 本地 app。Mortar 是 service,最终走 managed Postgres(阿里云 RDS / Neon / Supabase postgres)。SQLite 起步 = 之后 schema + driver rewrite 痛苦。前期多扛一点 dev-loop 开销。

2. RLS 是唯一多租户安全网

API 层 SET LOCAL mortar.tenant_id = '<uuid>',RLS 策略在每个 tenant-scoped 表上强制 tenant_id = current_setting('mortar.tenant_id')。 忘 SET LOCAL → 零行返回,单测覆盖、噪声明显,不会静默泄露跨租户 数据

3. JSONB 行存,不是用户驱动 DDL

app_tables + app_columns 是元数据;行真实数据在 app_rows.data JSONB + GIN 索引。AI 能 host().db.from('todos').insert({...}) 不需要 ops 介入。tradeoff 表见旧 architecture.md 第 78-82 行。

4. SSE realtime,不是 WebSocket

SSE 单向(server → client)over plain HTTP。Mortar realtime 需求 就是”行变更推送”,SSE 完美匹配 + 0 额外 infra(HTTP/1.1 long poll)+ 过任何代理 / CDN / 公司防火墙(只要允许 HTTP)。

broadcast + presence 也照样走 SSE,不引入 WebSocket。 看似需要双向 的协作(光标 / presence),我们拆成「收走 SSE + 发走 HTTP POST」: 客户端在 GET /v1/{tenant}/realtime/channel/{channel} 上收 broadcast / presence diff,发广播走 POST .../broadcast、上报 presence 走 POST .../presence。WebSocket 买的”同一条连接双向”对我们是负担(要 sticky session / 自建心跳 / 过不了某些代理),而 POST+SSE 天然无状态——任何副本 都能服务任何请求,fan-out 走 cache pub/sub。写已经走 POST,不需要第二个 写 channel。

5. ObjectStorage interface,LocalDisk default

storage.ObjectStorage 4 个方法(Put / Get / Sign / Delete)。当前 LocalDiskMORTAR_STORAGE_LOCAL_ROOT + nginx serve;规划中 AliyunOSS / TencentCOS / S3 impl 同接口。换 backend = 改 ENV。

6. Notify-driven realtime

Postgres trigger on app_rows INSERT/UPDATE/DELETE → pg_notify('mortar_row_change', payload)。Mortar Go listener LISTEN mortar_row_change → 解 payload → fan out 到 SSE subscribers。Redis pub/sub 用于跨副本(>1 个 Mortar 进程) fan out。详见 realtime-multi-region.md


暂不做的(已明确)

  • Edge Functions(用户任意代码) → 规划中通过 primitive/compute
    • 阿里云 FC 加
  • Vector / embeddings / pgvector → 规划中 rag-cookbook.md
  • OAuth providers → email + password only;规划中加微信 / Apple ID
  • Multi-region → 规划中 multi-region.md
  • Auto-scaling / k8s → 规划中
  • Web admin dashboard → 永久不做;控制平面入口是 mortar CLI (mortar/cli/)。面向 AI 的产品优先 CLI 先,浏览器 UI 不 是必需路径
  • Stripe / Alipay billing → 已有,详见 pricing.md

参考