Skip to content

CDM Import & Export

CDM (Canonical Data Model) is a versioned JSON document that captures a project’s permission graph—roles, groups, permissions, resources, tags, and optional users. Use it to clone, back up, or port a model between Grant instances or from an external system.

Operators typically work from Project → Import/Export in the dashboard. Each import or export runs as an async job; large payloads are never applied inline.

How it works

You start work from Import/Export. Grant runs the job in the background while the jobs list keeps updating; when it finishes, open the job to review tabs or download a file.

Import

  • mode in the file sets merge (default) or replace; replace requires confirmDestructive.
  • Only CDM-managed rows are updated; other project data is untouched.
  • Domain events: entity-level events (role.created, user.role_assigned, …) are suppressed for the duration of importProjectCdm (including replace teardown) so imports do not flood webhooks or in-app notifications. When the async job finishes, Grant emits a single summary event — project_sync.completed or project_sync.failed — with job/project ids, operation (import | export), and import counters or a short error message. See Events, Webhooks & Notifications.

Export

  • Contents chooses what goes in the file (e.g. permissions requires resources).
  • Options are embedded in the download as mode; they do not change live data now. Job Details shows the same strategy and version—there is no separate options tab.

Payload shape

Imports and exports use SyncProjectInput (GraphQL / codegen). Top-level fields:

FieldPurpose
versionSchema version (currently 1)
idOptional correlation id for audit / idempotency
modeImport policy: strategy, onConflict, confirmDestructive
resourcesProject-scoped custom resources + membership
permissionsProject-scoped custom permissions + membership
groupsGroup definitions and permission grouping
rolesRoles with groups, permissions, tags
usersUser definitions / assignments + optional userKey
tagsTag definitions + project_tags membership

Section rules

  • Every section is optional; omit a section to leave it unchanged (export) or skip it (import).
  • If permissions is present, resources must be present in the same document.
  • Grant does not auto-generate synthetic roles. Use the explicit CDM fields below for each grant path.
  • user_tags are global—re-importing them affects every project the user belongs to. See Tags & Relationships → CDM lifecycle.

Permission assignment paths (v1)

Each path in the document maps to native Grant edges on import:

CDM fieldsRuntime path
groups[].permissions + roles[].groups + users[].rolesUser → Role → Group → Permission
groups[].permissions + users[].groupsUser → Group → Permission
roles[].permissions (no roles[].groups) + users[].rolesUser → Role → Permission
users[].permissionsUser → Permission

Export emits the same fields. Users may have roles only, direct permissions only, direct groups only, or any combination.

Legacy documents: if users[].roles contains synthetic:role:user:…:direct, import strips those keys and adds a job warning. Use users.permissions instead.

Replace-import removes existing CDM-managed rows for the project, then applies the payload. See RBAC for how roles, groups, and permissions connect.

Global catalog vs CDM-owned

Global catalogCDM-owned rows
WhatSeeded resources / permissions shared by all projectsRows with metadata.cdmImport for this project
In export JSONReferenced as resourceSlug:action strings; resources / permissions sections often emptyListed in CDM sections when custom definitions exist
On editChanges affect every project using that catalog rowScoped to the project’s CDM import envelope
IsolationNot isolated per project today

Imports resolve standard grants via permissionRefs (resourceSlug + action [+ condition]). CDM resources / permissions arrays list only project-created definitions (metadata.cdmImport), not the full system catalog.

INFO

Strict per-project catalog isolation (forking global permissions so edits never leak across projects) is not shipped. Treat catalog rows as instance-wide until a portable-catalog mode exists. See Future: portable catalog below.

Permission strings in roles and users

Each roles[].permissions or users[].permissions entry is either:

FormExampleWhen
Document keyMatches permissions[].key in the same fileCustom permission defined in CDM
Catalog refproject:updateSlug + action; slug must not contain :

Slugs are normalized (lowercase, hyphenated). Export emits slug:action for catalog-backed grants even when resources / permissions sections are empty.

External keys

Every CDM-portable entity has an opaque externalKey:

SourceRule
ImporterYou supply the key; must be unique within its section
ExporterGrant generates stable keys like cdm-tag-3f2a9b1c… via buildExternalKey(kind, …)
TraceabilityOriginal Grant ids live in metadata.cdmSource (grantRoleId, grantTagId, …)—never as cross-references

Users: reference an existing account with userAssignments[].userId, or define users[] and link via userAssignments[].userKeyusers[].externalKey.

Resolving permission refs (in roleTemplates / userAssignments), in order:

  1. permissionKey → permission in the same document
  2. permissionId → existing Grant UUID (legacy; new exports omit this)
  3. (resourceSlug, action [, condition]) → global catalog

Provisioned users

The optional users section creates or resolves global Grant users for inbound porting, then the assignment fields (roles, groups, or permissions) add project membership. CDM user imports never create Grant accounts, sessions, passwords, OTPs, or outgoing verification emails.

User keys support two important modes:

ResolverBehavior
findBy: idkey.value is an existing Grant user id. Import skips global user provisioning and uses that user directly.
findBy: key/emptykey.value is a CDM-local external key. Grant creates or reuses a user with metadata.cdmImport for this project.
findBy: emailkey.value is normalized to lowercase and resolved against the global authentication catalog. If no identity exists, Grant creates a global user plus a passwordless email authentication method with isVerified: false.

For findBy: email, import treats the address as an identity claim, not proof of mailbox ownership. The email method reserves/links the global identity but remains unverified until Grant sees a later inbox-proof flow, such as project OAuth email magic link.

Export remains conservative: CDM-managed users are emitted with opaque external keys unless a future export mode explicitly opts into exposing global login identifiers.

Schema reference: SyncProjectInput.

Dashboard permissions

