Skip to main content

Export & Import Bundles

Export/import moves fully-resolved configurations between tenants (or between deployments) as a single JSON file. The feature lives in apps/backend/modules/bundle/.

Bundle format (v1)

{
"bundleVersion": 1,
"generator": "jet-admin",
"exportedAt": "2026-08-23T10:00:00.000Z",
"sourceTenant": "<source tenant UUID>",
"items": [
{
"type": "appPage" | "widget" | "workflow" | "dataQuery" | "datasource" | "listener",
"id": "<original UUID — reference key only, never reused as a new PK>",
"payload": { /* structural fields only, see sanitization */ },
"dependencies": [
{ "type": "dataQuery", "id": "<uuid>", "name": "Users query", "bundled": true },
{ "type": "listener", "id": "<uuid>", "name": "Webhook", "bundled": false }
]
}
]
}

Rules:

  • Items are emitted dependencies-first; the importer re-derives the order, so hand-edited files still import.
  • IDs inside payloads (widgets[] keys, dataSources[].queryID, nodeConfig.dataQueryID, …) are kept as-is at export and rewritten at import through an id map.
  • dependencies[].bundled: false marks references that are not part of the bundle. They cannot be remapped on import and surface as MISSING warnings.

What gets walked

Export rootDependency closure
App pageits widgets → their queries/workflows; page queries/workflows/listeners; those workflows' queries → datasources
WidgetTRIGGER_QUERY / TRIGGER_WORKFLOW targets → their datasources
WorkflowdataQuery nodes → datasources
Data queryits datasource
Listenerits datasource + referenced workflows/queries

Sanitization rules

  • Stripped everywhere: tenantID, creatorID, createdByApiKeyID, createdAt, updatedAt, disabledAt, connectionStatus, nextRunAt.
  • Datasource payload: decrypted server-side, then every field whose key matches a sensitive pattern (utils/sensitive.js SENSITIVE_KEY_PATTERNS plus vaultCredentialID, webhookToken, webhookSecret) is removed. The payload carries "requiresReconnect": true.
  • Listener payload: status forced to inactive; runtime state (lastError, lastEventAt, eventCount) never exported; webhookToken/webhookSecret stripped (regenerated by the engine).
  • Import strips sensitive fields again — imported files are untrusted input.

Endpoints

GET /api/v1/tenants/:tenantID/app-pages/:appPageID/export
GET /api/v1/tenants/:tenantID/workflows/:workflowID/export
GET /api/v1/tenants/:tenantID/queries/:dataQueryID/export
GET /api/v1/tenants/:tenantID/widgets/:widgetID/export
GET /api/v1/tenants/:tenantID/listeners/:listenerID/export

POST /api/v1/tenants/:tenantID/import/preview body: { bundle }
POST /api/v1/tenants/:tenantID/import/execute body: { bundle }

Permissions: <entity>.export per root type, plus bundle.preview / bundle.execute.

Preview response

{
"valid": true,
"summary": { "createCount": 4, "warningCount": 2, "missingDependencyCount": 1 },
"items": [
{
"type": "appPage", "id": "...", "title": "Ops dashboard", "action": "create",
"warnings": ["A appPage named \"Ops dashboard\" already exists in this tenant."],
"missingDependencies": [{ "type": "listener", "id": "..." }]
}
]
}

Execute semantics

  • One transaction for the whole bundle; entities are created bottom-up (datasource → query → workflow → widget → listener/page).
  • Every entity gets fresh UUIDs; all internal references are remapped.
  • Listeners whose datasource is not resolvable are skipped with a reason instead of failing the whole import.
  • Listener endpointPath collisions are auto-suffixed (-2, -3).
  • Title collisions do not block; they are warned about during preview.
  • Creator access policies are granted after commit.

Frontend

  • Export: download button in each entity editor header (BundleExportButton), produces <title>-<type>-bundle.json.
  • Import: upload/paste dialog in each drawer list (BundleImportDialog) with preview → confirm flow.