Public assets (public/ folder)
The public/ folder at the root of your app holds static files — images, icons, fonts, or any other assets your app needs at runtime. These files are automatically included in builds, synced during dev mode, and uploaded to the server.
Files placed in public/ are:
- Publicly accessible — once synced to the server, assets are served at a public URL. No authentication is needed to access them.
- Available in front components — use asset URLs to display images, icons, or any media inside your React components.
- Available in logic functions — reference asset URLs in emails, API responses, or any server-side logic.
- Used for marketplace metadata — the
logoUrl and screenshots fields in defineApplication() reference files from this folder (e.g., public/logo.png). These are displayed in the marketplace when your app is published.
- Auto-synced in dev mode — when you add, update, or delete a file in
public/, it is synced to the server automatically. No restart needed.
- Included in builds —
yarn twenty build bundles all public assets into the distribution output.
Accessing public assets with getPublicAssetUrl
Use the getPublicAssetUrl helper from twenty-sdk to get the full URL of a file in your public/ directory. It works in both logic functions and front components.
In a logic function:
src/logic-functions/send-invoice.ts
import { defineLogicFunction, getPublicAssetUrl } from 'twenty-sdk/define';
const handler = async (): Promise<any> => {
const logoUrl = getPublicAssetUrl('logo.png');
const invoiceUrl = getPublicAssetUrl('templates/invoice.png');
// Fetch the file content (no auth required — public endpoint)
const response = await fetch(invoiceUrl);
const buffer = await response.arrayBuffer();
return { logoUrl, size: buffer.byteLength };
};
export default defineLogicFunction({
universalIdentifier: 'a1b2c3d4-...',
name: 'send-invoice',
description: 'Sends an invoice with the app logo',
timeoutSeconds: 10,
handler,
});
In a front component:
src/front-components/company-card.tsx
import { defineFrontComponent, getPublicAssetUrl } from 'twenty-sdk/define';
export default defineFrontComponent(() => {
const logoUrl = getPublicAssetUrl('logo.png');
return <img src={logoUrl} alt="App logo" />;
});
The path argument is relative to your app’s public/ folder. Both getPublicAssetUrl('logo.png') and getPublicAssetUrl('public/logo.png') resolve to the same URL — the public/ prefix is stripped automatically if present.
Using npm packages
You can install and use any npm package in your app. Both logic functions and front components are bundled with esbuild, which inlines all dependencies into the output — no node_modules are needed at runtime.
Installing a package
Then import it in your code:
src/logic-functions/fetch-data.ts
import { defineLogicFunction } from 'twenty-sdk/define';
import axios from 'axios';
const handler = async (): Promise<any> => {
const { data } = await axios.get('https://api.example.com/data');
return { data };
};
export default defineLogicFunction({
universalIdentifier: '...',
name: 'fetch-data',
description: 'Fetches data from an external API',
timeoutSeconds: 10,
handler,
});
The same works for front components:
src/front-components/chart.tsx
import { defineFrontComponent } from 'twenty-sdk/define';
import { format } from 'date-fns';
const DateWidget = () => {
return <p>Today is {format(new Date(), 'MMMM do, yyyy')}</p>;
};
export default defineFrontComponent({
universalIdentifier: '...',
name: 'date-widget',
component: DateWidget,
});
How bundling works
构建步骤使用 esbuild 为每个逻辑函数和每个前端组件生成一个自包含文件。 All imported packages are inlined into the bundle.
Logic functions run in a Node.js environment. Node built-in modules (fs, path, crypto, http, etc.) are available and do not need to be installed.
Front components run in a Web Worker. Node built-in modules are not available — only browser APIs and npm packages that work in a browser environment.
Both environments have twenty-client-sdk/core and twenty-client-sdk/metadata available as pre-provided modules — these are not bundled but resolved at runtime by the server.
测试你的应用
该 SDK 提供可编程的 API,使你可以在测试代码中构建、部署、安装和卸载你的应用。 结合 Vitest 和类型化 API 客户端,你可以编写集成测试,在真实的 Twenty 服务器上验证你的应用端到端运行是否正常。
脚手架生成的应用已包含 Vitest。 如果你手动进行设置,请安装这些依赖:
yarn add -D vitest vite-tsconfig-paths
在应用根目录下创建一个 vitest.config.ts:
import tsconfigPaths from 'vite-tsconfig-paths';
import { defineConfig } from 'vitest/config';
export default defineConfig({
plugins: [
tsconfigPaths({
projects: ['tsconfig.spec.json'],
ignoreConfigErrors: true,
}),
],
test: {
testTimeout: 120_000,
hookTimeout: 120_000,
include: ['src/**/*.integration-test.ts'],
setupFiles: ['src/__tests__/setup-test.ts'],
env: {
TWENTY_API_URL: 'http://localhost:2020',
TWENTY_API_KEY: 'your-api-key',
},
},
});
创建一个设置文件,在测试运行前验证服务器可达:
src/__tests__/setup-test.ts
import * as fs from 'fs';
import * as os from 'os';
import * as path from 'path';
import { beforeAll } from 'vitest';
const TWENTY_API_URL = process.env.TWENTY_API_URL ?? 'http://localhost:2020';
const TEST_CONFIG_DIR = path.join(os.tmpdir(), '.twenty-sdk-test');
beforeAll(async () => {
// Verify the server is running
const response = await fetch(`${TWENTY_API_URL}/healthz`);
if (!response.ok) {
throw new Error(
`Twenty server is not reachable at ${TWENTY_API_URL}. ` +
'Start the server before running integration tests.',
);
}
// Write a temporary config for the SDK
fs.mkdirSync(TEST_CONFIG_DIR, { recursive: true });
fs.writeFileSync(
path.join(TEST_CONFIG_DIR, 'config.json'),
JSON.stringify({
remotes: {
local: {
apiUrl: process.env.TWENTY_API_URL,
apiKey: process.env.TWENTY_API_KEY,
},
},
defaultRemote: 'local',
}, null, 2),
);
});
可编程的 SDK API
子路径 twenty-sdk/cli 导出了可直接在测试代码中调用的函数:
| 函数 | 描述 |
|---|
appBuild | 构建应用,并可选地打包为 tar 包 |
appDeploy | 将 tar 包上传到服务器 |
appInstall | 在活动工作区安装该应用 |
appUninstall | 从活动工作区卸载该应用 |
每个函数都会返回一个结果对象,包含 success: boolean,以及 data 或 error 之一。
编写集成测试
下面是一个完整示例:构建、部署并安装该应用,然后验证它出现在工作区中:
src/__tests__/app-install.integration-test.ts
import { APPLICATION_UNIVERSAL_IDENTIFIER } from 'src/application-config';
import { appBuild, appDeploy, appInstall, appUninstall } from 'twenty-sdk/cli';
import { MetadataApiClient } from 'twenty-client-sdk/metadata';
import { afterAll, beforeAll, describe, expect, it } from 'vitest';
const APP_PATH = process.cwd();
describe('App installation', () => {
beforeAll(async () => {
const buildResult = await appBuild({
appPath: APP_PATH,
tarball: true,
onProgress: (message: string) => console.log(`[build] ${message}`),
});
if (!buildResult.success) {
throw new Error(`Build failed: ${buildResult.error?.message}`);
}
const deployResult = await appDeploy({
tarballPath: buildResult.data.tarballPath!,
onProgress: (message: string) => console.log(`[deploy] ${message}`),
});
if (!deployResult.success) {
throw new Error(`Deploy failed: ${deployResult.error?.message}`);
}
const installResult = await appInstall({ appPath: APP_PATH });
if (!installResult.success) {
throw new Error(`Install failed: ${installResult.error?.message}`);
}
});
afterAll(async () => {
await appUninstall({ appPath: APP_PATH });
});
it('should find the installed app in the workspace', async () => {
const metadataClient = new MetadataApiClient();
const result = await metadataClient.query({
findManyApplications: {
id: true,
name: true,
universalIdentifier: true,
},
});
const installedApp = result.findManyApplications.find(
(app: { universalIdentifier: string }) =>
app.universalIdentifier === APPLICATION_UNIVERSAL_IDENTIFIER,
);
expect(installedApp).toBeDefined();
});
});
运行测试
确保你的本地 Twenty 服务器正在运行,然后:
或者在开发期间使用监听模式:
类型检查
你也可以在不运行测试的情况下对应用进行类型检查:
这会运行 tsc --noEmit 并报告所有类型错误。
CLI 参考
除了 dev、build、add 和 typecheck 外,CLI 还提供了用于执行函数、查看日志和管理应用安装的命令。
执行函数(yarn twenty exec)
手动运行逻辑函数,而无需通过 HTTP、定时任务或数据库事件来触发:
# Execute by function name
yarn twenty exec -n create-new-post-card
# Execute by universalIdentifier
yarn twenty exec -u e56d363b-0bdc-4d8a-a393-6f0d1c75bdcf
# Pass a JSON payload
yarn twenty exec -n create-new-post-card -p '{"name": "Hello"}'
# Execute the post-install function
yarn twenty exec --postInstall
查看函数日志(yarn twenty logs)
实时流式查看你的应用逻辑函数的执行日志:
# Stream all function logs
yarn twenty logs
# Filter by function name
yarn twenty logs -n create-new-post-card
# Filter by universalIdentifier
yarn twenty logs -u e56d363b-0bdc-4d8a-a393-6f0d1c75bdcf
这与 yarn twenty server logs 不同,后者显示的是 Docker 容器日志。 yarn twenty logs 会显示来自 Twenty 服务器的应用函数执行日志。
卸载应用(yarn twenty uninstall)
将你的应用从活动工作区中移除:
yarn twenty uninstall
# Skip the confirmation prompt
yarn twenty uninstall --yes
管理远程
“远程”是指你的应用连接到的 Twenty 服务器。 在设置期间,脚手架工具会为你自动创建一个。 你可以随时添加更多远程或在它们之间切换。
# Add a new remote (opens a browser for OAuth login)
yarn twenty remote add
# Connect to a local Twenty server (auto-detects port 2020 or 3000)
yarn twenty remote add --local
# Add a remote non-interactively (useful for CI)
yarn twenty remote add --api-url https://your-twenty-server.com --api-key $TWENTY_API_KEY --as my-remote
# List all configured remotes
yarn twenty remote list
# Switch the active remote
yarn twenty remote switch <name>
你的凭据存储在 ~/.twenty/config.json 中。
使用 GitHub Actions 进行 CI
脚手架工具会在 .github/workflows/ci.yml 生成一个开箱即用的 GitHub Actions 工作流。 它会在每次向 main 推送以及拉取请求上自动运行你的集成测试。
工作流:
- 检出你的代码
- 使用
twentyhq/twenty/.github/actions/spawn-twenty-docker-image 动作启动一个临时的 Twenty 服务器
- 使用
yarn install --immutable 安装依赖
- 运行
yarn test,并从该动作的输出中注入 TWENTY_API_URL 和 TWENTY_API_KEY
name: CI
on:
push:
branches:
- main
pull_request: {}
env:
TWENTY_VERSION: latest
jobs:
test:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Spawn Twenty instance
id: twenty
uses: twentyhq/twenty/.github/actions/spawn-twenty-docker-image@main
with:
twenty-version: ${{ env.TWENTY_VERSION }}
github-token: ${{ secrets.GITHUB_TOKEN }}
- name: Enable Corepack
run: corepack enable
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version-file: '.nvmrc'
cache: 'yarn'
- name: Install dependencies
run: yarn install --immutable
- name: Run integration tests
run: yarn test
env:
TWENTY_API_URL: ${{ steps.twenty.outputs.server-url }}
TWENTY_API_KEY: ${{ steps.twenty.outputs.access-token }}
你无需配置任何机密——spawn-twenty-docker-image 动作会在运行器中直接启动一个临时的 Twenty 服务器,并输出连接详情。 GitHub 会自动提供 GITHUB_TOKEN 机密。
若要固定为特定的 Twenty 版本而不是 latest,请在工作流顶部修改 TWENTY_VERSION 环境变量。