首页
/ Twenty 开源 CRM 实战指南:以 create-twenty-app 与 defineObject 为核心,把 CRM 当代码来构建、发布与自托管

Twenty 开源 CRM 实战指南:以 create-twenty-app 与 defineObject 为核心,把 CRM 当代码来构建、发布与自托管

2026-09-05 21:44:56作者:殷蕙予

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-appapp:publish 发布"的完整链路及其底层机制。

一、Why Twenty:把 CRM 当作可版本化的代码资产

README 中"Why Twenty"一节给出了项目的核心主张:Twenty 给技术团队提供构建定制化 CRM 的"building blocks"(构建块),使其能匹配复杂业务需求,并随业务演进而快速调整;Twenty 是一个"你可以像构建、发布和版本管理你技术栈中的其他部分一样去构建和版本管理的 CRM"。

这个主张在仓库中得到了结构性印证:

  • 根目录 package.json 声明了 18 个 workspace(twenty-fronttwenty-servertwenty-sdktwenty-client-sdktwenty-apps 等),整个产品本身就是一个可用 Git 管理的代码资产;
  • "以代码定义"的对象模型 API 位于 twenty-sdk 的 define 模块,实体(object)、视图、逻辑函数、Agent 都通过类型安全的 TypeScript 配置声明;
  • 应用的发布被设计为两种目标:npm 包,或 Twenty 服务端的应用注册表(registry),对应 app:publish 命令的两个分支。

项目采用 AGPL-3.0 许可证(见 package.jsonlicense 字段)。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 指出自托管有两条路线:

  1. 用 Docker Compose 在自己的基础设施上运行 Twenty;
  2. 按本地开发指南贡献代码(本地 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_TYPESTORAGE_S3_*)、SMTP 邮件(EMAIL_DRIVEREMAIL_SMTP_*)以及 Google/Microsoft 日历与邮件集成等环境变量,均以注释形式给出,按需打开。

除 Compose 外,packages/twenty-docker 还提供了一键脚本(scripts/1-click.shscripts/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> oauthapiKey;缺省时本地实例用 apiKey、远程实例用 oauth

3.2 七步执行流程

create-app.command.ts 中的 CreateAppCommand.execute 把整个过程组织为带步骤计数的流水线(本地实例 6 步、远程实例 5 步):

  1. 创建项目目录fs.ensureDir 建立目标目录;
  2. 脚手架模板文件:调用 app-template.tscopyBaseApplicationProject
  3. 安装依赖
  4. 初始化 Git 仓库tryGitInitmain 分支创建首个 commit,失败或已处于仓库中则跳过;
  5. 启动 Twenty 服务器(仅本地实例):要求 Docker 已安装且正在运行,后台 docker pull twentycrm/twenty-app-dev:latest 后调用 serverStart
  6. 认证:优先复用 ~/.twenty/config.json 中已有的 remote 凭据(会真实调用 {serverUrl}/metadatacurrentWorkspace 查询验证 token 有效);否则按认证方式走 OAuth 浏览器流程,或使用本地开发 API key(日志显示为 Authenticated as tim@apple.dev (development API key));
  7. 安装应用:在应用目录内执行 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:addyarn twenty dev --once

3.3 模板生成了什么

模板源文件位于 src/constants/template,生成的应用是一个完整的 TypeScript 工程。copyBaseApplicationProject 在复制之后还做了四件"个性化"工作:

  1. 补回点文件:npm 打包会剥离 dotfile,因此模板中以 gitignoregithubyarnrc.yml 存放,复制后重命名为 .gitignore.github.yarnrc.yml(其中含 CI/CD/Publish 三个 GitHub Actions 工作流);
  2. 镜像 AGENTS.md 为 CLAUDE.md:以 AGENTS.md 为跨工具单一事实来源(源码注释说明了原因:Claude Code 优先读 CLAUDE.md);
  3. 生成唯一标识:将 universal-identifiers.ts 中的占位符替换为真实值——DISPLAY-NAME-TO-BE-GENERATEDDESCRIPTION-TO-BE-GENERATED 以及多个 UUID-TO-BE-GENERATED(用 uuid.v4() 生成)。这解释了为什么 Twenty 的实体体系强调 universalIdentifier:它在跨工作区迁移应用时保证身份唯一且稳定;
  4. 更新 package.json:写入应用名,并把 twenty-sdktwenty-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.jsonenginespackageManager: yarn@4.13.0 一致)。

四、以代码定义数据模型:defineObject 的校验机制

README 示例中 defineObject 返回的对象并不只是配置,而是一个带校验结果的结构。阅读 define-object.ts 可以看到其校验规则:

  • 必填项universalIdentifiernameSingularnamePlurallabelSingularlabelPlural 任一缺失都会产生 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.tsFieldMetadataDefaultValue.ts),例如 TEXT 的默认值类型是 string | null。从这套类型结构可以推断,twenty-sdk/define 是前端类型系统对服务端元数据模型的一比一映射,保证了"以代码定义的 CRM"在编译期就与运行时语义对齐。

defineObject 的单元测试见 define-object.spec.ts

五、发布链路:app:publishapp:installapp: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:addyarn 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 up1-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-fronttwenty-emailstwenty-websitelocales 目录下的 .po 文件)即由该翻译流程维护。贡献者可以在 packages/twenty-docs 中查看并参与文档建设(含 developers/contribute 下的贡献指南),根目录的 AGENTS.mdCLAUDE.md 则定义了面向 AI 协作开发的仓库约定。

结语:Twenty README 的核心信息可以浓缩为一句话——CRM 的一切构件(对象、字段、视图、布局、权限、逻辑)都可以通过 twenty-sdk/define 以类型安全的代码声明,并经 create-twenty-apptwenty devapp:publish 这条链路完成开发、同步与发布;自托管侧则由 Docker Compose/K8s/Helm 提供完整编排。读懂本文对应的 README.mdcreate-twenty-app 源码twenty-sdk define 模块,即可在 Twenty 上建立"以代码构建 CRM"的完整工程实践。

登录后查看全文
热门项目推荐
相关项目推荐