---
title: Publishing
icon: "upload"
description: Distribute your Twenty app to the marketplace or deploy it internally.
---

## Overview

Once your app is [built and tested locally](/developers/extend/apps/getting-started/concepts), you have two paths for distributing it:

- **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

Run the build command to compile your app and generate a distribution-ready `manifest.json`:

```bash filename="Terminal"
yarn twenty dev:build
```

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 publish command.

## 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
```

### Deploying

Build and upload your app to the server in one step:

```bash filename="Terminal"
yarn twenty app:publish --private
# To deploy to a specific remote:
# yarn twenty app:publish --private --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.

### Version management

When updating an already deployed tarball app, the server requires the `version` in `package.json` to be **strictly higher** (per [semver](https://semver.org) ordering) than the currently deployed version. Re-deploying the same version, or pushing a lower one, is rejected before the tarball is stored — you'll see a `VERSION_ALREADY_EXISTS` error from the CLI.

To release an update:

1. Bump the `version` field in your `package.json` (e.g. `1.2.3` → `1.2.4`, `1.3.0`, or `2.0.0`)
2. Run `yarn twenty app:publish --private` (or `yarn twenty app:publish --private --remote production`)
3. Workspaces that have the app installed and enabled auto-upgrade for it (in the app's Settings tab) are upgraded automatically in the background; the others will see the upgrade available in their settings

<Note>
Pre-release tags work as expected: bumping `1.0.0-rc.1` → `1.0.0-rc.2` is allowed, and a final release like `1.0.0` is correctly recognized as higher than `1.0.0-rc.5`. The version in `package.json` must itself be a valid semver string.
</Note>

{/* TODO: add screenshot of the Upgrade button */}

### Server version compatibility

If your app uses a feature introduced in a specific Twenty server version (for example, OAuth providers added in v2.3.0), you should declare the minimum server version your app requires using the `engines.twenty` field in `package.json`:

```json filename="package.json"
{
  "name": "twenty-my-app",
  "version": "1.0.0",
  "engines": {
    "node": "^24.5.0",
    "twenty": ">=2.3.0"
  }
}
```

The value is a standard [semver range](https://github.com/npm/node-semver#ranges). Common patterns:

| Range | Meaning |
|-------|---------|
| `>=2.3.0` | Any server from 2.3.0 onward |
| `>=2.3.0 <3.0.0` | 2.3.0 or later, but below the next major |
| `^2.3.0` | Same as `>=2.3.0 <3.0.0` |

**What happens at deploy and install time:**

- If `engines.twenty` is set and the target server's version does not satisfy the range, the deploy (tarball upload) or install is rejected with a `SERVER_VERSION_INCOMPATIBLE` error and a message indicating both the required range and the actual server version.
- If `engines.twenty` is **not set**, the app is accepted on any server version (backward-compatible with existing apps).
- If the server has no `APP_VERSION` configured, the check is skipped.

<Note>
The server is the authoritative check — it validates `engines.twenty` on both tarball upload and workspace install. If you deploy a tarball out-of-band or install from the marketplace, the server still enforces compatibility.
</Note>

## Automated CI/CD (scaffolded workflows)

Apps generated with `create-twenty-app` ship with three GitHub Actions workflows out of the box, under `.github/workflows/`. CI runs with no setup, CD requires a single secret, and publishing to npm requires a one-time npm trusted-publisher setup.

### CI — `ci.yml`

Runs integration tests on every push to `main` and every pull request.

**What it does:**

1. Checks out your app's source.
2. Spawns an isolated Twenty test instance using the `twentyhq/twenty/.github/actions/spawn-twenty-app-dev-test@main` composite action (the CI equivalent of `yarn twenty docker:start --test`).
3. Enables Corepack, sets up Node.js from your `.nvmrc`, and installs dependencies with `yarn install --immutable`.
4. Runs `yarn test`, passing `TWENTY_API_URL` and `TWENTY_API_KEY` from the spawned instance so your tests can talk to a real server.

**Config knobs:**

- `TWENTY_VERSION` (env, defaults to `latest`) — pin the Twenty server version used in CI by editing this in `ci.yml`.
- Concurrency is grouped by `github.ref` and cancels in-progress runs on new pushes.

No secrets are required — the test instance is ephemeral and lives only for the duration of the job.

### CD — `cd.yml`

Deploys your app to a configured Twenty server on every push to `main`, and optionally from a pull request when the `deploy` label is applied.

**What it does:**

1. Checks out the PR head (for labeled PRs) or the pushed commit.
2. Runs `twentyhq/twenty/.github/actions/deploy-twenty-app@main` — the CI equivalent of `yarn twenty app:publish --private`.
3. Runs `twentyhq/twenty/.github/actions/install-twenty-app@main` so the newly deployed version is installed into the target workspace.

**Required configuration:**

| Setting | Where | Purpose |
|---------|-------|---------|
| `TWENTY_DEPLOY_URL` | `env` in `cd.yml` (defaults to `http://localhost:3000`) | The Twenty server to deploy to. Change this to your real server URL before first use. |
| `TWENTY_DEPLOY_API_KEY` | GitHub repo **Settings → Secrets and variables → Actions** | API key with deploy permission on the target server. |

<Note>
The default `TWENTY_DEPLOY_URL` of `http://localhost:3000` is a placeholder — it will not reach anything from a GitHub-hosted runner. Update it to your server's public URL (or use a self-hosted runner with network access) before enabling CD.
</Note>

**Triggering a preview deploy from a PR:**

Add the `deploy` label to a pull request. The `if:` guard in `cd.yml` will run the job for that PR using the PR's head commit, letting you validate a change on the target server before merging.

### Publish — `publish.yml`

Publishes your app to npm with provenance when you push a version tag (e.g. `v1.0.0`), or when you run the workflow manually from the Actions tab.

**What it does:**

1. Checks out your app, sets up Node.js, and updates npm (trusted publishing requires npm 11.5.1 or later).
2. Runs `yarn twenty app:publish`, which builds the app and publishes `.twenty/output` to npm. In CI it automatically adds `--provenance` and `--access public`, so no flags are needed in the workflow.

**One-time setup:**

On npmjs.com open your package > **Settings → Trusted Publisher** and register this repository with the `publish.yml` workflow (see the [npm trusted publishing docs](https://docs.npmjs.com/trusted-publishers)). Publishing with provenance certifies which GitHub repository built the package, which is also how you claim ownership of your app in a Twenty marketplace.

<Note>
npm only accepts provenance from **public** source repositories. If you publish from a private repo, npm rejects the OIDC provenance bundle with an `E422 ... Unsupported GitHub Actions source repository visibility: "private"` error. To publish from a private repo, opt out of provenance by setting `TWENTY_APP_PUBLISH_DISABLE_PROVENANCE: 'true'` in the publish step's `env` (a commented-out hint is included in the scaffolded `publish.yml`):

```yaml filename=".github/workflows/publish.yml"
      - name: Publish to npm
        env:
          TWENTY_APP_PUBLISH_DISABLE_PROVENANCE: 'true'
        run: yarn twenty app:publish
```
</Note>

### Pinning the reusable actions

The `ci.yml` and `cd.yml` workflows reference reusable actions at `@main`, so action updates in the `twentyhq/twenty` repo are picked up automatically. If you want deterministic builds, replace `@main` with a commit SHA or release tag on each `uses:` line.

## 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.

### Requirements

- An [npm](https://www.npmjs.com) account
- The `twenty-app` keyword in your `package.json` `keywords` array (add it manually — it is not included by default in the `create-twenty-app` template)

```json filename="package.json"
{
  "name": "twenty-app-postcard-sender",
  "version": "1.0.0",
  "keywords": ["twenty-app"]
}
```

### Marketplace metadata

The `defineApplication()` config supports optional fields that control how your app appears in the marketplace. Use `logo` and `galleryImages` to reference images from the `public/` folder:

```ts src/application-config.ts
export default defineApplication({
  universalIdentifier: '...',
  displayName: 'My App',
  description: 'A great app',
  logo: 'public/logo.png',
  galleryImages: [
    'public/screenshot-1.png',
    'public/screenshot-2.png',
  ],
});
```

See the [defineApplication accordion](/developers/extend/apps/config/application#marketplace-metadata) in the Building Apps page for the full list of marketplace fields (`author`, `category`, `aboutDescription`, `websiteUrl`, `termsUrl`, etc.).

#### Recommended gallery image dimensions

The marketplace renders `galleryImages` in a fixed `8:5` container (for example, `1600×1000 px`).

<Note>
Gallery images of any aspect ratio are displayed in full and are never cropped, but anything significantly taller or narrower than `8:5` will show empty bands on the sides.
</Note>

#### Image size limit

The `logo` and each `galleryImages` file must not exceed **10 MB**. Larger files are skipped when the marketplace rehosts your published assets, so they will not be displayed.

### Publish

```bash filename="Terminal"
yarn twenty app:publish
```

To publish under a specific dist-tag (e.g., `beta` or `next`):

```bash filename="Terminal"
yarn twenty app:publish --tag beta
```

### How marketplace discovery works

The Twenty server syncs its marketplace catalog from the npm registry **every hour**.

You can trigger the sync immediately instead of waiting:

```bash filename="Terminal"
yarn twenty dev:catalog-sync
# To target a specific remote:
# yarn twenty dev:catalog-sync --remote production
```

The metadata shown in the marketplace comes from your `defineApplication()` config — see [Marketplace metadata](#marketplace-metadata) above.

<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`.
</Note>

### CI publishing

The scaffolded `publish.yml` workflow described above publishes to npm automatically on version tags, with provenance. Because `yarn twenty app:publish` adds `--provenance` and `--access public` for you when it runs in CI, the workflow needs no npm flags — only the one-time trusted-publisher setup.

For other CI systems (GitLab CI, CircleCI, etc.), run `yarn install` then `yarn twenty app:publish`. Provenance is emitted when the environment can mint an OIDC token and skipped automatically otherwise.

<Note>
**npm 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. It is also what lets you claim ownership of your app in a Twenty marketplace. See the [npm provenance docs](https://docs.npmjs.com/generating-provenance-statements) for details.
</Note>

## Installing apps

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 app:install
```

<Note>
The server enforces semver versioning on install, mirroring the rules on deploy:

- Installing the same version that is already installed in your workspace is rejected with an `APP_ALREADY_INSTALLED` error.
- Installing a lower version than the one currently installed is rejected with a `CANNOT_DOWNGRADE_APPLICATION` error.

To install a newer version, deploy or publish it first, then re-run `yarn twenty app:install`.
</Note>
