多租户与 RLS
每个项目都是独立的空间。隔离发生在数据库层,而不是应用层:即使应用层出错,PostgreSQL 行级安全也不会让一个租户的数据泄露到另一个租户。
API 优先的多租户无头 CMS。读取路径从不经过 API —— 访客越多,上涨的是缓存命中率,而不是您的账单。
您的数据存放在我们自己运维的服务器上没有按用量计费的第三方服务企业版可部署到您自己的服务器
每个组件都在我们自己的基础设施上
功能
一切都在 API 之后;没有任何功能被锁在面板里。
每个项目都是独立的空间。隔离发生在数据库层,而不是应用层:即使应用层出错,PostgreSQL 行级安全也不会让一个租户的数据泄露到另一个租户。
内容类型不写死在代码里;您可以在面板或通过 API 定义。字段带有 title、slug、body 等角色 —— 构建流水线和 SEO 生成看的是角色,而不是字段名。
媒体、搜索、Webhook、SEO、GraphQL、导入、分析 —— 每一项都独立且可关闭。关闭不会删除数据;重新启用后,模块会从中断处继续。
已发布的内容在构建时写入不可变地址,并由 nginx 边缘缓存提供。无论内容条数还是访客数量,都不会抬高一次读取的成本。
兼容 S3 的对象存储(Garage)、跨三个区域复制的图片以及自动生成的衍生图。您上传的文件留在我们自己的基础设施上,绝不会进入按用量计费的 CDN。
基于 Typesense 的多语言全文搜索。即使访客把「Istanbul」拼成「Istanul」,也能找到正确的内容;响应在 50 毫秒以内返回。
发布、更新、删除 —— 每个事件都会通知到您的系统。投递保存在持久队列中,并以指数退避方式重试;即使进程崩溃,事件也不会丢失。
模型无需手写,而是由空间的内容类型推导而来。新增一个类型,它立即可被查询。深度和长度限制是强制的,用于防御查询炸弹。
@corpusctl/client 附带 React、Vue 和 Next.js 适配器。区块渲染器不带任何默认样式 —— 我们不强加一行 CSS,设计完全由您决定。
代码示例
面板中能做的每件事都有对应的 API;没有任何功能被锁在界面里。
# 1) 定义内容类型 —— 模型不写死在代码里
curl -X PUT "$API/v1/content-types/post" \
-H "authorization: Bearer $TOKEN" \
-H "x-corpusctl-tenant: $TENANT" \
-H "content-type: application/json" \
-d '{
"name": "post",
"title": "Post",
"fields": [
{ "name": "title", "type": "text", "role": "title", "required": true },
{ "name": "slug", "type": "slug", "role": "slug", "required": true },
{ "name": "body", "type": "blocks", "role": "body" }
]
}'
# 2) 创建草稿
curl -X POST "$API/v1/documents" \
-H "authorization: Bearer $TOKEN" \
-H "x-corpusctl-tenant: $TENANT" \
-d '{ "type": "post", "data": { "title": "Hello", "slug": "hello" } }'
# 3) 发布 —— 校验、构建与推送到边缘,一次调用完成
curl -X POST "$API/v1/documents/$ID/publish" \
-H "authorization: Bearer $TOKEN" \
-H "x-corpusctl-tenant: $TENANT"
# → { "data": { "version": 3, "contentHash": "…", "durationMs": 42 } }import { ReadClient } from '@corpusctl/client'
import { Content } from '@corpusctl/react'
// ReadClient 不携带任何令牌,也从不调用 API —— 它直接从边缘读取。
const client = new ReadClient({
tenantId: '3f2b…',
edgeUrl: 'https://cdn.example.com',
manifestTtlMs: 60_000,
})
export default async function Page({ params }) {
// slug → 分片计算(本地)→ 清单 → 不可变地址。
// 两个 HTTP 请求,都来自缓存。找不到时返回 undefined,
// 不会抛出错误 —— 是否 404 由您的页面决定。
const post = await client.getBySlug(params.slug)
if (!post) notFound()
return (
<article>
<h1>{post.data.title}</h1>
{/* 零默认样式:每种区块类型都可覆盖 */}
<Content value={post.data.body} components={{ ... }} />
</article>
)
}# 模型无需手写:由空间的内容类型推导而来。
# 只要定义了「post」类型,它立即可被查询。
query GetPost($slug: String!) {
post(slug: $slug) {
title
slug
body
# 计算字段也在模型中:阅读时长、字数……
readingTime
}
}
# 列表查询 —— 游标分页,没有 offset
query LatestPosts {
postList(limit: 10) {
data { title slug }
cursor
}
}ReadClient 不携带密钥令牌,可在浏览器中运行;ManagementClient 仅限服务端。如果合并成一个类,泄露令牌就只是一行代码的失误。架构
点击发布会触发一次构建;访客读到的是那次构建的产物,而不是系统本身。
内容以其哈希寻址。同一个地址永远不会返回不同的内容,因此可以缓存 30 天;发布时会刷新清单。
读取哪个清单分片由客户端根据 slug 计算。即使空间里有一万条内容,也只下载一个很小的文件。
每条记录都有版本,旧版本可以恢复为草稿。可以安排未来时间发布;调度器会自行触发构建。
对比
我们并非每一行都胜出。这张表存在的意义,是让您清楚看到自己选择了什么。
| 对比项 | corpusctl | Contentful | DatoCMS |
|---|---|---|---|
| 计费模式 | 固定套餐。读取请求不经过 API,因此没有可计量的调用。 | 按 API 调用、记录数和用户数计费;超额另行收费。 | 按 API 调用和流量计费。 |
| 多项目管理 | 内建于核心。一个面板、无限空间;用户在每个空间中拥有各自的角色。 | 每个项目是一个独立的「space」;多数套餐按 space 收费。 | 每个项目是一个独立环境;按项目计套餐。 |
| 租户隔离 | 在数据库层面 —— PostgreSQL 行级安全(RLS)。 | 在应用层面;细节未公开。 | 在应用层面;细节未公开。 |
| 数据存放位置 | 我们自己运维的服务器;企业版可完全部署到您自己的服务器。 | 厂商的云;高阶套餐可选区域。 | 厂商的云;高阶套餐可选区域。 |
| 按用量计费的第三方服务 | 没有。PostgreSQL、Valkey、Typesense、Garage、nginx —— 全部在我们自己的基础设施上。 | 有;搜索、图片和 CDN 层按用量计费。 | 有;图片 CDN 与图像处理按用量计费。 |
| 强加的渲染方式 | 没有。区块渲染器不带任何默认样式;遇到未知区块会跳过,绝不崩溃。 | 提供富文本渲染器;样式由您决定。 | 提供结构化文本渲染器;样式由您决定。 |
| 关闭功能 | 每项功能都是可关闭的模块;关闭不会删除数据,降级套餐同样不会。 | 套餐限制会让功能不可用。 | 套餐限制会让功能不可用。 |
| 全球边缘网络 | 单区域边缘缓存。我们没有全球 PoP 网络。 | 多区域全球 CDN。 | 多区域全球 CDN。 |
| 应用市场 | 没有。集成通过 Webhook 和 API 自行编写。 | 广泛且成熟的应用生态。 | 拥有插件生态。 |
| 企业级成熟度 | 年轻的产品;SSO 与正式 SLA 在路线图上。 | SOC 2、SSO、SLA —— 成熟。 | 高阶套餐提供 SSO 与 SLA。 |