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: falsemarks references that are not part of the bundle. They cannot be remapped on import and surface as MISSING warnings.
What gets walked
| Export root | Dependency closure |
|---|---|
| App page | its widgets → their queries/workflows; page queries/workflows/listeners; those workflows' queries → datasources |
| Widget | TRIGGER_QUERY / TRIGGER_WORKFLOW targets → their datasources |
| Workflow | dataQuery nodes → datasources |
| Data query | its datasource |
| Listener | its 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.jsSENSITIVE_KEY_PATTERNS plusvaultCredentialID,webhookToken,webhookSecret) is removed. The payload carries"requiresReconnect": true. - Listener payload: status forced to
inactive; runtime state (lastError,lastEventAt,eventCount) never exported;webhookToken/webhookSecretstripped (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
endpointPathcollisions 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.