Improve getting started doc (#19138)

- improves
`packages/twenty-docs/developers/extend/apps/getting-started.mdx`

---------

Co-authored-by: cubic-dev-ai[bot] <191113872+cubic-dev-ai[bot]@users.noreply.github.com>
This commit is contained in:
martmull
2026-04-01 22:39:44 +02:00
committed by GitHub
parent 4cc3deb937
commit 16e3e38b79
50 changed files with 2001 additions and 2630 deletions
+1 -1
View File
@@ -156,7 +156,7 @@ jobs:
SEED_API_KEY: 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIyMDIwMjAyMC1lNmI1LTQ2ODAtOGEzMi1iODIwOTczNzE1NmIiLCJ1c2VySWQiOiIyMDIwMjAyMC1lNmI1LTQ2ODAtOGEzMi1iODIwOTczNzE1NmIiLCJ3b3Jrc3BhY2VJZCI6IjIwMjAyMDIwLTFjMjUtNGQwMi1iZjI1LTZhZWNjZjdlYTQxOSIsIndvcmtzcGFjZU1lbWJlcklkIjoiMjAyMDIwMjAtNDYzZi00MzViLTgyOGMtMTA3ZTAwN2EyNzExIiwidXNlcldvcmtzcGFjZUlkIjoiMjAyMDIwMjAtMWU3Yy00M2Q5LWE1ZGItNjg1YjUwNjlkODE2IiwidHlwZSI6IkFDQ0VTUyIsImF1dGhQcm92aWRlciI6InBhc3N3b3JkIiwiaWF0IjoxNzUxMjgxNzA0LCJleHAiOjIwNjY4NTc3MDR9.HMGqCsVlOAPVUBhKSGlD1X86VoHKt4LIUtET3CGIdik'
run: |
cd /tmp/e2e-test-workspace/test-app
npx --no-install twenty remote add --token $SEED_API_KEY --url http://localhost:3000
npx --no-install twenty remote add --api-key $SEED_API_KEY --api-url http://localhost:3000
- name: Deploy scaffolded app
run: |
@@ -121,7 +121,7 @@ jobs:
- name: Start container
run: |
docker run -d --name twenty-app-dev \
-p 3000:3000 \
-p 2020:2020 \
twenty-app-dev-ci
docker logs twenty-app-dev -f &
- name: Wait for server health
@@ -129,10 +129,10 @@ jobs:
echo "Waiting for twenty-app-dev to become healthy..."
count=0
while true; do
status=$(curl -s -o /dev/null -w '%{http_code}' http://localhost:3000/healthz 2>/dev/null || echo "000")
status=$(curl -s -o /dev/null -w '%{http_code}' http://localhost:2020/healthz 2>/dev/null || echo "000")
if [ "$status" = "200" ]; then
echo "Server is healthy!"
curl -s http://localhost:3000/healthz
curl -s http://localhost:2020/healthz
break
fi
+23 -138
View File
@@ -12,164 +12,49 @@
</div>
Create Twenty App is the official scaffolding CLI for building apps on top of [Twenty CRM](https://twenty.com). It sets up a readytorun project that works seamlessly with the [twenty-sdk](https://www.npmjs.com/package/twenty-sdk).
- Zeroconfig project bootstrap
- Preconfigured scripts for auth, dev mode (watch & sync), uninstall, and function management
- Strong TypeScript support and typed client generation
## Documentation
See Twenty application documentation https://docs.twenty.com/developers/extend/capabilities/apps
## Prerequisites
- Node.js 24+ (recommended) and Yarn 4
- Docker (for the local Twenty dev server)
The official scaffolding CLI for building apps on top of [Twenty CRM](https://twenty.com). Sets up a ready-to-run project with [twenty-sdk](https://www.npmjs.com/package/twenty-sdk).
## Quick start
```bash
# Scaffold a new app — the CLI will offer to start a local Twenty server
npx create-twenty-app@latest my-twenty-app
cd my-twenty-app
# The scaffolder can automatically:
# 1. Start a local Twenty server (Docker)
# 2. Open the browser to log in (tim@apple.dev / tim@apple.dev)
# 3. Authenticate your app via OAuth
# Or do it manually:
yarn twenty server start # Start local Twenty server
yarn twenty remote add http://localhost:2020 --as local # Authenticate via OAuth
# Start dev mode: watches, builds, and syncs local changes to your workspace
# (also auto-generates typed CoreApiClient — MetadataApiClient ships pre-built — both available via `twenty-client-sdk`)
yarn twenty dev
# Watch your application's function logs
yarn twenty logs
# Execute a function with a JSON payload
yarn twenty exec -n my-function -p '{"key": "value"}'
# Execute the pre-install function
yarn twenty exec --preInstall
# Execute the post-install function
yarn twenty exec --postInstall
# Build the app for distribution
yarn twenty build
# Publish the app to npm or directly to a Twenty server
yarn twenty publish
# Uninstall the application from the current workspace
yarn twenty uninstall
```
The scaffolder will:
1. Create a new project with TypeScript, linting, and a preconfigured `twenty` CLI
2. Optionally start a local Twenty server (Docker)
3. Open the browser for OAuth authentication
4. Scaffold example entities and an integration test
## Scaffolding modes
Control which example files are included when creating a new app:
| Flag | Behavior |
| -------------- | -------------------------------------------------------------------------------------------------------------- |
| `--minimal` | **(default)** Creates only core files (`application-config.ts`, `default-role.ts`, pre/post-install functions) |
| `--exhaustive` | Creates all example entities |
| Flag | Behavior |
| ------------------ | ----------------------------------------------------------------------- |
| `-e, --exhaustive` | **(default)** Creates all example files |
| `-m, --minimal` | Creates only core files (`application-config.ts` and `default-role.ts`) |
Other flags:
```bash
# Default: all examples included
npx create-twenty-app@latest my-app
- `--name <name>` — set the app name (skips the prompt)
- `--display-name <displayName>` — set the display name (skips the prompt)
- `--description <description>` — set the description (skips the prompt)
- `--skip-local-instance` — skip the local server setup prompt
# Minimal: only core files
npx create-twenty-app@latest my-app -m
```
## Documentation
## What gets scaffolded
Full documentation is available at **[docs.twenty.com/developers/extend/apps](https://docs.twenty.com/developers/extend/apps/getting-started)**:
**Core files (always created):**
- `application-config.ts` — Application metadata configuration
- `roles/default-role.ts` — Default role for logic functions
- `logic-functions/pre-install.ts` — Pre-install logic function (runs before app installation)
- `logic-functions/post-install.ts` — Post-install logic function (runs after app installation)
- TypeScript configuration, Oxlint, package.json, .gitignore
- A prewired `twenty` script that delegates to the `twenty` CLI from twenty-sdk
**Example files (controlled by scaffolding mode):**
- `objects/example-object.ts` — Example custom object with a text field
- `fields/example-field.ts` — Example standalone field extending the example object
- `logic-functions/hello-world.ts` — Example logic function with HTTP trigger
- `front-components/hello-world.tsx` — Example front component
- `views/example-view.ts` — Example saved view for the example object
- `navigation-menu-items/example-navigation-menu-item.ts` — Example sidebar navigation link
- `skills/example-skill.ts` — Example AI agent skill definition
- `__tests__/app-install.integration-test.ts` — Integration test that builds, installs, and verifies the app (includes `vitest.config.ts`, `tsconfig.spec.json`, and a setup file)
## Local server
The scaffolder can start a local Twenty dev server for you (all-in-one Docker image with PostgreSQL, Redis, server, and worker on port 2020). These commands only apply to the Docker-based dev server — they do not manage a Twenty instance started from source (e.g. `npx nx start twenty-server` on port 3000). You can also manage it manually:
```bash
yarn twenty server start # Start (pulls image if needed)
yarn twenty server status # Check if it's healthy
yarn twenty server logs # Stream logs
yarn twenty server stop # Stop (data is preserved)
yarn twenty server reset # Wipe all data and start fresh
```
The server is pre-seeded with a workspace and user (`tim@apple.dev` / `tim@apple.dev`).
## Next steps
- Run `yarn twenty help` to see all available commands.
- Use `yarn twenty remote add <url>` to authenticate with your Twenty workspace via OAuth.
- Explore the generated project and add your first entity with `yarn twenty add` (logic functions, front components, objects, roles, views, navigation menu items, skills).
- Use `yarn twenty dev` while you iterate — it watches, builds, and syncs changes to your workspace in real time.
- `CoreApiClient` is auto-generated by `yarn twenty dev`. `MetadataApiClient` (for workspace configuration and file uploads via `/metadata`) ships pre-built with the SDK. Both are available via `import { CoreApiClient } from 'twenty-client-sdk/core'` and `import { MetadataApiClient } from 'twenty-client-sdk/metadata'`.
## Build and publish your application
Once your app is ready, build and publish it using the CLI:
```bash
# Build the app (output goes to .twenty/output/)
yarn twenty build
# Build and create a tarball (.tgz) for distribution
yarn twenty build --tarball
# Publish to npm (requires npm login)
yarn twenty publish
# Publish with a dist-tag (e.g. beta, next)
yarn twenty publish --tag beta
# Deploy directly to a Twenty server (builds, uploads, and installs in one step)
yarn twenty deploy
```
### Publish to the Twenty marketplace
You can also contribute your application to the curated marketplace:
```bash
git clone https://github.com/twentyhq/twenty.git
cd twenty
git checkout -b feature/my-awesome-app
```
- Copy your app folder into `twenty/packages/twenty-apps`.
- Commit your changes and open a pull request on https://github.com/twentyhq/twenty
Our team reviews contributions for quality, security, and reusability before merging.
- [Getting Started](https://docs.twenty.com/developers/extend/apps/getting-started) — step-by-step setup, project structure, server management, CI
- [Building Apps](https://docs.twenty.com/developers/extend/apps/building) — entity definitions, API clients, testing
- [Publishing](https://docs.twenty.com/developers/extend/apps/publishing) — deploy, npm publish, marketplace
## Troubleshooting
- Server not starting: check Docker is running (`docker info`), then try `yarn twenty server logs`.
- Auth not working: make sure you're logged in to Twenty in the browser first, then run `yarn twenty remote add <url>`.
- Auth not working: make sure you are logged in to Twenty in the browser, then run `yarn twenty remote add`.
- Types not generated: ensure `yarn twenty dev` is running — it auto-generates the typed client.
## Contributing
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "create-twenty-app",
"version": "0.8.0-canary.7",
"version": "0.8.0-canary.8",
"description": "Command-line interface to create Twenty application",
"main": "dist/cli.cjs",
"bin": "dist/cli.cjs",
+5 -3
View File
@@ -13,10 +13,10 @@ const program = new Command(packageJson.name)
'Output the current version of create-twenty-app.',
)
.argument('[directory]')
.option('-e, --exhaustive', 'Create all example entities (default)')
.option('-e, --exhaustive', 'Create all example entities')
.option(
'-m, --minimal',
'Create only core entities (application-config and default-role)',
'Create only core entities (application-config and default-role) (default)',
)
.option('-n, --name <name>', 'Application name (skips prompt)')
.option(
@@ -69,7 +69,9 @@ const program = new Command(packageJson.name)
process.exit(1);
}
const mode: ScaffoldingMode = options?.minimal ? 'minimal' : 'exhaustive';
const mode: ScaffoldingMode = options?.exhaustive
? 'exhaustive'
: 'minimal';
await new CreateAppCommand().execute({
directory,
@@ -39,7 +39,7 @@ export class CreateAppCommand {
try {
const exampleOptions = this.resolveExampleOptions(
options.mode ?? 'exhaustive',
options.mode ?? 'minimal',
);
await this.validateDirectory(appDirectory);
@@ -163,7 +163,7 @@ export class CreateAppCommand {
includeExampleNavigationMenuItem: false,
includeExampleSkill: false,
includeExampleAgent: false,
includeExampleIntegrationTest: false,
includeExampleIntegrationTest: true,
};
}
@@ -0,0 +1,19 @@
{
"$schema": "./node_modules/oxlint/configuration_schema.json",
"plugins": ["typescript"],
"categories": {
"correctness": "off"
},
"ignorePatterns": ["node_modules", "dist"],
"rules": {
"no-unused-vars": "off",
"typescript/no-unused-vars": [
"warn",
{
"argsIgnorePattern": "^_"
}
],
"typescript/no-explicit-any": "off"
}
}
@@ -0,0 +1,19 @@
{
"$schema": "./node_modules/oxlint/configuration_schema.json",
"plugins": ["typescript"],
"categories": {
"correctness": "off"
},
"ignorePatterns": ["node_modules", "dist"],
"rules": {
"no-unused-vars": "off",
"typescript/no-unused-vars": [
"warn",
{
"argsIgnorePattern": "^_"
}
],
"typescript/no-explicit-any": "off"
}
}
@@ -9,17 +9,7 @@
},
"packageManager": "yarn@4.9.2",
"scripts": {
"remote:add": "twenty remote add --local",
"remote:status": "twenty remote status",
"remote:switch": "twenty remote switch",
"remote:list": "twenty remote list",
"remote:remove": "twenty remote remove",
"dev": "twenty dev",
"add": "twenty add",
"logs": "twenty logs",
"exec": "twenty exec",
"uninstall": "twenty uninstall",
"help": "twenty help",
"twenty": "twenty",
"lint": "oxlint -c .oxlintrc.json .",
"lint:fix": "oxlint --fix -c .oxlintrc.json ."
},
@@ -0,0 +1,19 @@
{
"$schema": "./node_modules/oxlint/configuration_schema.json",
"plugins": ["typescript"],
"categories": {
"correctness": "off"
},
"ignorePatterns": ["node_modules", "dist"],
"rules": {
"no-unused-vars": "off",
"typescript/no-unused-vars": [
"warn",
{
"argsIgnorePattern": "^_"
}
],
"typescript/no-explicit-any": "off"
}
}
@@ -9,17 +9,7 @@
},
"packageManager": "yarn@4.9.2",
"scripts": {
"remote:add": "twenty remote add --local",
"remote:status": "twenty remote status",
"remote:switch": "twenty remote switch",
"remote:list": "twenty remote list",
"remote:remove": "twenty remote remove",
"dev": "twenty dev",
"add": "twenty add",
"logs": "twenty logs",
"exec": "twenty exec",
"uninstall": "twenty uninstall",
"help": "twenty help",
"twenty": "twenty",
"lint": "oxlint -c .oxlintrc.json .",
"lint:fix": "oxlint --fix -c .oxlintrc.json ."
},
@@ -0,0 +1,19 @@
{
"$schema": "./node_modules/oxlint/configuration_schema.json",
"plugins": ["typescript"],
"categories": {
"correctness": "off"
},
"ignorePatterns": ["node_modules", "dist"],
"rules": {
"no-unused-vars": "off",
"typescript/no-unused-vars": [
"warn",
{
"argsIgnorePattern": "^_"
}
],
"typescript/no-explicit-any": "off"
}
}
@@ -9,7 +9,9 @@
},
"packageManager": "yarn@4.9.2",
"scripts": {
"twenty": "twenty"
"twenty": "twenty",
"lint": "oxlint -c .oxlintrc.json .",
"lint:fix": "oxlint --fix -c .oxlintrc.json ."
},
"dependencies": {
"twenty-sdk": "latest",
+2 -2
View File
@@ -5,7 +5,7 @@ This is a [Twenty](https://twenty.com) application project bootstrapped with [`c
First, authenticate to your workspace:
```bash
yarn twenty remote add http://localhost:2020 --as local
yarn twenty remote add --api-url http://localhost:2020 --as local
```
Then, start development mode to sync your app and watch for changes:
@@ -22,7 +22,7 @@ Run `yarn twenty help` to list all available commands. Common commands:
```bash
# Remotes & Authentication
yarn twenty remote add http://localhost:2020 --as local # Authenticate with Twenty
yarn twenty remote add --api-url http://localhost:2020 --as local # Authenticate with Twenty
yarn twenty remote status # Check auth status
yarn twenty remote switch # Switch default remote
yarn twenty remote list # List all configured remotes
@@ -5,7 +5,7 @@ This is a [Twenty](https://twenty.com) application project bootstrapped with [`c
First, authenticate to your workspace:
```bash
yarn twenty remote add http://localhost:2020 --as local
yarn twenty remote add --api-url http://localhost:2020 --as local
```
Then, start development mode to sync your app and watch for changes:
@@ -22,7 +22,7 @@ Run `yarn twenty help` to list all available commands. Common commands:
```bash
# Remotes & Authentication
yarn twenty remote add http://localhost:2020 --as local # Authenticate with Twenty
yarn twenty remote add --api-url http://localhost:2020 --as local # Authenticate with Twenty
yarn twenty remote status # Check auth status
yarn twenty remote switch # Switch default remote
yarn twenty remote list # List all configured remotes
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "twenty-client-sdk",
"version": "0.8.0-canary.7",
"version": "0.8.0-canary.8",
"sideEffects": false,
"license": "AGPL-3.0",
"scripts": {
@@ -0,0 +1 @@
/bin/sh /etc/s6-overlay/scripts/register-crons.sh
@@ -1,10 +1,13 @@
#!/bin/sh
set -e
step_start() { echo "==> START $1"; }
step_done() { echo "==> DONE"; }
# Wait for PostgreSQL to be ready (timeout after 60s)
echo "Waiting for PostgreSQL..."
step_start "Waiting for PostgreSQL"
TRIES=0
until su-exec postgres pg_isready -h localhost; do
until su-exec postgres pg_isready -h localhost > /dev/null 2>&1; do
TRIES=$((TRIES + 1))
if [ "$TRIES" -ge 120 ]; then
echo "ERROR: PostgreSQL did not become ready within 60s"
@@ -12,7 +15,7 @@ until su-exec postgres pg_isready -h localhost; do
fi
sleep 0.5
done
echo "PostgreSQL is ready."
step_done
# Create role if it doesn't exist
su-exec postgres psql -h localhost -tc \
@@ -31,26 +34,36 @@ has_schema=$(PGPASSWORD=twenty psql -h localhost -U twenty -d default -tAc \
"SELECT EXISTS (SELECT 1 FROM information_schema.schemata WHERE schema_name = 'core')")
if [ "$has_schema" = "f" ]; then
echo "Database appears to be empty, running initial setup..."
step_start "Running initial database setup"
NODE_OPTIONS="--max-old-space-size=1500" node ./dist/database/scripts/setup-db.js
step_done
fi
# Always run migrations (idempotent — skips already-applied ones)
step_start "Running migrations"
yarn database:migrate:prod --force
step_done
step_start "Flushing cache"
yarn command:prod cache:flush
step_done
step_start "Running upgrade"
yarn command:prod upgrade
step_done
step_start "Flushing cache"
yarn command:prod cache:flush
step_done
# Only seed on first boot — check if the dev workspace already exists
has_workspace=$(PGPASSWORD=twenty psql -h localhost -U twenty -d default -tAc \
"SELECT EXISTS (SELECT 1 FROM core.workspace WHERE id = '20202020-1c25-4d02-bf25-6aeccf7ea419')")
if [ "$has_workspace" = "f" ]; then
echo "Seeding app dev data..."
step_start "Seeding workspace data"
yarn command:prod workspace:seed:dev --light || true
else
echo "Dev workspace already seeded, skipping."
step_done
fi
echo "Database initialization complete."
echo "==> START Database ready"
echo "==> DONE"
@@ -0,0 +1,9 @@
#!/bin/sh
set -e
echo "==> START Registering cron jobs"
cd /app/packages/twenty-server
yarn command:prod cron:register:all --dev-mode
echo "==> DONE"
+5 -5
View File
@@ -195,9 +195,9 @@ RUN find /app/packages/twenty-server/dist -name '*.js.map' -delete
# s6 service definitions
COPY packages/twenty-docker/twenty-app-dev/rootfs/ /
RUN mkdir -p /data/postgres /data/redis /app/.local-storage \
RUN mkdir -p /data/postgres /data/redis /app/packages/twenty-server/.local-storage \
&& chown -R postgres:postgres /data/postgres \
&& chown 1000:1000 /data/redis /app/.local-storage
&& chown 1000:1000 /data/redis /app/packages/twenty-server/.local-storage
ARG REACT_APP_SERVER_BASE_URL
ARG APP_VERSION=0.0.0
@@ -211,14 +211,14 @@ ENV PG_DATABASE_URL=postgres://twenty:twenty@localhost:5432/default \
REACT_APP_SERVER_BASE_URL=$REACT_APP_SERVER_BASE_URL \
APP_VERSION=$APP_VERSION \
NODE_ENV=development \
NODE_PORT=3000 \
NODE_PORT=2020 \
DISABLE_DB_MIGRATIONS=true \
DISABLE_CRON_JOBS_REGISTRATION=true \
IS_BILLING_ENABLED=false \
SIGN_IN_PREFILLED=true
EXPOSE 3000
VOLUME ["/data/postgres", "/app/.local-storage"]
EXPOSE 2020
VOLUME ["/data/postgres", "/app/packages/twenty-server/.local-storage"]
LABEL org.opencontainers.image.source=https://github.com/twentyhq/twenty
LABEL org.opencontainers.image.description="All-in-one Twenty image for local development and SDK usage. Includes PostgreSQL, Redis, server, and worker."
File diff suppressed because it is too large Load Diff
@@ -4,72 +4,142 @@ description: Create your first Twenty app in minutes.
---
<Warning>
Apps are currently in alpha testing. The feature is functional but still evolving.
Apps are currently in alpha. The feature works but is still evolving.
</Warning>
Apps let you extend Twenty with custom objects, fields, logic functions, AI skills, and UI components — all managed as code.
**What you can build:**
- Custom objects, fields, views, and navigation items to shape your data model
- Logic functions triggered by HTTP routes, cron schedules, or database events
- Front components that render directly inside Twenty's UI
- Skills that extend Twenty's AI agents
- Deploy an app across multiple workspaces
## Prerequisites
- Node.js 24+
- Yarn 4
- Docker (or a running local Twenty instance)
Before you begin, make sure the following is installed on your machine:
## Getting Started
- **Node.js 24+** — [Download here](https://nodejs.org/)
- **Yarn 4** — Comes with Node.js via Corepack. Enable it by running `corepack enable`
- **Docker** — [Download here](https://www.docker.com/products/docker-desktop/). Required to run a local Twenty instance. Not needed if you already have a Twenty server running.
Create a new app using the official scaffolder, then authenticate and start developing:
## Step 1: Scaffold your app
Open a terminal and run:
```bash filename="Terminal"
# Scaffold a new app (includes all examples by default)
npx create-twenty-app@latest my-twenty-app
```
> Use `--minimal` option to scaffold a minimal installation
You will be prompted to enter a name and a description for your app. Press **Enter** to accept the defaults.
From here you can:
This creates a new folder called `my-twenty-app` with everything you need.
<Note>
The scaffolder supports these flags:
- `--minimal` — scaffold only the essential files, no examples (default)
- `--exhaustive` — scaffold all example entities
- `--name <name>` — set the app name (skips the prompt)
- `--display-name <displayName>` — set the display name (skips the prompt)
- `--description <description>` — set the description (skips the prompt)
- `--skip-local-instance` — skip the local server setup prompt
</Note>
## Step 2: Set up a local Twenty instance
The scaffolder will ask:
> **Would you like to set up a local Twenty instance?**
- **Type `yes`** (recommended) — This pulls the `twenty-app-dev` Docker image and starts a local Twenty server on port `2020`. Make sure Docker is running before you continue.
- **Type `no`** — Choose this if you already have a Twenty server running locally.
<div style={{textAlign: 'center'}}>
<img src="/images/docs/developers/extends/apps/start-instance.png" alt="Should start local instance?" />
</div>
## Step 3: Sign in to your workspace
Next, a browser window will open with the Twenty login page. Sign in with the pre-seeded demo account:
- **Email:** `tim@apple.dev`
- **Password:** `tim@apple.dev`
<div style={{textAlign: 'center'}}>
<img src="/images/docs/developers/extends/apps/login.png" alt="Twenty login screen" />
</div>
## Step 4: Authorize the app
After you sign in, you will see an authorization screen. This lets your app interact with your workspace.
Click **Authorize** to continue.
<div style={{textAlign: 'center'}}>
<img src="/images/docs/developers/extends/apps/authorize.png" alt="Twenty CLI authorization screen" />
</div>
Once authorized, your terminal will confirm that everything is set up.
<div style={{textAlign: 'center'}}>
<img src="/images/docs/developers/extends/apps/scaffolded.png" alt="App scaffolded successfully" />
</div>
## Step 5: Start developing
Go into your new app folder and start the development server:
```bash filename="Terminal"
# Add a new entity to your application (guided)
yarn twenty add
# Watch your application's function logs
yarn twenty function:logs
# Execute a function by name
yarn twenty function:execute -n my-function -p '{"name": "test"}'
# Execute the pre-install function
yarn twenty function:execute --preInstall
# Execute the post-install function
yarn twenty function:execute --postInstall
# Uninstall the application from the current workspace
yarn twenty uninstall
# Display commands' help
yarn twenty help
cd my-twenty-app
yarn twenty dev
```
See also: the CLI reference pages for [create-twenty-app](https://www.npmjs.com/package/create-twenty-app) and [twenty-sdk CLI](https://www.npmjs.com/package/twenty-sdk).
This watches your source files, rebuilds on every change, and syncs your app to the local Twenty server automatically. You should see a live status panel in your terminal.
## Project structure (scaffolded)
For more detailed output (build logs, sync requests, error traces), use the `--verbose` flag:
When you run `npx create-twenty-app@latest my-twenty-app`, the scaffolder:
```bash filename="Terminal"
yarn twenty dev --verbose
```
- Copies a minimal base application into `my-twenty-app/`
- Adds a local `twenty-sdk` dependency and Yarn 4 configuration
- Creates config files and scripts wired to the `twenty` CLI
- Generates core files (application config, default function role, pre-install and post-install functions) plus example files based on the scaffolding mode
<Warning>
Dev mode is only available on Twenty instances running in development (`NODE_ENV=development`). Production instances reject dev sync requests. Use `yarn twenty deploy` to deploy to production servers — see [Publishing Apps](/developers/extend/apps/publishing) for details.
</Warning>
A freshly scaffolded app with the default `--exhaustive` mode looks like this:
<div style={{textAlign: 'center'}}>
<img src="/images/docs/developers/extends/apps/dev.jpg" alt="Dev mode terminal output" />
</div>
## Step 6: See your app in Twenty
Open [http://localhost:2020/settings/applications#developer](http://localhost:2020/settings/applications#developer) in your browser. Navigate to **Settings > Apps** and select the **Developer** tab. You should see your app listed under **Your Apps**:
<div style={{textAlign: 'center'}}>
<img src="/images/docs/developers/extends/apps/app-in-ui-1.png" alt="Your Apps list showing My twenty app" />
</div>
Click on **My twenty app** to open its **application registration**. A registration is a server-level record that describes your app — its name, unique identifier, OAuth credentials, and source (local, npm, or tarball). It lives on the server, not inside any specific workspace. When you install an app into a workspace, Twenty creates a workspace-scoped **application** that points back to this registration. One registration can be installed across multiple workspaces on the same server.
<div style={{textAlign: 'center'}}>
<img src="/images/docs/developers/extends/apps/app-in-ui-2.png" alt="Application registration details" />
</div>
Click **View installed app** to see the installed app. The **About** tab shows the current version and management options:
<div style={{textAlign: 'center'}}>
<img src="/images/docs/developers/extends/apps/app-in-ui-3.png" alt="Installed app — About tab" />
</div>
Switch to the **Content** tab to see everything your app provides — objects, fields, logic functions, and agents:
<div style={{textAlign: 'center'}}>
<img src="/images/docs/developers/extends/apps/app-in-ui-4.png" alt="Installed app — Content tab" />
</div>
You are all set! Edit any file in `src/` and the changes will be picked up automatically.
Head over to [Building Apps](/developers/extend/apps/building) for a detailed guide on creating objects, logic functions, front components, skills, and more.
---
## Project structure
The scaffolder generates the following file structure (shown with `--exhaustive` mode, which includes examples for every entity type):
```text filename="my-twenty-app/"
my-twenty-app/
@@ -82,123 +152,238 @@ my-twenty-app/
install-state.gz
.oxlintrc.json
tsconfig.json
tsconfig.spec.json # TypeScript config for tests
vitest.config.ts # Vitest test runner configuration
LLMS.md
README.md
public/ # Public assets folder (images, fonts, etc.)
.github/
└── workflows/
└── ci.yml # GitHub Actions CI workflow
public/ # Public assets (images, fonts, etc.)
src/
├── application-config.ts # Required - main application configuration
├── application-config.ts # Required main application configuration
├── __tests__/
│ ├── setup-test.ts # Test setup (server health check, config)
│ └── app-install.integration-test.ts # Example integration test
├── roles/
│ └── default-role.ts # Default role for logic functions
│ └── default-role.ts # Default role for logic functions
├── objects/
│ └── example-object.ts # Example custom object definition
│ └── example-object.ts # Example custom object definition
├── fields/
│ └── example-field.ts # Example standalone field definition
│ └── example-field.ts # Example standalone field definition
├── logic-functions/
│ ├── hello-world.ts # Example logic function
│ ├── pre-install.ts # Pre-install logic function
── post-install.ts # Post-install logic function
│ ├── hello-world.ts # Example logic function
│ ├── create-hello-world-company.ts # Example logic function using CoreApiClient
── pre-install.ts # Runs before installation
│ └── post-install.ts # Runs after installation
├── front-components/
│ └── hello-world.tsx # Example front component
│ └── hello-world.tsx # Example front component
├── page-layouts/
│ └── example-record-page-layout.ts # Example page layout with front component
├── views/
│ └── example-view.ts # Example saved view definition
│ └── example-view.ts # Example saved view definition
├── navigation-menu-items/
│ └── example-navigation-menu-item.ts # Example sidebar navigation link
── skills/
└── example-skill.ts # Example AI agent skill definition
── skills/
└── example-skill.ts # Example AI agent skill definition
└── agents/
└── example-agent.ts # Example AI agent definition
```
With `--minimal`, only the core files are created (`application-config.ts`, `roles/default-role.ts`, `logic-functions/pre-install.ts`, and `logic-functions/post-install.ts`).
By default (`--minimal`), only the core files are created: `application-config.ts`, `roles/default-role.ts`, `logic-functions/pre-install.ts`, and `logic-functions/post-install.ts`. Use `--exhaustive` to include all the example files shown above.
At a high level:
### Key files
- **package.json**: Declares the app name, version, engines (Node 24+, Yarn 4), and adds `twenty-sdk` plus a `twenty` script that delegates to the local `twenty` CLI. Run `yarn twenty help` to list all available commands.
- **.gitignore**: Ignores common artifacts such as `node_modules`, `.yarn`, `.twenty/`, `dist/`, `build/`, coverage folders, log files, and `.env*` files.
- **yarn.lock**, **.yarnrc.yml**, **.yarn/**: Lock and configure the Yarn 4 toolchain used by the project.
- **.nvmrc**: Pins the Node.js version expected by the project.
- **.oxlintrc.json** and **tsconfig.json**: Provide linting and TypeScript configuration for your app's TypeScript sources.
- **README.md**: A short README in the app root with basic instructions.
- **public/**: A folder for storing public assets (images, fonts, static files) that will be served with your application. Files placed here are uploaded during sync and accessible at runtime.
- **src/**: The main place where you define your application-as-code
| File / Folder | Purpose |
|---|---|
| `package.json` | Declares your app name, version, and dependencies. Includes a `twenty` script so you can run `yarn twenty help` to see all commands. |
| `src/application-config.ts` | **Required.** The main configuration file for your app. |
| `src/roles/` | Defines roles that control what your logic functions can access. |
| `src/logic-functions/` | Server-side functions triggered by routes, cron schedules, or database events. |
| `src/front-components/` | React components that render inside Twenty's UI. |
| `src/objects/` | Custom object definitions to extend your data model. |
| `src/fields/` | Custom fields added to existing objects. |
| `src/views/` | Saved view configurations. |
| `src/navigation-menu-items/` | Custom links in the sidebar navigation. |
| `src/skills/` | Skills that extend Twenty's AI agents. |
| `src/agents/` | AI agents with custom prompts. |
| `src/page-layouts/` | Custom page layouts for record views. |
| `src/__tests__/` | Integration tests (setup + example test). |
| `public/` | Static assets (images, fonts) served with your app. |
### Entity detection
## Managing remotes
The SDK detects entities by parsing your TypeScript files for **`export default define<Entity>({...})`** calls. Each entity type has a corresponding helper function exported from `twenty-sdk`:
| Helper function | Entity type |
|-----------------|-------------|
| `defineObject` | Custom object definitions |
| `defineLogicFunction` | Logic function definitions |
| `definePreInstallLogicFunction` | Pre-install logic function (runs before installation) |
| `definePostInstallLogicFunction` | Post-install logic function (runs after installation) |
| `defineFrontComponent` | Front component definitions |
| `defineRole` | Role definitions |
| `defineField` | Field extensions for existing objects |
| `defineView` | Saved view definitions |
| `defineNavigationMenuItem` | Navigation menu item definitions |
| `defineSkill` | AI agent skill definitions |
<Note>
**File naming is flexible.** Entity detection is AST-based — the SDK scans your source files for the `export default define<Entity>({...})` pattern. You can organize your files and folders however you like. Grouping by entity type (e.g., `logic-functions/`, `roles/`) is just a convention for code organization, not a requirement.
</Note>
Example of a detected entity:
```typescript
// This file can be named anything and placed anywhere in src/
import { defineObject, FieldType } from 'twenty-sdk';
export default defineObject({
universalIdentifier: '...',
nameSingular: 'postCard',
// ... rest of config
});
```
Later commands will add more files and folders:
- `yarn twenty dev` will auto-generate the typed `CoreApiClient` (for workspace data via `/graphql`) into `node_modules/twenty-client-sdk/`. The `MetadataApiClient` (for workspace configuration and file uploads via `/metadata`) ships pre-built and is available immediately. Import them from `twenty-client-sdk/core` and `twenty-client-sdk/metadata` respectively.
- `yarn twenty add` will add entity definition files under `src/` for your custom objects, functions, front components, roles, skills, and more.
## Authentication
The first time you run `yarn twenty auth:login`, you'll be prompted for:
- API URL (defaults to http://localhost:3000 or your current workspace profile)
- API key
Your credentials are stored per-user in `~/.twenty/config.json`. You can maintain multiple profiles and switch between them.
### Managing workspaces
A **remote** is a Twenty server that your app connects to. During setup, the scaffolder creates one for you automatically. You can add more remotes or switch between them at any time.
```bash filename="Terminal"
# Login interactively (recommended)
yarn twenty auth:login
# Add a new remote (opens a browser for OAuth login)
yarn twenty remote add
# Login to a specific workspace profile
yarn twenty auth:login --workspace my-custom-workspace
# Connect to a local Twenty server (auto-detects port 2020 or 3000)
yarn twenty remote add --local
# List all configured workspaces
yarn twenty auth:list
# 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
# Switch the default workspace (interactive)
yarn twenty auth:switch
# List all configured remotes
yarn twenty remote list
# Switch to a specific workspace
yarn twenty auth:switch production
# Check current authentication status
yarn twenty auth:status
# Switch the active remote
yarn twenty remote switch <name>
```
Once you've switched workspaces with `yarn twenty auth:switch`, all subsequent commands will use that workspace by default. You can still override it temporarily with `--workspace <name>`.
Your credentials are stored in `~/.twenty/config.json`.
## Local development server (`yarn twenty server`)
The CLI can manage a local Twenty server running in Docker. This is the same server started automatically when you scaffold an app with `create-twenty-app`, but you can also manage it manually.
### Starting the server
```bash filename="Terminal"
yarn twenty server start
```
This pulls the `twentycrm/twenty-app-dev:latest` Docker image (if not already present), creates a container named `twenty-app-dev`, and starts it on port **2020**. The CLI waits until the server passes its health check before returning.
Two Docker volumes are created to persist data between restarts:
- `twenty-app-dev-data` — PostgreSQL database
- `twenty-app-dev-storage` — file storage
If port 2020 is already in use, you can start on a different port:
```bash filename="Terminal"
yarn twenty server start --port 3030
```
The CLI automatically configures the container's internal `NODE_PORT` and `SERVER_URL` to match the chosen port, so logic functions, OAuth, and all other internal networking work correctly.
Once started, the server is automatically registered as the `local` remote in your CLI config.
### Checking server status
```bash filename="Terminal"
yarn twenty server status
```
Displays whether the server is running, its URL, and the default login credentials (`tim@apple.dev` / `tim@apple.dev`).
### Viewing server logs
```bash filename="Terminal"
yarn twenty server logs
```
Streams the container logs. Use `--lines` to control how many recent lines to show:
```bash filename="Terminal"
yarn twenty server logs --lines 100
```
### Stopping the server
```bash filename="Terminal"
yarn twenty server stop
```
Stops the container. Your data is preserved in the Docker volumes — the next `start` picks up where you left off.
### Resetting the server
```bash filename="Terminal"
yarn twenty server reset
```
Removes the container **and** deletes both Docker volumes, wiping all data. The next `start` creates a fresh instance.
<Note>
The server requires **Docker** to be running. If you see a "Docker not running" error, make sure Docker Desktop (or the Docker daemon) is started.
</Note>
### Command reference
| Command | Description |
|---------|-------------|
| `yarn twenty server start` | Start the local server (pulls image if needed) |
| `yarn twenty server start --port 3030` | Start on a custom port |
| `yarn twenty server stop` | Stop the server (preserves data) |
| `yarn twenty server status` | Show server status, URL, and credentials |
| `yarn twenty server logs` | Stream server logs |
| `yarn twenty server logs --lines 100` | Show the last 100 log lines |
| `yarn twenty server reset` | Delete all data and start fresh |
## CI with GitHub Actions
The scaffolder generates a ready-to-use GitHub Actions workflow at `.github/workflows/ci.yml`. It runs your integration tests automatically on every push to `main` and on pull requests.
The workflow:
1. Checks out your code
2. Spins up a temporary Twenty server using the `twentyhq/twenty/.github/actions/spawn-twenty-docker-image` action
3. Installs dependencies with `yarn install --immutable`
4. Runs `yarn test` with `TWENTY_API_URL` and `TWENTY_API_KEY` injected from the action outputs
```yaml .github/workflows/ci.yml
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 }}
```
You don't need to configure any secrets — the `spawn-twenty-docker-image` action starts an ephemeral Twenty server directly in the runner and outputs the connection details. The `GITHUB_TOKEN` secret is provided automatically by GitHub.
To pin a specific Twenty version instead of `latest`, change the `TWENTY_VERSION` environment variable at the top of the workflow.
## Manual setup (without the scaffolder)
While we recommend using `create-twenty-app` for the best getting-started experience, you can also set up a project manually. Do not install the CLI globally. Instead, add `twenty-sdk` as a local dependency and wire a single script in your package.json:
If you prefer to set things up yourself instead of using `create-twenty-app`, you can do it in two steps.
**1. Add `twenty-sdk` and `twenty-client-sdk` as dependencies:**
```bash filename="Terminal"
yarn add -D twenty-sdk
yarn add twenty-sdk twenty-client-sdk
```
Then add a `twenty` script:
**2. Add a `twenty` script to your `package.json`:**
```json filename="package.json"
{
@@ -208,25 +393,19 @@ Then add a `twenty` script:
}
```
Now you can run all commands via `yarn twenty <command>`, e.g. `yarn twenty dev`, `yarn twenty help`, etc.
You can now run `yarn twenty dev`, `yarn twenty help`, and all other commands.
## How to use a local Twenty instance
If you're already running a Twenty instance locally (e.g. via `npx nx start twenty-server`), you can connect to it instead of using Docker:
```bash filename="Terminal"
# During scaffolding — skip Docker, connect to your running instance
npx create-twenty-app@latest my-app --port 3000
# Or after scaffolding — add a remote pointing to your instance
yarn twenty remote add --local --port 3000
```
<Note>
Do not install `twenty-sdk` globally. Always use it as a local project dependency so that each project can pin its own version.
</Note>
## Troubleshooting
- Authentication errors: run `yarn twenty auth:login` and ensure your API key has the required permissions.
- Cannot connect to server: verify the API URL and that the Twenty server is reachable.
- Types or client missing/outdated: restart `yarn twenty dev` — it auto-generates the typed client.
- Dev mode not syncing: ensure `yarn twenty dev` is running and that changes are not ignored by your environment.
If you run into issues:
Discord Help Channel: https://discord.com/channels/1130383047699738754/1130386664812982322
- Make sure **Docker is running** before starting the scaffolder with a local instance.
- Make sure you are using **Node.js 24+** (`node -v` to check).
- Make sure **Corepack is enabled** (`corepack enable`) so Yarn 4 is available.
- Try deleting `node_modules` and running `yarn install` again if dependencies seem broken.
Still stuck? Ask for help on the [Twenty Discord](https://discord.com/channels/1130383047699738754/1130386664812982322).
@@ -4,34 +4,76 @@ description: Distribute your Twenty app to the marketplace or deploy it internal
---
<Warning>
Apps are currently in alpha testing. The feature is functional but still evolving.
Apps are currently in alpha. The feature works but is still evolving.
</Warning>
## Overview
Once your app is [built and tested locally](/developers/extend/apps/building), you have two paths for distributing it:
- **Publish to npm** — list your app in the Twenty marketplace for any workspace to discover and install.
- **Deploy a tarball** — upload your app directly to a specific Twenty server for internal or private use.
- **Publish to npm** — list your app in the Twenty marketplace for any workspace to discover and install.
Both paths start from the same **build** step.
## Building your app
The `build` command compiles your TypeScript sources, transpiles logic functions and front components, and generates a `manifest.json` that describes your app's contents:
Run the build command to compile your app and generate a distribution-ready `manifest.json`:
```bash filename="Terminal"
yarn twenty build
```
The output is written to `.twenty/output/`. This directory contains everything needed for distribution: compiled code, assets, the manifest, and a copy of your `package.json`.
This compiles TypeScript sources, transpiles logic functions and front components, and writes everything to `.twenty/output/`. Add `--tarball` to also produce a `.tgz` package for manual distribution or the deploy command.
To also create a `.tgz` tarball (used by the deploy command internally, or for manual distribution):
## Deploying to a server (tarball)
For apps you don't want publicly available — proprietary tools, enterprise-only integrations, or experimental builds — you can deploy a tarball directly to a Twenty server.
### Prerequisites
Before deploying, you need a configured remote pointing to the target server. Remotes store the server URL and authentication credentials locally in `~/.twenty/config.json`.
Add a remote:
```bash filename="Terminal"
yarn twenty build --tarball
yarn twenty remote add --api-url https://your-twenty-server.com --as production
```
### Deploying
Build and upload your app to the server in one step:
```bash filename="Terminal"
yarn twenty deploy
# To deploy to a specific remote:
# yarn twenty deploy --remote production
```
### Sharing a deployed app
Tarball apps are not listed in the public marketplace, so other workspaces on the same server won't discover them by browsing. To share a deployed app:
1. Go to **Settings > Applications > Registrations** and open your app
2. In the **Distribution** tab, click **Copy share link**
3. Share this link with users on other workspaces — it takes them directly to the app's install page
The share link uses the server's base URL (without any workspace subdomain) so it works for any workspace on the server.
<Warning>
Sharing private apps is an Enterprise feature. Go to [Settings > Admin Panel > Enterprise](/settings/admin-panel#enterprise) to enable it.
</Warning>
### Version management
To release an update:
1. Bump the `version` field in your `package.json`
2. Run `yarn twenty deploy` (or `yarn twenty deploy --remote production`)
3. Workspaces that have the app installed will see the upgrade available in their settings
{/* TODO: add screenshot of the Upgrade button */}
## Publishing to npm
Publishing to npm makes your app discoverable in the Twenty marketplace. Any Twenty workspace can browse, install, and upgrade marketplace apps directly from the UI.
@@ -39,41 +81,42 @@ Publishing to npm makes your app discoverable in the Twenty marketplace. Any Twe
### Requirements
- An [npm](https://www.npmjs.com) account
- The `twenty-app` keyword **must** be listed in your `package.json` `keywords` array
### Adding the required keyword
The Twenty marketplace discovers apps by searching the npm registry for packages with the `twenty-app` keyword. Add it to your `package.json`:
- The `twenty-app` keyword in your `package.json` `keywords` array (already included when you scaffold with `create-twenty-app`)
```json filename="package.json"
{
"name": "twenty-app-postcard-sender",
"version": "1.0.0",
"keywords": ["twenty-app"],
...
"keywords": ["twenty-app"]
}
```
<Note>
The marketplace searches for `keywords:twenty-app` on the npm registry. Without this keyword, your package won't appear in the marketplace even if it has the `twenty-app-` name prefix.
</Note>
### Marketplace metadata
### Steps
The `defineApplication()` config supports optional fields that control how your app appears in the marketplace. Use `logoUrl` and `screenshots` to reference images from the `public/` folder:
1. **Build your app:**
```bash filename="Terminal"
yarn twenty build
```ts src/application-config.ts
export default defineApplication({
universalIdentifier: '...',
displayName: 'My App',
description: 'A great app',
defaultRoleUniversalIdentifier: DEFAULT_ROLE_UNIVERSAL_IDENTIFIER,
logoUrl: 'public/logo.png',
screenshots: [
'public/screenshot-1.png',
'public/screenshot-2.png',
],
});
```
2. **Publish to npm:**
See the [defineApplication accordion](/developers/extend/apps/building#defineentity-functions) in the Building Apps page for the full list of marketplace fields (`author`, `category`, `aboutDescription`, `websiteUrl`, `termsUrl`, etc.).
### Publish
```bash filename="Terminal"
yarn twenty publish
```
This runs `npm publish` from the `.twenty/output/` directory.
To publish under a specific dist-tag (e.g., `beta` or `next`):
```bash filename="Terminal"
@@ -82,25 +125,17 @@ yarn twenty publish --tag beta
### How marketplace discovery works
The Twenty server syncs its marketplace catalog from the npm registry **every hour**:
The Twenty server syncs its marketplace catalog from the npm registry **every hour**.
1. It searches for all npm packages with the `keywords:twenty-app` keyword
2. For each package, it fetches the `manifest.json` from the npm CDN
3. The app's metadata (name, description, author, logo, screenshots, category) is extracted from the manifest and displayed in the marketplace
After publishing, your app can take up to one hour to appear in the marketplace. To trigger the sync immediately instead of waiting for the next hourly run:
You can trigger the sync immediately instead of waiting:
```bash filename="Terminal"
yarn twenty catalog-sync
# To target a specific remote:
# yarn twenty catalog-sync --remote production
```
To target a specific remote:
```bash filename="Terminal"
yarn twenty catalog-sync -r production
```
The metadata shown in the marketplace comes from your `defineApplication()` call in your app source code — fields like `displayName`, `description`, `author`, `category`, `logoUrl`, `screenshots`, `aboutDescription`, `websiteUrl`, and `termsUrl`.
The metadata shown in the marketplace comes from your `defineApplication()` config — fields like `displayName`, `description`, `author`, `category`, `logoUrl`, `screenshots`, `aboutDescription`, `websiteUrl`, and `termsUrl`.
<Note>
If your app does not define an `aboutDescription` in `defineApplication()`, the marketplace will automatically use your package's `README.md` from npm as the about page content. This means you can maintain a single README for both npm and the Twenty marketplace. If you want a different description in the marketplace, explicitly set `aboutDescription`.
@@ -108,7 +143,7 @@ If your app does not define an `aboutDescription` in `defineApplication()`, the
### CI publishing
The scaffolded project includes a GitHub Actions workflow that publishes on every release:
Use this GitHub Actions workflow to publish automatically on every release (uses [OIDC](https://docs.npmjs.com/trusted-publishers)):
```yaml filename=".github/workflows/publish.yml"
name: Publish
@@ -133,121 +168,24 @@ jobs:
- run: npx twenty build
- run: npm publish --provenance --access public
working-directory: .twenty/output
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
```
For other CI systems (GitLab CI, CircleCI, etc.), the same three commands apply: `yarn install`, `yarn twenty build`, then `npm publish` from `.twenty/output`.
<Tip>
<Note>
**npm provenance** is optional but recommended. Publishing with `--provenance` adds a trust badge to your npm listing, letting users verify the package was built from a specific commit in a public CI pipeline. See the [npm provenance docs](https://docs.npmjs.com/generating-provenance-statements) for setup instructions.
</Tip>
## Deploying to a server (tarball)
For apps you don't want publicly available — proprietary tools, enterprise-only integrations, or experimental builds — you can deploy a tarball directly to a Twenty server.
### Prerequisites
Before deploying, you need a configured remote pointing to the target server. Remotes store the server URL and authentication credentials locally in `~/.twenty/config.json`.
Add a remote:
```bash filename="Terminal"
yarn twenty remote add --url https://your-twenty-server.com --as production
```
For a local development server:
```bash filename="Terminal"
yarn twenty remote add --local --as local
```
You can also authenticate with an API key for non-interactive environments:
```bash filename="Terminal"
yarn twenty remote add --url https://your-twenty-server.com --token <api-key> --as production
```
Manage your remotes:
```bash filename="Terminal"
yarn twenty remote list # List all configured remotes
yarn twenty remote switch prod # Set the default remote
yarn twenty remote status # Show active remote and auth status
yarn twenty remote remove old # Remove a remote
```
### Deploying
Build and upload your app to the server in one step:
```bash filename="Terminal"
yarn twenty deploy
```
This builds the app with `--tarball`, then uploads the tarball to the default remote via a GraphQL multipart upload.
To deploy to a specific remote:
```bash filename="Terminal"
yarn twenty deploy -r production
```
### Sharing a deployed app
Tarball apps are not listed in the public marketplace, so other workspaces on the same server won't discover them by browsing. To share a deployed app:
1. Go to **Settings > Applications > Registrations** and open your app
2. In the **Distribution** tab, click **Copy share link**
3. Share this link with users on other workspaces — it takes them directly to the app's install page
The share link uses the server's base URL (without any workspace subdomain) so it works for any workspace on the server.
### Version management
To release an update:
1. Bump the `version` field in your `package.json`
2. Run `yarn twenty deploy` (or `yarn twenty deploy -r production`)
3. Workspaces that have the app installed will see the upgrade available in their settings
</Note>
## Installing apps
Once an app is published (npm) or deployed (tarball), workspaces install it through the UI:
Once an app is published (npm) or deployed (tarball), workspaces can install it through the UI.
Go to the **Settings > Applications** page in Twenty, where both marketplace and tarball-deployed apps can be browsed and installed.
{/* TODO: add screenshot of the UI when the app is registered */}
You can also install apps from the command line:
```bash filename="Terminal"
yarn twenty install
```
Or from the **Settings > Applications** page in the Twenty UI, where both marketplace and tarball-deployed apps can be browsed and installed.
## App distribution categories
Twenty organizes apps into three categories based on how they're distributed:
| Category | How it works | Visible in marketplace? |
|----------|-------------|------------------------|
| **Development** | Local dev mode apps running via `yarn twenty dev`. Used for building and testing. | No |
| **Published (npm)** | Apps published to npm with the `twenty-app` keyword. Listed in the marketplace for any workspace to install. | Yes |
| **Internal (tarball)** | Apps deployed via tarball to a specific server. Available only to workspaces on that server via a share link. | No |
<Tip>
Start in **Development** mode while building your app. When it's ready, choose **Published** (npm) for broad distribution or **Internal** (tarball) for private deployment.
</Tip>
## CLI reference
| Command | Description | Key flags |
|---------|-------------|-----------|
| `yarn twenty build` | Compile app and generate manifest | `--tarball` — also create a `.tgz` package |
| `yarn twenty publish` | Build and publish to npm | `--tag <tag>` — npm dist-tag (e.g., `beta`, `next`) |
| `yarn twenty deploy` | Build and upload tarball to a server | `-r, --remote <name>` — target remote |
| `yarn twenty catalog-sync` | Trigger marketplace catalog sync on the server | `-r, --remote <name>` — target remote |
| `yarn twenty install` | Install a deployed app on a workspace | `-r, --remote <name>` — target remote |
| `yarn twenty dev` | Watch and sync local changes | Uses default remote |
| `yarn twenty remote add` | Add a server connection | `--url`, `--token`, `--as`, `--local`, `--port` |
| `yarn twenty remote list` | List configured remotes | — |
| `yarn twenty remote switch` | Set default remote | — |
| `yarn twenty remote status` | Show connection status | — |
| `yarn twenty remote remove` | Remove a remote | — |
File diff suppressed because it is too large Load Diff
Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 315 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 788 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 996 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 173 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 88 KiB

+23 -324
View File
@@ -14,12 +14,9 @@
A CLI and SDK to develop, build, and publish applications that extend [Twenty CRM](https://twenty.com).
- Typed GraphQL clients: `CoreApiClient` (auto-generated per app for workspace data) and `MetadataApiClient` (pre-built with the SDK for workspace configuration & file uploads)
- Builtin CLI for auth, dev mode (watch & sync), uninstall, and function management
## Quick start
## Getting Started
The recommended way to start building a Twenty app is with [**create-twenty-app**](https://www.npmjs.com/package/create-twenty-app), which scaffolds a project with everything preconfigured:
The recommended way to start is with [create-twenty-app](https://www.npmjs.com/package/create-twenty-app):
```bash
npx create-twenty-app@latest my-app
@@ -27,330 +24,47 @@ cd my-app
yarn twenty dev
```
See the [create-twenty-app README](https://www.npmjs.com/package/create-twenty-app) or the [full documentation](https://docs.twenty.com/developers/extend/capabilities/apps) for details.
## Documentation
## Prerequisites
Full documentation is available at **[docs.twenty.com/developers/extend/apps](https://docs.twenty.com/developers/extend/apps/getting-started)**:
- Node.js 24+ (recommended) and Yarn 4
- Docker (for the local Twenty dev server) or a remote Twenty workspace
- [Getting Started](https://docs.twenty.com/developers/extend/apps/getting-started) — scaffolding, local server, authentication, dev mode
- [Building Apps](https://docs.twenty.com/developers/extend/apps/building) — entity definitions, API clients, testing, CLI reference
- [Publishing](https://docs.twenty.com/developers/extend/apps/publishing) — deploy, npm publish, marketplace
## Manual Installation
## Manual installation
If you're adding `twenty-sdk` to an existing project instead of using `create-twenty-app`:
If you are adding `twenty-sdk` to an existing project instead of using `create-twenty-app`:
```bash
npm install twenty-sdk
# or
yarn add twenty-sdk
yarn add twenty-sdk twenty-client-sdk
```
## Usage
```
Usage: twenty [options] [command]
CLI for Twenty application development
Options:
-V, --version output the version number
-r, --remote <name> Use a specific remote (overrides the default set by remote switch)
-h, --help display help for command
Commands:
dev [appPath] Watch and sync local application changes
build [appPath] Build, sync, and generate API client into .twenty/output/
deploy [appPath] Build and deploy to a Twenty server
publish [appPath] Build and publish to npm
typecheck [appPath] Run TypeScript type checking on the application
uninstall [appPath] Uninstall application from Twenty
remote Manage remote Twenty servers
server Manage a local Twenty server instance
add [entityType] Add a new entity to your application
exec [appPath] Execute a logic function with a JSON payload
logs [appPath] Watch application function logs
help [command] display help for command
```
In a project created with `create-twenty-app` (recommended), use `yarn twenty <command>` instead of calling `twenty` directly. For example: `yarn twenty help`, `yarn twenty dev`, etc.
## Global Options
- `--remote <name>` (or `-r <name>`): Use a specific remote configuration. Defaults to `local`. See Configuration for details.
## Commands
### Server
Manage a local Twenty dev server (all-in-one Docker image on port 2020). These commands only apply to the Docker-based dev server — they do not manage a Twenty instance started from source (e.g. `npx nx start twenty-server` on port 3000).
- `twenty server start` — Start the local server (pulls image if needed). Automatically configures the `local` remote.
- Options:
- `-p, --port <port>`: HTTP port (default: `2020`).
- `twenty server stop` — Stop the local server.
- `twenty server logs` — Stream server logs.
- Options:
- `-n, --lines <lines>`: Number of initial lines to show (default: `50`).
- `twenty server status` — Show server status (running/stopped/healthy).
- `twenty server reset` — Delete all data and start fresh.
The server comes pre-seeded with a workspace and user (`tim@apple.dev` / `tim@apple.dev`).
Examples:
```bash
# Start the local server
twenty server start
# Check if it's ready
twenty server status
# Follow logs during first startup
twenty server logs
# Stop the server (data is preserved)
twenty server stop
# Wipe everything and start over
twenty server reset
```
### Remote
Manage remote server connections and authentication.
- `twenty remote add [nameOrUrl]` — Add a new remote or re-authenticate an existing one.
- Options:
- `--token <token>`: API key for non-interactive auth.
- `--url <url>`: Server URL (alternative to positional arg).
- `--as <name>`: Name for this remote (otherwise derived from URL hostname).
- Behavior: If `nameOrUrl` matches an existing remote name, re-authenticates it. Otherwise, creates a new remote and authenticates via OAuth (with API key fallback).
- `twenty remote remove <name>` — Remove a remote and its credentials.
- `twenty remote list` — List all configured remotes with their auth status and URLs.
- `twenty remote switch [name]` — Set the default remote.
- If omitted, shows an interactive selection.
- `twenty remote status` — Print the current remote name, server URL, and auth status.
Examples:
```bash
# Add a remote interactively (recommended)
twenty remote add
# Provide values in flags (non-interactive, for CI)
twenty remote add https://api.twenty.com --token $TWENTY_API_KEY
# Name a remote explicitly
twenty remote add https://api.twenty.com --as production
# Re-authenticate an existing remote by name
twenty remote add production
# Check status
twenty remote status
# List all configured remotes
twenty remote list
# Switch default remote
twenty remote switch production
# Remove a remote
twenty remote remove production
```
### App
Application development commands.
- `twenty dev [appPath]` — Start development mode: watch and sync local application changes.
- Behavior: Builds your application (functions and front components), computes the manifest, syncs everything to your remote, then watches the directory for changes and re-syncs automatically. Displays an interactive UI showing build and sync status in real time. Press Ctrl+C to stop.
- `twenty build [appPath]` — Build the application, sync to the server, generate the typed API client, then rebuild with the real client.
- Options:
- `--tarball`: Also pack the output into a `.tgz` tarball.
- `twenty publish [appPath]` — Build and publish the application to npm.
- Behavior: Builds the application and runs `npm publish` on the output directory.
- Options:
- `--tag <tag>`: npm dist-tag (e.g. `beta`, `next`).
- `twenty deploy [appPath]` — Build and deploy the application to a Twenty server.
- Behavior: Builds the tarball, uploads it to the server, and installs the application.
- Options:
- `--server <url>`: Target Twenty server URL.
- `--token <token>`: Auth token for the server.
- `twenty typecheck [appPath]` — Run TypeScript type checking on the application (runs `tsc --noEmit`). Exits with code 1 if type errors are found.
- `twenty uninstall [appPath]` — Uninstall the application from the current remote.
### Entity
- `twenty add [entityType]` — Add a new entity to your application.
- Arguments:
- `entityType`: one of `object`, `field`, `function`, `front-component`, `role`, `view`, `navigation-menu-item`, or `skill`. If omitted, an interactive prompt is shown.
- Options:
- `--path <path>`: The path where the entity file should be created (relative to the current directory).
- Behavior:
- `object`: prompts for singular/plural names and labels, then creates a `*.object.ts` definition file.
- `field`: prompts for name, label, type, and target object, then creates a `*.field.ts` definition file.
- `function`: prompts for a name and scaffolds a `*.function.ts` logic function file.
- `front-component`: prompts for a name and scaffolds a `*.front-component.tsx` file.
- `role`: prompts for a name and scaffolds a `*.role.ts` role definition file.
- `view`: prompts for a name and target object, then creates a `*.view.ts` definition file.
- `navigation-menu-item`: prompts for a name and scaffolds a `*.navigation-menu-item.ts` file.
- `skill`: prompts for a name and scaffolds a `*.skill.ts` skill definition file.
### Function
- `twenty logs [appPath]` — Stream application function logs.
- Options:
- `-u, --functionUniversalIdentifier <id>`: Only show logs for a specific function universal ID.
- `-n, --functionName <name>`: Only show logs for a specific function name.
- `twenty exec [appPath]` — Execute a logic function with a JSON payload.
- Options:
- `--preInstall`: Execute the pre-install logic function defined in the application manifest (required if `--postInstall`, `-n`, and `-u` not provided).
- `--postInstall`: Execute the post-install logic function defined in the application manifest (required if `--preInstall`, `-n`, and `-u` not provided).
- `-n, --functionName <name>`: Name of the function to execute (required if `--postInstall` and `-u` not provided).
- `-u, --functionUniversalIdentifier <id>`: Universal ID of the function to execute (required if `--postInstall` and `-n` not provided).
- `-p, --payload <payload>`: JSON payload to send to the function (default: `{}`).
Examples:
```bash
# Start dev mode (watch, build, and sync)
twenty dev
# Start dev mode with a custom remote
twenty dev --remote my-custom-remote
# Type check the application
twenty typecheck
# Add a new entity interactively
twenty add
# Add a new function
twenty add function
# Add a new front component
twenty add front-component
# Add a new view
twenty add view
# Add a new navigation menu item
twenty add navigation-menu-item
# Add a new skill
twenty add skill
# Build the app (output in .twenty/output/)
twenty build
# Build and create a tarball
twenty build --tarball
# Publish to npm
twenty publish
# Publish with a dist-tag
twenty publish --tag beta
# Deploy directly to a Twenty server (builds, uploads, and installs)
twenty deploy --server https://app.twenty.com
# Uninstall the app from the remote
twenty uninstall
# Watch all function logs
twenty logs
# Watch logs for a specific function by name
twenty logs -n my-function
# Execute a function by name (with empty payload)
twenty exec -n my-function
# Execute a function with a JSON payload
twenty exec -n my-function -p '{"name": "test"}'
# Execute a function by universal identifier
twenty exec -u e56d363b-0bdc-4d8a-a393-6f0d1c75bdcf -p '{"key": "value"}'
# Execute the pre-install function
twenty exec --preInstall
# Execute the post-install function
twenty exec --postInstall
```
## Configuration
The CLI stores configuration per user in a JSON file:
- Location: `~/.twenty/config.json`
- Structure: Remotes keyed by name. The active remote is selected with `--remote <name>` or by the `defaultRemote` setting.
Example configuration file:
Then add a `twenty` script to your `package.json`:
```json
{
"defaultRemote": "production",
"remotes": {
"local": {
"apiUrl": "http://localhost:2020",
"apiKey": "<your-api-key>"
},
"production": {
"apiUrl": "https://api.twenty.com",
"accessToken": "<oauth-token>",
"refreshToken": "<refresh-token>",
"oauthClientId": "<client-id>"
}
"scripts": {
"twenty": "twenty"
}
}
```
Notes:
Run `yarn twenty help` to see all available commands.
- If a remote is missing, `apiUrl` defaults to `http://localhost:2020`.
- `twenty remote add` writes credentials for the active remote (OAuth tokens or API key).
- `twenty remote add --as my-remote` saves under a custom name.
- `twenty remote switch` sets the `defaultRemote` field, used when `-r` is not specified.
- `twenty remote list` shows all configured remotes and their authentication status.
## Configuration
## How to use a local Twenty instance
If you're already running a local Twenty instance, you can connect to it instead of using Docker:
```bash
twenty remote add http://localhost:3000 --as local
```
The CLI stores credentials per remote in `~/.twenty/config.json`. Run `yarn twenty remote add` to configure a remote, or `yarn twenty remote list` to see existing ones.
## Troubleshooting
- Auth errors: run `twenty remote add` again (or add a new remote) and ensure the API key has the required permissions.
- Typings out of date: restart `twenty dev` to refresh the client and types.
- Not seeing changes in dev: make sure dev mode is running (`twenty dev`).
- Auth errors: run `yarn twenty remote add` to re-authenticate.
- Typings out of date: restart `yarn twenty dev` to refresh the client and types.
- Not seeing changes in dev: make sure dev mode is running (`yarn twenty dev`).
## Contributing
### Development Setup
To contribute to the twenty-sdk package, clone the repository and install dependencies:
### Development setup
```bash
git clone https://github.com/twentyhq/twenty.git
@@ -358,37 +72,22 @@ cd twenty
yarn install
```
### Development Mode
Run the SDK build in watch mode to automatically rebuild on file changes:
### Development mode
```bash
npx nx run twenty-sdk:dev
```
This will watch for changes and rebuild the `dist` folder automatically.
### Production Build
Build the SDK for production:
### Production build
```bash
npx nx run twenty-sdk:build
```
### Running the CLI Locally
After building, you can run the CLI directly:
### Running the CLI locally
```bash
npx nx run twenty-sdk:start -- <command>
# Example: npx nx run twenty-sdk:start -- remote status
```
Or run the built CLI directly:
```bash
node packages/twenty-sdk/dist/cli.cjs <command>
```
### Resources
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "twenty-sdk",
"version": "0.8.0-canary.7",
"version": "0.8.0-canary.8",
"main": "dist/index.cjs",
"module": "dist/index.mjs",
"types": "dist/sdk/index.d.ts",
@@ -134,6 +134,7 @@ export const registerCommands = (program: Command): void => {
program
.command('exec [appPath]')
.option('--preInstall', 'Execute pre-install logic function if defined')
.option('--postInstall', 'Execute post-install logic function if defined')
.option(
'-p, --payload <payload>',
@@ -153,6 +154,7 @@ export const registerCommands = (program: Command): void => {
async (
appPath?: string,
options?: {
preInstall?: boolean;
postInstall?: boolean;
payload?: string;
functionUniversalIdentifier?: string;
@@ -160,13 +162,14 @@ export const registerCommands = (program: Command): void => {
},
) => {
if (
!options?.preInstall &&
!options?.postInstall &&
!options?.functionUniversalIdentifier &&
!options?.functionName
) {
console.error(
chalk.red(
'Error: Either --postInstall or --functionName (-n) or --functionUniversalIdentifier (-u) is required.',
'Error: Either --preInstall, --postInstall, --functionName (-n), or --functionUniversalIdentifier (-u) is required.',
),
);
process.exit(1);
+14 -8
View File
@@ -7,12 +7,14 @@ import { isDefined } from 'twenty-shared/utils';
export class LogicFunctionExecuteCommand {
async execute({
appPath = CURRENT_EXECUTION_DIRECTORY,
preInstall = false,
postInstall = false,
functionUniversalIdentifier,
functionName,
payload = '{}',
}: {
appPath?: string;
preInstall?: boolean;
postInstall?: boolean;
functionUniversalIdentifier?: string;
functionName?: string;
@@ -28,18 +30,22 @@ export class LogicFunctionExecuteCommand {
process.exit(1);
}
const identifier = postInstall
? 'post install'
: (functionUniversalIdentifier ?? functionName);
const identifier = preInstall
? 'pre install'
: postInstall
? 'post install'
: (functionUniversalIdentifier ?? functionName);
console.log(chalk.blue(`🚀 Executing function "${identifier}"...`));
console.log(chalk.gray(` Payload: ${JSON.stringify(parsedPayload)}\n`));
const executeOptions = postInstall
? { appPath, postInstall: true as const, payload: parsedPayload }
: functionUniversalIdentifier
? { appPath, functionUniversalIdentifier, payload: parsedPayload }
: { appPath, functionName: functionName!, payload: parsedPayload };
const executeOptions = preInstall
? { appPath, preInstall: true as const, payload: parsedPayload }
: postInstall
? { appPath, postInstall: true as const, payload: parsedPayload }
: functionUniversalIdentifier
? { appPath, functionUniversalIdentifier, payload: parsedPayload }
: { appPath, functionName: functionName!, payload: parsedPayload };
const result = await functionExecute(executeOptions);
+19 -31
View File
@@ -17,9 +17,9 @@ const deriveRemoteName = (url: string): string => {
}
};
const authenticate = async (apiUrl: string, token?: string): Promise<void> => {
const result = token
? await authLogin({ apiKey: token, apiUrl })
const authenticate = async (apiUrl: string, apiKey?: string): Promise<void> => {
const result = apiKey
? await authLogin({ apiKey, apiUrl })
: await runOAuthWithApiKeyFallback(apiUrl);
if (!result.success) {
@@ -66,40 +66,32 @@ export const registerRemoteCommands = (program: Command): void => {
.description('Manage remote Twenty servers');
remote
.command('add [nameOrUrl]')
.command('add')
.description('Add a new remote or re-authenticate an existing one')
.option('--as <name>', 'Name for this remote')
.option('--token <token>', 'API key for non-interactive auth')
.option('--url <url>', 'Server URL (alternative to positional arg)')
.option('--api-key <apiKey>', 'API key for non-interactive auth')
.option('--api-url <apiUrl>', 'Server URL')
.option('--local', 'Connect to a local Twenty server (auto-detect)')
.action(
async (
nameOrUrl: string | undefined,
options: {
as?: string;
token?: string;
url?: string;
local?: boolean;
},
) => {
async (options: {
as?: string;
apiKey?: string;
apiUrl?: string;
local?: boolean;
}) => {
const configService = new ConfigService();
const existingRemotes = await configService.getRemotes();
// Re-authenticate an existing remote by name
const isExistingRemote =
nameOrUrl !== undefined && existingRemotes.includes(nameOrUrl);
if (options.as !== undefined && existingRemotes.includes(options.as)) {
const config = await configService.getConfigForRemote(options.as);
if (isExistingRemote) {
const config = await configService.getConfigForRemote(nameOrUrl);
ConfigService.setActiveRemote(nameOrUrl);
await authenticate(config.apiUrl, options.token);
ConfigService.setActiveRemote(options.as);
await authenticate(config.apiUrl, options.apiKey);
return;
}
// Resolve the URL — from args, flags, auto-detect, or interactive prompt
let apiUrl = nameOrUrl ?? options.url;
let apiUrl = options.apiUrl;
if (!apiUrl) {
const detectedUrl = await detectLocalServer();
@@ -115,12 +107,8 @@ export const registerRemoteCommands = (program: Command): void => {
process.exit(1);
}
apiUrl = detectedUrl;
} else if (detectedUrl) {
console.log(chalk.gray(`Found local server at ${detectedUrl}`));
apiUrl = detectedUrl;
} else if (options.token) {
apiUrl = 'http://localhost:2020';
} else {
apiUrl = (
await inquirer.prompt<{ apiUrl: string }>([
@@ -146,7 +134,7 @@ export const registerRemoteCommands = (program: Command): void => {
const name = options.as ?? deriveRemoteName(apiUrl);
ConfigService.setActiveRemote(name);
await authenticate(apiUrl, options.token);
await authenticate(apiUrl, options.apiKey);
const defaultRemote = await configService.getDefaultRemote();
@@ -166,7 +154,7 @@ export const registerRemoteCommands = (program: Command): void => {
if (remotes.length === 0) {
console.log('No remotes configured.');
console.log("Use 'twenty remote add <url>' to add one.");
console.log("Use 'twenty remote add' to add one.");
return;
}
@@ -15,6 +15,7 @@ export type FunctionExecuteOptions = {
remote?: string;
payload?: Record<string, unknown>;
} & (
| { preInstall: true }
| { postInstall: true }
| { functionUniversalIdentifier: string }
| { functionName: string }
@@ -38,6 +39,7 @@ const belongsToApplication = (
};
const resolveIdentifier = (options: FunctionExecuteOptions): string => {
if ('preInstall' in options) return 'pre install';
if ('postInstall' in options) return 'post install';
if ('functionUniversalIdentifier' in options)
return options.functionUniversalIdentifier;
@@ -90,6 +92,12 @@ const innerFunctionExecute = async (
);
const targetFunction = appFunctions.find((logicFunction) => {
if ('preInstall' in options && options.preInstall) {
return (
logicFunction.universalIdentifier ===
manifest.application.preInstallLogicFunctionUniversalIdentifier
);
}
if ('postInstall' in options && options.postInstall) {
return (
logicFunction.universalIdentifier ===
@@ -14,25 +14,86 @@ import {
checkServerHealth,
detectLocalServer,
} from '@/cli/utilities/server/detect-local-server';
import { execSync, spawnSync } from 'node:child_process';
import { execSync, spawn, spawnSync } from 'node:child_process';
import chalk from 'chalk';
const HEALTH_POLL_INTERVAL_MS = 2000;
const HEALTH_TIMEOUT_MS = 180 * 1000;
const MILESTONE_START = '==> START ';
const MILESTONE_DONE = '==> DONE';
const waitForHealthy = async (port: number): Promise<boolean> => {
const startTime = Date.now();
const onProgress = (message: string) =>
process.stdout.write(chalk.gray(message));
while (Date.now() - startTime < HEALTH_TIMEOUT_MS) {
if (await checkServerHealth(port)) {
return true;
const logStream = spawn(
'docker',
['logs', '-f', '--since', '1s', CONTAINER_NAME],
{ stdio: ['ignore', 'pipe', 'pipe'] },
);
logStream.on('error', () => {});
let hasPendingStep = false;
const handleLogLine = (line: string) => {
const trimmed = line.trim();
const startIndex = trimmed.indexOf(MILESTONE_START);
const doneIndex = trimmed.indexOf(MILESTONE_DONE);
if (startIndex !== -1) {
if (hasPendingStep) {
onProgress('Done\n');
}
const message = trimmed.slice(startIndex + MILESTONE_START.length);
onProgress(`==> ${message}... `);
hasPendingStep = true;
} else if (doneIndex !== -1 && hasPendingStep) {
onProgress('Done\n');
hasPendingStep = false;
}
};
let logBuffer = '';
const onData = (chunk: Buffer) => {
logBuffer += chunk.toString();
const lines = logBuffer.split('\n');
logBuffer = lines.pop() ?? '';
lines.forEach(handleLogLine);
};
logStream.stdout?.on('data', onData);
logStream.stderr?.on('data', onData);
try {
while (Date.now() - startTime < HEALTH_TIMEOUT_MS) {
if (await checkServerHealth(port)) {
if (hasPendingStep) {
onProgress('Done\n');
}
return true;
}
await new Promise((resolve) =>
setTimeout(resolve, HEALTH_POLL_INTERVAL_MS),
);
}
await new Promise((resolve) =>
setTimeout(resolve, HEALTH_POLL_INTERVAL_MS),
);
}
if (hasPendingStep) {
onProgress('Failed\n');
}
return false;
return false;
} finally {
logStream.kill();
}
};
export type ServerStartOptions = {
@@ -126,8 +187,6 @@ const innerServerStart = async (
} else {
onProgress?.('Starting Twenty container...');
const serverUrl = `http://localhost:${port}`;
const runResult = spawnSync(
'docker',
[
@@ -135,14 +194,16 @@ const innerServerStart = async (
'-d',
'--name',
CONTAINER_NAME,
'-e',
`SERVER_URL=${serverUrl}`,
'-p',
`${port}:3000`,
`${port}:${port}`,
'-e',
`NODE_PORT=${port}`,
'-e',
`SERVER_URL=http://localhost:${port}`,
'-v',
'twenty-app-dev-data:/data/postgres',
'-v',
'twenty-app-dev-storage:/app/.local-storage',
'twenty-app-dev-storage:/app/packages/twenty-server/.local-storage',
IMAGE,
],
{ stdio: 'inherit' },
@@ -73,7 +73,7 @@ export class CheckServerOrchestratorStep {
this.state.applyStepEvents([
{
message:
'Authentication failed. Run `yarn twenty remote add` to authenticate.',
'Authentication failed. Run `yarn twenty remote add --local` to authenticate.',
status: 'error',
},
]);
@@ -20,11 +20,13 @@ export const isContainerRunning = (): boolean => {
export const getContainerPort = (): number => {
try {
const result = execSync(
`docker inspect -f '{{(index (index .NetworkSettings.Ports "3000/tcp") 0).HostPort}}' ${CONTAINER_NAME}`,
`docker inspect -f '{{range .Config.Env}}{{println .}}{{end}}' ${CONTAINER_NAME}`,
{ encoding: 'utf-8', stdio: ['pipe', 'pipe', 'ignore'] },
).trim();
);
return parseInt(result, 10) || DEFAULT_PORT;
const match = result.match(/^NODE_PORT=(\d+)$/m);
return match ? parseInt(match[1], 10) : DEFAULT_PORT;
} catch {
return DEFAULT_PORT;
}
@@ -0,0 +1,42 @@
import {
DEFAULT_API_URL_NAME,
DEFAULT_APP_ACCESS_TOKEN_NAME,
} from 'twenty-shared/application';
const decodeTokenPayload = (
token: string,
): { workspaceId: string; applicationId: string } => {
const payload = JSON.parse(atob(token.split('.')[1]));
return {
workspaceId: payload.workspaceId,
applicationId: payload.applicationId,
};
};
// Returns the public URL for a file in the app's public/ directory.
// Works in both logic functions and front components.
// The path is relative to the public/ folder (e.g. "images/logo.png").
export const getPublicAssetUrl = (path: string): string => {
const apiUrl = process.env[DEFAULT_API_URL_NAME];
const token = process.env[DEFAULT_APP_ACCESS_TOKEN_NAME];
if (!apiUrl || !token) {
throw new Error(
'getPublicAssetUrl can only be called from within a logic function or front component',
);
}
const { workspaceId, applicationId } = decodeTokenPayload(token);
const withoutLeadingSlash = path.startsWith('/') ? path.slice(1) : path;
const withPublicPrefix = withoutLeadingSlash.startsWith('public/')
? withoutLeadingSlash
: `public/${withoutLeadingSlash}`;
const encodedPath = withPublicPrefix
.split('/')
.map(encodeURIComponent)
.join('/');
return `${apiUrl}/public-assets/${workspaceId}/${applicationId}/${encodedPath}`;
};
+1
View File
@@ -35,6 +35,7 @@ export type {
FrontComponentConfig,
FrontComponentType,
} from './front-component-config';
export { getPublicAssetUrl } from './get-public-asset-url';
export { defineLogicFunction } from './logic-functions/define-logic-function';
export { definePostInstallLogicFunction } from './logic-functions/define-post-install-logic-function';
export { definePreInstallLogicFunction } from './logic-functions/define-pre-install-logic-function';
@@ -1,6 +1,6 @@
import { Logger } from '@nestjs/common';
import { Command, CommandRunner } from 'nest-commander';
import { Command, CommandRunner, Option } from 'nest-commander';
import { MarketplaceCatalogSyncCronCommand } from 'src/engine/core-modules/application/application-marketplace/crons/commands/marketplace-catalog-sync.cron.command';
import { StaleRegistrationCleanupCronCommand } from 'src/engine/core-modules/application/application-oauth/stale-registration-cleanup/commands/stale-registration-cleanup.cron.command';
@@ -66,10 +66,35 @@ export class CronRegisterAllCommand extends CommandRunner {
super();
}
async run(): Promise<void> {
this.logger.log('Registering all background sync cron jobs...');
private devMode = false;
const commands = [
@Option({
flags: '--dev-mode',
description:
'Only register cron jobs relevant to app development (cron triggers, marketplace sync, version check, stale cleanup)',
required: false,
})
parseDevMode(): boolean {
this.devMode = true;
return true;
}
private static readonly DEV_MODE_COMMANDS = new Set([
'CronTrigger',
'MarketplaceCatalogSync',
'ApplicationVersionCheck',
'StaleRegistrationCleanup',
]);
async run(): Promise<void> {
this.logger.log(
this.devMode
? 'Registering app-dev cron jobs...'
: 'Registering all background sync cron jobs...',
);
const allCommands = [
{
name: 'MessagingMessagesImport',
command: this.messagingMessagesImportCronCommand,
@@ -164,6 +189,12 @@ export class CronRegisterAllCommand extends CommandRunner {
},
];
const commands = this.devMode
? allCommands.filter(({ name }) =>
CronRegisterAllCommand.DEV_MODE_COMMANDS.has(name),
)
: allCommands;
let successCount = 0;
let failureCount = 0;
const failures: string[] = [];
@@ -38,6 +38,7 @@ import {
import { UserRoleService } from 'src/engine/metadata-modules/user-role/user-role.service';
import { WorkspaceCacheStorageService } from 'src/engine/workspace-cache-storage/workspace-cache-storage.service';
import { WorkspaceCacheService } from 'src/engine/workspace-cache/services/workspace-cache.service';
import { isApplicationAuthContext } from 'src/engine/core-modules/auth/guards/is-application-auth-context.guard';
export interface PageInfo {
hasNextPage?: boolean;
@@ -105,6 +106,11 @@ export abstract class RestApiBaseHandler {
}
roleId = userWorkspaceRoleId;
} else if (
isApplicationAuthContext(authContext) &&
isDefined(authContext.application.defaultRoleId)
) {
roleId = authContext.application.defaultRoleId;
} else {
throw new PermissionsException(
'Authentication context is invalid',