> Documentation index: https://docs.beetl.io/llms.txt

# Pack and upload

> Packing a Connect package, uploading it to a tenant, reading the build result, and removing a connection type.

Source: https://docs.beetl.io/connect/distribution/
Last updated: 2026-11-10

A Connect package reaches your tenant as one `.tgz` file. You create it with `beetl-connect pack`,
a TenantAdmin uploads it in Settings, and Beetl builds it into a connection type that anyone in
the tenant can create connections from.

## What the package must contain

`pack` refuses to produce an archive unless your project meets these rules:

* `package.json` sets `"type": "module"` and lists what to ship in an explicit `files` allowlist,
  without wildcards or broad entries such as `.`.
* `integration.ts` is included and default-exports the integration.
* `package-lock.json` matches `package.json`. `pack` adds it to the archive for you.
* Dependencies come from the public npm registry. Local paths, workspaces, Git dependencies and
  remote tarballs are not supported, and install scripts do not run during the build.
* The archive is at most 25 MiB, and at most 50 MiB unpacked.

It also refuses `.env*` files, the `.beetl` folder that local syncs write to, `node_modules`, and
`.ndjson` output.

For an icon in the app, put `icon.png` or `icon.webp` (up to 512 KiB) next to `integration.ts`,
set `icon: "icon.png"` in `defineIntegration`, and list the file in `files`:

```json
{
  "name": "acme-integration",
  "version": "1.0.0",
  "private": true,
  "type": "module",
  "files": ["integration.ts", "icon.png"],
  "devDependencies": {
    "@beetlio/connect": "^0.6.0"
  }
}
```

```sh
npx beetl-connect pack . --output acme.tgz
```

The command prints every file it included. Read that list before you upload.

## Upload the package

You need the TenantAdmin role. See [Tenants and roles](https://docs.beetl.io/concepts/tenants-and-roles).

    ## Add the package [#add-the-package]

    Go to Settings, switch to the **Tenant** view and open **Custom connection types**. Choose
    **Add custom connection type**, then drop the `.tgz` onto the upload area or click it to pick the
    file.

    ## Start the build [#start-the-build]

    Choose **Create custom connection type**. The page shows &#x2A;*Waiting for package build…**, then
    &#x2A;*Building and validating package…**, and returns you to the list once the build succeeds.

You can leave the page while the build runs. The list shows a **Building connection type** card
with a **Queued**, **Building** or **Build failed** badge. A finished type shows its display name,
its key, and **Ready**.

Only **Ready** types are offered when someone creates a connection. **Unsupported form** means the
app cannot draw a settings form from the package's input schemas; check them against the
[SDK configuration guide](https://beetlio.github.io/connect/#configuration) and upload again.

## When the build fails

The build installs the locked dependencies, compiles `integration.ts` with strict TypeScript
checks, loads the integration, and validates its definition and icon. If a step fails, the upload
page shows &#x2A;*The package build failed.** with the output under **Technical details**. On the list,
the same output is under **Build diagnostics**.

| Output mentions                                                            | What to fix                                                                                           |
| -------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------- |
| `TypeScript check failed`                                                  | A type error. The file, line and message follow.                                                      |
| `Integration dependencies` or `Could not install integration dependencies` | A dependency not from the public npm registry, or a stale lockfile. Run `npm install` and pack again. |
| `must default-export an integration`                                       | `integration.ts` lacks a default export of `defineIntegration(...)`.                                  |
| `does not contain a valid png image`                                       | The icon is damaged or does not match its file name.                                                  |

The local `configure` and `sync` commands compile your package the same way, so a sync that runs
on your machine catches most of these first. See [Local development](https://docs.beetl.io/connect/local-development).

To retry on the upload page, pack the fixed code, choose the new file and press
**Retry custom connection type**. If you left the page, delete the failed card and upload again.

## Changing a package later

Each upload through **Add custom connection type** installs a new, separate connection type, even
when the package keeps its key. Existing connections stay on the type they were created from.
Give the changed package a display name that sets it apart, so people pick the right entry when
they create a connection. Its syncs write to new datasets. For what a package's contract covers,
see [The contract rule](https://docs.beetl.io/connect/schema-change-contract).

## Rebuilding older packages

Beetl accepts only packages built with a current SDK (0.5 or later; the current release is 0.6).
A package built with an older SDK is rejected when Beetl reads or runs it, with this message:

```text
Unsupported integration version. Recreate this connection using a current integration package.
```

A connection created from such a package shows **Unsupported integration version** on Home.
To bring it forward:

1. Update the SDK with `npm install --save-dev @beetlio/connect@latest`.
2. In each sync, remove `mode`, `primaryKey` and `deletedKeys`. A sync no longer declares how it
   writes: the person who adds it chooses Append or Replace. See [Write modes](https://docs.beetl.io/connect/write-modes).
3. Run the sync locally, then pack and upload the package as a new connection type, as described
   above.
4. Create connections of the new type, add the syncs and choose a write mode for each. The old
   connection type, its connections and its datasets are not changed, so follow the steps in
   [The contract rule](https://docs.beetl.io/connect/schema-change-contract#ship-a-change) to move across.

A sync that merged rows by key becomes an Append or Replace sync, and Beetl no longer merges by key
inside a sync. Emit each record's stable ID and last-modified time, and let a
[pipeline](https://docs.beetl.io/pipelines/write-modes-and-partitioning) keep the latest row per ID.

## Delete a connection type

Choose the trash icon on the card and confirm. Beetl refuses while any connection or saved
credentials still use the type, and the error names them. Deleting a card that is still building
cancels its build.

## Create connections

Once the type is **Ready**, anyone in the tenant can right-click the canvas on Home, choose
**New connection** and pick it. See [Integrations](https://docs.beetl.io/sources/integrations).
