Twenty 开源 CRM 实战指南:以 create-twenty-app 与 defineObject 为核心,把 CRM 当代码来构建、发布与自托管
Twenty 是一个以"像构建软件一样构建 CRM"为核心理念的开源 CRM 项目,官方定位为 Salesforce 的开源替代(The open alternative to Salesforce, designed for AI)。本文以仓库根目录的 README.md 为骨架,完整覆盖其中的三条使用路径(云版、以代码构建应用、自托管),并结合 create-twenty-app 脚手架源码、twenty-sdk 的实体定义与发布命令实现、Docker Compose 编排文件,讲透"从一行 npx create-twenty-app my-app 到 app:publish 发布"的完整链路及其底层机制。
一、Why Twenty:把 CRM 当作可版本化的代码资产
README 中"Why Twenty"一节给出了项目的核心主张:Twenty 给技术团队提供构建定制化 CRM 的"building blocks"(构建块),使其能匹配复杂业务需求,并随业务演进而快速调整;Twenty 是一个"你可以像构建、发布和版本管理你技术栈中的其他部分一样去构建和版本管理的 CRM"。
这个主张在仓库中得到了结构性印证:
- 根目录 package.json 声明了 18 个 workspace(
twenty-front、twenty-server、twenty-sdk、twenty-client-sdk、twenty-apps等),整个产品本身就是一个可用 Git 管理的代码资产; - "以代码定义"的对象模型 API 位于 twenty-sdk 的 define 模块,实体(object)、视图、逻辑函数、Agent 都通过类型安全的 TypeScript 配置声明;
- 应用的发布被设计为两种目标:npm 包,或 Twenty 服务端的应用注册表(registry),对应
app:publish命令的两个分支。
项目采用 AGPL-3.0 许可证(见 package.json 的 license 字段)。README 首页自称 "The #1 Open-Source CRM",这是项目方的自我定位表述,供读者自行甄别。
二、三条使用路径:云版、构建应用、自托管
README 的 Installation 一节给出三种接入方式,下面逐一展开,并补充仓库中的实际实现细节。
2.1 Cloud(云版)
README 说明这是最快的上手方式:注册后一分钟之内即可拥有工作区,无需管理基础设施,且始终使用最新版本。云版服务托管于 twenty.com(README 原文的外部链接,此处不再重复输出),开发者通常用云版做产品验证,用自托管或本地实例做深度定制。
2.2 Build an app(以代码构建应用)
README 给出的完整流程是:
npx create-twenty-app my-app
然后用 defineObject 以代码定义对象、字段(README 原始示例完整保留):
import { defineObject, FieldType } from 'twenty-sdk/define';
export default defineObject({
nameSingular: 'deal',
namePlural: 'deals',
labelSingular: 'Deal',
labelPlural: 'Deals',
fields: [
{ name: 'name', label: 'Name', type: FieldType.TEXT },
{ name: 'amount', label: 'Amount', type: FieldType.CURRENCY },
{ name: 'closeDate', label: 'Close Date', type: FieldType.DATE_TIME },
],
});
最后发布到工作区:
npx twenty app:publish --private
这三条命令在仓库中分别对应 create-twenty-app 包(当前版本 2.38.0,bin 入口为 dist/cli.cjs)、define-object.ts 的实体定义实现,以及 app 命令注册器。下一节将逐一剖析其源码行为。
2.3 Self-hosting(自托管)
README 指出自托管有两条路线:
- 用 Docker Compose 在自己的基础设施上运行 Twenty;
- 按本地开发指南贡献代码(本地 setup)。
仓库中 docker-compose.yml 编排了核心服务,关键配置如下(摘自该文件):
| 服务/配置 | 值 | 说明 |
|---|---|---|
| server 镜像 | twentycrm/twenty:${TAG:-latest} |
主服务容器 |
| 端口 | 3000:3000 |
server 监听端口,健康检查为 curl --fail http://localhost:3000/healthz |
PG_DATABASE_URL |
postgres://postgres:postgres@db:5432/default(均可用环境变量覆盖) |
PostgreSQL 连接串 |
REDIS_URL |
redis://redis:6379 |
Redis 连接串,供 BullMQ 队列等使用 |
ENCRYPTION_KEY / FALLBACK_ENCRYPTION_KEY |
需注入 | 字段加密密钥,自托管必填 |
APP_SECRET |
需注入 | 应用签名密钥 |
| worker 命令 | yarn worker:prod |
独立 worker 容器处理队列任务,且 DISABLE_DB_MIGRATIONS=true(迁移只在 server 上跑一次) |
compose 文件还支持可选的 S3 对象存储(STORAGE_TYPE、STORAGE_S3_*)、SMTP 邮件(EMAIL_DRIVER、EMAIL_SMTP_*)以及 Google/Microsoft 日历与邮件集成等环境变量,均以注释形式给出,按需打开。
除 Compose 外,packages/twenty-docker 还提供了一键脚本(scripts/1-click.sh、scripts/install.sh)、Kubernetes 清单(k8s/manifests 及 Terraform)、Helm Chart(helm/twenty)以及 Podman 部署方案(podman/podman-compose.yml),覆盖了从单机到集群的常见部署形态。
三、脚手架深度剖析:create-twenty-app 的完整参数与执行流程
3.1 命令参数全集
从 cli.ts 的 commander 注册可以看出,除了 README 中演示的位置参数 directory,脚手架还支持以下选项:
| 参数 | 说明 |
|---|---|
[directory] |
项目目录,只允许小写字母、数字和连字符(/^[a-z0-9-]+$/ 校验) |
-n, --name <name> |
应用名(不可为空字符串) |
-d, --display-name <displayName> |
应用显示名 |
--description <description> |
应用描述 |
--url <url> |
Twenty 服务器地址,默认 http://localhost:2020 |
--api-url <apiUrl> |
已废弃,会打印警告并回退到 --url |
--authentication-method <method> |
oauth 或 apiKey;缺省时本地实例用 apiKey、远程实例用 oauth |
3.2 七步执行流程
create-app.command.ts 中的 CreateAppCommand.execute 把整个过程组织为带步骤计数的流水线(本地实例 6 步、远程实例 5 步):
- 创建项目目录:
fs.ensureDir建立目标目录; - 脚手架模板文件:调用 app-template.ts 的
copyBaseApplicationProject; - 安装依赖;
- 初始化 Git 仓库:
tryGitInit在main分支创建首个 commit,失败或已处于仓库中则跳过; - 启动 Twenty 服务器(仅本地实例):要求 Docker 已安装且正在运行,后台
docker pull twentycrm/twenty-app-dev:latest后调用serverStart; - 认证:优先复用
~/.twenty/config.json中已有的 remote 凭据(会真实调用{serverUrl}/metadata的currentWorkspace查询验证 token 有效);否则按认证方式走 OAuth 浏览器流程,或使用本地开发 API key(日志显示为Authenticated as tim@apple.dev (development API key)); - 安装应用:在应用目录内执行
yarn twenty dev --once完成一次性同步;成功后还会解析工作区前端的子域名/自定义域名与主页面布局的universalIdentifier,拼接出https://<workspace>/page/<id>并在浏览器打开欢迎页(Windows 用rundll32、macOS 用open、Linux 用xdg-open,且 URL 会做协议白名单净化)。
几个值得注意的行为约束:
- 目标目录已存在且非空时会直接抛错(
validateDirectory); - 对远程实例指定
--authentication-method apiKey会被拒绝并自动切换为 OAuth("API key authentication is only supported on a local Docker instance"); - 认证或同步失败不会中断脚手架本身,而是提示手动补救:
yarn twenty remote:add或yarn twenty dev --once。
3.3 模板生成了什么
模板源文件位于 src/constants/template,生成的应用是一个完整的 TypeScript 工程。copyBaseApplicationProject 在复制之后还做了四件"个性化"工作:
- 补回点文件:npm 打包会剥离 dotfile,因此模板中以
gitignore、github、yarnrc.yml存放,复制后重命名为.gitignore、.github、.yarnrc.yml(其中含 CI/CD/Publish 三个 GitHub Actions 工作流); - 镜像 AGENTS.md 为 CLAUDE.md:以 AGENTS.md 为跨工具单一事实来源(源码注释说明了原因:Claude Code 优先读 CLAUDE.md);
- 生成唯一标识:将 universal-identifiers.ts 中的占位符替换为真实值——
DISPLAY-NAME-TO-BE-GENERATED、DESCRIPTION-TO-BE-GENERATED以及多个UUID-TO-BE-GENERATED(用uuid.v4()生成)。这解释了为什么 Twenty 的实体体系强调universalIdentifier:它在跨工作区迁移应用时保证身份唯一且稳定; - 更新 package.json:写入应用名,并把
twenty-sdk、twenty-client-sdk的版本固定为与create-twenty-app相同的版本号,保证 CLI 与 SDK 版本对齐。
模板自带的 application-config.ts 展示了应用的顶层入口:
import { defineApplication } from 'twenty-sdk/define';
import {
APP_DESCRIPTION,
APP_DISPLAY_NAME,
APPLICATION_UNIVERSAL_IDENTIFIER,
} from 'src/constants/universal-identifiers';
export default defineApplication({
universalIdentifier: APPLICATION_UNIVERSAL_IDENTIFIER,
displayName: APP_DISPLAY_NAME,
description: APP_DESCRIPTION,
});
模板目录还包含 src/page-layouts/main-page.page-layout.ts(页面布局)、src/front-components/main-page.tsx(前端组件)、src/navigation-menu-items/(导航菜单)、src/default-role.ts(默认角色)以及 vitest 测试配置——即一个应用天然具备"布局 + 组件 + 导航 + 权限"四类构件的骨架。模板 package.json 的脚本约定了日常开发命令:
{
"scripts": {
"twenty": "twenty",
"lint": "oxlint -c .oxlintrc.json .",
"typecheck": "tsgo --noEmit -p tsconfig.spec.json",
"test": "vitest run"
}
}
运行环境要求 Node ^24.5.0、Yarn >=4.0.2(与根仓库 package.json 的 engines 与 packageManager: yarn@4.13.0 一致)。
四、以代码定义数据模型:defineObject 的校验机制
README 示例中 defineObject 返回的对象并不只是配置,而是一个带校验结果的结构。阅读 define-object.ts 可以看到其校验规则:
- 必填项:
universalIdentifier、nameSingular、namePlural、labelSingular、labelPlural任一缺失都会产生 error(这意味着 README 的最简示例在实际同步时通常还需要补universalIdentifier,可由脚手架模板的universal-identifiers.ts机制生成); - 字段校验:
fields数组逐一走validateFields; - 交叉引用校验:若声明了
labelIdentifierFieldMetadataUniversalIdentifier,它必须指向fields中真实存在的字段的universalIdentifier,否则报错; - 默认值警告:
getFieldDefaultValueWarnings会针对字段的defaultValue产生 warnings。
校验结果通过 createValidationResult({ config, errors, warnings }) 返回,errors 与 warnings 分离,使"配置错误导致发布失败"和"配置不佳的提示"能在 CLI 中区别对待。
FieldType 的取值与 Twenty 服务端的字段元数据类型一致,可对照 FieldMetadataType.ts 中的枚举(如 CURRENCY = 'CURRENCY'、DATE_TIME = 'DATE_TIME' 等),每类字段还有各自的设置与默认值类型约束(见 FieldMetadataSettings.ts、FieldMetadataDefaultValue.ts),例如 TEXT 的默认值类型是 string | null。从这套类型结构可以推断,twenty-sdk/define 是前端类型系统对服务端元数据模型的一比一映射,保证了"以代码定义的 CRM"在编译期就与运行时语义对齐。
defineObject 的单元测试见 define-object.spec.ts。
五、发布链路:app:publish、app:install 与 app:uninstall
README 中 npx twenty app:publish --private 的语义在 app/index.ts 中有精确定义:
program
.command('app:publish [appPath]')
.description('Build and publish to npm (default) or server registry')
.option('--private', "Push to a Twenty server's registry instead of npm")
.option('-r, --remote <name>', 'Publish to a specific remote (with --private)')
.option('--tag <tag>', 'npm dist-tag (e.g. beta, next)')
即:默认行为是构建并 npm publish;加上 --private 才改为推送到 Twenty 服务端的应用注册表(可用 -r 指定多 remote 场景下的目标)。这解释了 README 选择 --private 演示的原因——面向自己工作区发布时走注册表,面向市场分发时走 npm。
与之配套的两个命令:
app:install [appPath] -r <remote>:把已部署的应用安装到服务端(服务端安装);app:uninstall [appPath] -y:从服务端卸载,-y跳过确认提示。
开发态与发布态的分工也很清晰:本地开发用 yarn twenty dev(开发模式,热同步 + 客户端/类型刷新),一次性同步用 yarn twenty dev --once(脚手架内部调用的正是它)。twenty-sdk 的 README 补充了手动集成方式——在已有项目中 yarn add twenty-sdk twenty-client-sdk,并在 package.json 加 "scripts": { "twenty": "twenty" };CLI 凭据按 remote 存储在 ~/.twenty/config.json,可用 yarn twenty remote:add 配置、yarn twenty remote:list 查看;认证错误时重新执行 remote:add,类型过期时重启 yarn twenty dev 即可刷新。
六、单仓库结构与技术栈:README "Stack" 一节的仓库实证
README 的 Stack 一节声明:TypeScript、Nx、NestJS(配 BullMQ)、PostgreSQL、Redis、React(配 Jotai、Linaria、Lingui)。以 package.json 的 workspaces 列表对照仓库目录,可得到如下模块地图:
| 包 | 角色 |
|---|---|
twenty-server |
NestJS 后端(GraphQL API、队列 worker、数据库迁移,源码位于 src,入口 main.ts) |
twenty-front |
React 前端(src,配 Jotai 状态、Lingui 国际化——src/locales 下有多语言 .po 文件) |
twenty-sdk |
开发者 CLI 与 twenty-sdk/define 定义层 |
twenty-client-sdk |
面向应用开发者的运行时客户端(src,含元数据客户端生成器 generate-metadata-client.ts) |
twenty-ui |
设计系统组件库 |
twenty-shared |
前后端共享的类型与工具(前文引用的 FieldMetadataType 即在此) |
twenty-emails |
模板邮件 |
twenty-website / twenty-docs |
官网与文档站(文档源码见 packages/twenty-docs) |
twenty-apps |
官方应用示例与 fixtures(examples 中如 hello-world、document-generator、postcard,可直接作为应用开发范本) |
twenty-e2e-testing |
Playwright 端到端测试 |
create-twenty-app / twenty-cli |
应用脚手架与 CLI(twenty-cli 已是废弃占位包) |
twenty-codex-plugin / twenty-claude-skills / twenty-oxlint-rules |
AI 辅助开发插件与自定义 lint 规则 |
根仓库的本地开发入口是一条 start 脚本:
concurrently --kill-others \
'nx run-many -t start -p twenty-server twenty-front' \
'wait-on tcp:3000 && nx run twenty-server:worker'
即并行拉起 server 与 front,等 3000 端口就绪后再启动 worker。仓库还内置了 oxlint 自定义规则(packages/twenty-oxlint-rules,如 GraphQL resolver 必须加守卫、禁止在 selector 中直接访问 atom family 等),体现其"约束随代码库演进"的工程文化。
七、上手路径小结
| 目标 | 命令 |
|---|---|
| 脚手架新应用 | npx create-twenty-app my-app(支持 --name / --url / --authentication-method 等选项) |
| 本地开发(热同步) | yarn twenty dev |
| 一次性同步 | yarn twenty dev --once |
| 配置/查看远端 | yarn twenty remote:add、yarn twenty remote:list |
| 发布到自己的工作区 | npx twenty app:publish --private(可加 -r <remote>) |
| 安装/卸载服务端应用 | npx twenty app:install <appPath> / npx twenty app:uninstall <appPath> -y |
| 自托管 | docker compose -f packages/twenty-docker/docker-compose.yml up 或 1-click.sh / k8s 清单 |
需要说明的适用前提:应用开发链路要求 Node ^24.5.0 与 Yarn 4.x;本地 Docker 实例默认服务地址为 http://localhost:2020(脚手架 --url 缺省值),而自托管 compose 部署的对外端口是 3000,两者场景不同,连接远程实例时请以实际 --url/remote:add 配置为准。
八、生态与社区
README 的 Thanks 一节感谢了 Greptile(代码评审)、Sentry(缺陷捕获)与 Crowdin(翻译协作)三个工具服务;仓库内多语言翻译资产(twenty-front、twenty-emails、twenty-website 的 locales 目录下的 .po 文件)即由该翻译流程维护。贡献者可以在 packages/twenty-docs 中查看并参与文档建设(含 developers/contribute 下的贡献指南),根目录的 AGENTS.md 与 CLAUDE.md 则定义了面向 AI 协作开发的仓库约定。
结语:Twenty README 的核心信息可以浓缩为一句话——CRM 的一切构件(对象、字段、视图、布局、权限、逻辑)都可以通过 twenty-sdk/define 以类型安全的代码声明,并经 create-twenty-app → twenty dev → app:publish 这条链路完成开发、同步与发布;自托管侧则由 Docker Compose/K8s/Helm 提供完整编排。读懂本文对应的 README.md、create-twenty-app 源码 与 twenty-sdk define 模块,即可在 Twenty 上建立"以代码构建 CRM"的完整工程实践。
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0623
Hy4-previewHy4 preview 是由腾讯混元团队研发的新一代混合专家(MoE)旗舰模型。模型总参数量 770B,每个 token 激活 49B,主干共包含78层,第一层采用标准 FFN,其余 77 层均为 MoE 结构,每层包含 256 个路由专家与 1 个共享专家,每个 token 激活 top-8 路由专家及共享专家。主干之外原生内置 1 层 MTP(总参数量 10B,激活 0.7B)以支持投机解码。Python00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
GLM-5.3-FlashGLM-5.3-Flash (320B-A18B),是GLM-5系列的首个原生多模态模型。320B总参数,能力超过GLM-5.2Jinja00
Spark-X2.5-4BSpark-X2.5-4B 旨在让强大的 AI 更实用、更高效、更易获得。在广泛日常任务中表现强劲,涵盖对话、写作、翻译、推理、编码、工具调用以及智能体工作流,并在同等规模的开源模型中取得领先成绩。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00