ActionPermissionNotes
Start importProject:UpdateEnqueues async job
Start exportProject:UpdateEnqueues async job
View jobsProject:QueryList and detail
Cancel jobProject:UpdateBest-effort if already running

Scope must match the project (organization or personal), same as other project screens.

Web UI

  1. Open Project → Import/Export (…/projects/{projectId}/import-export).
  2. Import — pick a .json file; validated, then enqueued. Original payload stored on the job.
  3. Export — dialog with Contents and Options tabs (below). Download CDM from the job row when complete.
  4. The list polls while any job is PENDING or RUNNING.
  5. Job details — status on Details; import adds Result, Payload, Rollback snapshot; export adds Exported CDM.

Export dialog

The export dialog enqueues a job. It does not change live data—the worker snapshots the database when the job runs.

TabPurpose
ContentsWhich CDM sections to include
OptionsRe-import defaults embedded in exported mode (hints only)

Contents

CheckboxNotes
UsersOptional; nested User API keys (identities only, no secrets)
Roles, Groups, Resources, Permissions, TagsOmit to exclude from JSON
PermissionsRequires Resources checked
  • Selecting every section = full export (same as omitting sections filter).
  • Exporting users without roles is valid when users have direct permissions or groups assignments.

Re-import options

Values are stored in the job payload and copied into the artifact’s mode block. They describe how a future import of this file should behave—they do not run merge/replace during export.

The jobs table Strategy column shows the chosen mode for quick scanning.

Job name

Optional label for the jobs table and idempotency (StartProjectExportInput.jobName). Empty → defaults to project display name.

RuleBehavior
Same name, job in flightReturns existing pending/running job
After completionMay enqueue again with the same name
Failed / cancelledRetry with the same name allowed

Version

Schema version written into the export (version, currently 1). Must match importer support.

Strategy

ValueOn import
mergeApply alongside existing CDM-managed rows (default)
replaceRemove CDM-managed rows, then apply; requires confirm destructive

On conflict

Maps to mode.onConflict when set: fail, skip, or update. Importer default omits the field (typically fail for merge).

Confirm destructive

Required when strategy is replace (mode.confirmDestructive: true in JSON). Acknowledges that a future replace import may delete CDM-managed data. Does not delete anything during export.

Sync jobs

Each operation is a row in project_sync_jobs (operation = import | export), run by the job adapter.

StateMeaning
pendingQueued
runningWorker active
completedSuccess
failedError on job row
cancelledOperator or system cancelled

Idempotency — optional jobName (export) or CDM id (import):

ImportExport
KeySyncProjectInput.id (often omitted)jobName (defaults to project name)
In-flight duplicateReturns same job per (project, operation, job_name)Same
After completeNew upload = new row if no idSame name allowed again
  • Poll — GraphQL projectSyncJobs / projectSyncJob, or REST equivalents.
  • PayloadGET …/sync/jobs/{jobId}/payload (submitted import JSON or export options).

Rollback snapshot

Before an import, the worker exports the full project CDM inside the same transaction, then applies the import. If the import fails, the snapshot rolls back with the transaction.

SurfaceContent
GraphQL job fieldshasSnapshot, snapshotTakenAt, snapshotSizeBytes
GET …/snapshotFull JSON bytes (not inlined in GraphQL)

TIP

There is no one-click “rollback” in the UI yet. Download the snapshot and start a new import to replay. Future secret-bearing CDM fields (e.g. BYOK) would need secrets re-supplied on replay.

REST API

Routes under /api/projects/{id}/… with scopeId and tenant (see REST API).

MethodPathRole
GET…/sync/jobsList jobs
POST…/sync/jobsEnqueue import (body = CDM JSON)
POST…/sync/jobs/exportEnqueue export
GET…/sync/jobs/{jobId}Job status
GET…/sync/jobs/{jobId}/payloadRequest JSON (import CDM or export options)
GET…/sync/jobs/{jobId}/snapshotImport: pre-sync snapshot; export: generated CDM when completed
DELETE…/sync/jobs/{jobId}Cancel

GraphQL

OperationPurpose
startProjectSyncEnqueue import
startProjectExportEnqueue export (sections, mode, jobName, …)
cancelProjectSyncCancel job
projectSyncJobsPaginated list
projectSyncJobSingle job

Operation documents live in packages/@grantjs/schema for codegen.

Searchable metadata

List-view text search uses denormalized search_document columns (not raw JSONB scans). Importers can supply explicit search tokens via the top-level CDM field searchable on users[], roles[], and groups[].

EntityStorageList search
Project usersproject_users.search_documentProject-scoped user lists
Rolesroles.search_documentRole lists (searchDocument field)
Groupsgroups.search_documentGroup lists (searchDocument field)

On import, searchable is merged into metadata.cdmSource.searchable for export round-trip. Grant also extracts allowlisted metadata paths (e.g. cdmSource.legacy.email) when searchable is absent.

Never indexed: passwordHash, clientSecret, accessKeySecretReferenceId, raw permissionJson, and similar secret fields.

Backfill: Re-run a CDM replace import after deploying Grant + ETL with searchable support; pivot and entity rows are recomputed on apply.

Row metadata

CDM-managed rows carry:

FieldRole
metadata.cdmImportprojectId, kind, externalKey — teardown, export filter, correlation
metadata.cdmSourceTrace ids (grantRoleId, grantUserId, …) and importer-owned JSON

Contributor details: CDM README in the API tree.

Future: portable catalog

Copying global catalog permissions per project (so edits never cross projects) would need coordinated changes to resolution, export, and slug rules. Not shipped—plan against global catalog semantics until such a mode exists.

Extending CDM (contributors)

New entity types register via ICdmEntityHandler (shared by sync and export). Handler order, cdmVersion bumps, and tests are documented in:


Related

Released under the MIT License.