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>
@@ -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
|
||||
|
||||
|
||||
@@ -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 ready‑to‑run project that works seamlessly with the [twenty-sdk](https://www.npmjs.com/package/twenty-sdk).
|
||||
|
||||
- Zero‑config 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,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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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,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 @@
|
||||
oneshot
|
||||
@@ -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"
|
||||
@@ -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."
|
||||
|
||||
@@ -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 | — |
|
||||
|
||||
|
After Width: | Height: | Size: 1.2 MiB |
|
After Width: | Height: | Size: 1.3 MiB |
|
After Width: | Height: | Size: 1.3 MiB |
|
After Width: | Height: | Size: 1.4 MiB |
|
After Width: | Height: | Size: 1.6 MiB |
|
After Width: | Height: | Size: 315 KiB |
|
After Width: | Height: | Size: 788 KiB |
|
After Width: | Height: | Size: 996 KiB |
|
After Width: | Height: | Size: 173 KiB |
|
After Width: | Height: | Size: 88 KiB |
@@ -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)
|
||||
- Built‑in 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,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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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}`;
|
||||
};
|
||||
@@ -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',
|
||||
|
||||