Overview
Once your app is built and tested locally, 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 — publish your package, claim it in Twenty, and complete its setup so other workspaces can discover and install it.
Registration ownership
Publishing an app creates an application registration: the app’s identity on the instance, shared by every workspace that installs it. It holds the app’s marketplace entry, its OAuth client, and its server-side state — theserverVariables an operator configures and the server-scoped entries of the key-value store.
A universalIdentifier is unique across the whole instance and a registration belongs to at most one workspace (its owner) — or none until it is claimed — so installing an app and developing one are two different rights:
Syncing code and a manifest under an identifier is a claim of authorship, so being able to install an app never grants the right to publish code under its identity. A workspace that is not the owner is refused, whether the app is listed, pre-installed, or private.
Two situations lead to that refusal:
- The identifier belongs to another workspace. Change the
universalIdentifierin your app config, which is the usual answer when you forked someone else’s app. If you need to keep the identifier, the owning workspace has to transfer the registration to yours. - The identifier belongs to no workspace. Apps imported by the marketplace catalog sync start unowned. Claim yours with GitHub before developing on it. Publishing with npm provenance provides proof of authorship; it does not automatically assign an owner workspace in Twenty.
Building your app
Run the build command to compile your app and generate a distribution-readymanifest.json:
.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:
Deploying
Build and upload your app to the server in one step: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:- Go to Settings > Applications > Registrations and open your app
- In the Distribution tab, click Copy share link
- Share this link with users on other workspaces — it takes them directly to the app’s install page
Version management
When updating an already deployed tarball app, the server requires theversion in package.json to be strictly higher (per semver 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:
- Bump the
versionfield in yourpackage.json(e.g.1.2.3→1.2.4,1.3.0, or2.0.0) - Run
yarn twenty app:publish --private(oryarn twenty app:publish --private --remote production) - Workspaces that have the app installed and enabled auto-upgrade for it (in the app’s General tab) are upgraded automatically in the background; the others will see the upgrade available in their settings
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.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 theengines.twenty field in package.json:
What happens at deploy and install time:
- If
engines.twentyis set and the target server’s version does not satisfy the range, the deploy (tarball upload) or install is rejected with aSERVER_VERSION_INCOMPATIBLEerror and a message indicating both the required range and the actual server version. - If
engines.twentyis not set, the app is accepted on any server version (backward-compatible with existing apps). - If the server has no
APP_VERSIONconfigured, the check is skipped.
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.Automated CI/CD (scaffolded workflows)
Apps generated withcreate-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:
- Checks out your app’s source.
- Spawns an isolated Twenty test instance using the
twentyhq/twenty/.github/actions/spawn-twenty-app-dev-test@maincomposite action (the CI equivalent ofyarn twenty docker:start --test). - Enables Corepack, sets up Node.js from your
.nvmrc, and installs dependencies withyarn install --immutable. - Runs
yarn test, passingTWENTY_API_URLandTWENTY_API_KEYfrom the spawned instance so your tests can talk to a real server.
TWENTY_VERSION(env, defaults tolatest) — pin the Twenty server version used in CI by editing this inci.yml.- Concurrency is grouped by
github.refand cancels in-progress runs on new pushes.
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:
- Checks out the PR head (for labeled PRs) or the pushed commit.
- Runs
twentyhq/twenty/.github/actions/deploy-twenty-app@main— the CI equivalent ofyarn twenty app:publish --private. - Runs
twentyhq/twenty/.github/actions/install-twenty-app@mainso the newly deployed version is installed into the target workspace.
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.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:
- Checks out your app, sets up Node.js, and updates npm (trusted publishing requires npm 11.5.1 or later).
- Runs
yarn twenty app:publish, which builds the app and publishes.twenty/outputto npm. In CI it automatically adds--provenanceand--access public, so no flags are needed in the workflow.
publish.yml workflow (see the npm trusted publishing docs). Publishing with provenance certifies which GitHub repository built the package. After publishing, claim ownership in Twenty.
npm only accepts provenance from public source repositories. If you publish from a private repo, npm rejects the OIDC provenance bundle with an Without GitHub provenance, the package cannot be claimed through Claim with GitHub. Use a public repository with provenance if you need this ownership flow.
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):Pinning the reusable actions
Theci.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 eligible for the Twenty marketplace. After catalog sync and any required server-route setup, workspaces can browse, install, and upgrade it from the UI. Community apps appear when users turn off the Vetted only filter.Requirements
- An npm account
- The
twenty-appkeyword in yourpackage.jsonkeywordsarray (add it manually — it is not included by default in thecreate-twenty-apptemplate)
Marketplace metadata
ThedefineApplication() config supports optional fields that control how your app appears in the marketplace. Use logo and galleryImages to reference images from the public/ folder:
src/application-config.ts
author, category, aboutDescription, websiteUrl, termsUrl, etc.).
Recommended gallery image dimensions
The marketplace rendersgalleryImages in a fixed 8:5 container (for example, 1600×1000 px).
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.Image size limit
Thelogo 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
beta or next):
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:defineApplication() config — see Marketplace metadata above.
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.The About tab renders markdown text only. Images do not render, whether they use markdown syntax or <img> tags, and raw HTML such as badges or <div align="center"> blocks shows as plain text.CI publishing
The scaffoldedpublish.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.
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 for details.
Claiming your app
Claiming assigns your app’s registration to the Twenty workspace that will manage it. App claiming is available to all workspaces. Your role needs the Applications and MCP & APIs permissions to claim through the Developer tab. Ownership is per Twenty instance: claiming on Twenty Cloud does not claim the app on a separate self-hosted server. Other workspaces on the same instance install your app without claiming it themselves.Before you claim
- Publish your package from a public GitHub repository using GitHub Actions and npm trusted publishing with provenance. The version imported into Twenty’s catalog must carry the provenance attestation.
- Include the
twenty-appkeyword inpackage.jsonand wait for catalog sync. - Choose the workspace that will own the app. For apps with server routes, this workspace also runs and pays for the shared resolver.
- Sign in to GitHub as the personal account that owns the source repository, or as an owner of the organization that owns it. Repository collaborator access or npm package maintainer access alone is not sufficient.
Claim with GitHub
- In your intended owner workspace, go to Settings > Applications > Developer (
/settings/applications#developer). - In Claim an application, enter the exact npm package name, including its scope (for example,
@your-org/twenty-app), or the app’suniversalIdentifier. - Click Look up and check that the result is your app. If it is not found, click Sync catalog if available, wait for the sync to finish, then look it up again.
- Click Claim with GitHub and authorize Twenty. For an organization-owned repository, grant access to that organization in GitHub’s Organization access section. Personal repositories do not need an organization grant.
- When you return to Twenty, confirm that the app appears under My apps with type NPM. Open it and check Distribution > Ownership for Claimed by this workspace.
Setting up server routes
A server route receives events for multiple workspaces at one URL. Its resolver runs in the registration’s owner workspace, then dispatches work to the appropriate installed workspace.- Open your app under My apps and fill in any required Server Variables in the Config tab.
- In Distribution, click See on marketplace to open the app page and install it in the owner workspace. You can open this page even before the app appears in marketplace search.
- Register the server webhook URL with your provider and test an event from a second workspace where the app is installed. Check both the resolver and the target function’s logs.
Owner workspace costs and availability
See how logic function runs are metered for credit consumption, pricing, and which workspace is billed. Uninstalling the app from the owner workspace or deleting that workspace removes the shared resolver and interrupts server webhooks for every install on that instance.Ownership after a repository move
Moving the GitHub repository to another personal account or organization does not transfer or remove an existing Twenty claim. The registration remains attached to its owner workspace. Keep the same npm package and appuniversalIdentifier; changing the Twenty owner workspace requires a separate ownership transfer.
Troubleshooting a claim
If you have trouble claiming your app, contact us on Discord or open a GitHub issue.Self-hosted GitHub claim configuration
Self-hosted GitHub claim configuration
The server administrator configures
APP_CLAIM_GITHUB_CLIENT_ID and APP_CLAIM_GITHUB_CLIENT_SECRET with a GitHub OAuth app. Set its authorization callback URL to your SERVER_URL followed by /application-registration-claim/github/callback, for example https://twenty.example.com/application-registration-claim/github/callback. This configuration is already managed for you on Twenty Cloud.Marketplace visibility and vetting
Claiming establishes ownership; it does not grant a Vetted badge. The marketplace defaults to Vetted only. To find community apps, go to Settings > Applications > Marketplace, open the filter, and uncheck Vetted only. You can also share your app’s direct install page using My apps > your app > Distribution > Copy sharing link. The Vetted label currently applies to apps maintained by Twenty, for which the team can vouch for security and quality. Third-party apps can be published and installed without this label. If your app is still missing with Vetted only turned off, confirm catalog sync has completed, all required server variables are filled in, and any server-route setup is complete.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. You can also install apps from the command line: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_INSTALLEDerror. - Installing a lower version than the one currently installed is rejected with a
CANNOT_DOWNGRADE_APPLICATIONerror.
yarn twenty app:install.