Skip to main content
Version: Next

Config Export & Import

o2idctl export writes a tenant's configuration to a portable JSON document; o2idctl import re-creates it in another tenant, on the same instance or a different one. Use it to promote a configuration from staging to production, to ship a starter kit of flows and authenticators, or to keep a tenant's configuration in version control.

# On the source instance
o2idctl export --out dump.json

# On the target instance, against an existing tenant
o2idctl import --in dump.json

Both commands act on whichever tenant the selected profile targets — see o2idctl config, or set O2IDCTL_TENANT for a single invocation. The target tenant must already exist (o2idctl tenants create or o2idctl organizations create).

What's in a dump

ExportedNot exported
Resource servers and their scopesUsers and user types
RolesAgents and mandates
ConnectionsVerifiable credentials, offers, issued credentials
AuthenticatorsApplication shares and tenant grants
Applications, with their resource-server authorizationsTenant signing keys
FlowsSessions, tokens, consent, flow runs
Tenant settingsSecret values (see below)

Resources every tenant seeds for itself are skipped — O2ID's own management-API resource servers and the o2idctl application — because the target already has its own.

Users are deliberately out of scope: a dump is a configuration bootstrap, not a migration of identities. Create people in the target with the users API, or let them arrive through an external IdP connection.

Secrets

A dump carries no secret values, so it's safe to commit or hand to a partner. Connection and authenticator secrets are stored encrypted under a key derived from the instance's root key and the tenant's ID, so their ciphertext is meaningless anywhere else.

To carry them across, ask for them explicitly:

o2idctl export --out dump.json --include-secrets
# writes dump.json and dump.secrets.json

o2idctl import --in dump.json --secrets dump.secrets.json

dump.secrets.json holds decrypted credentials in plaintext. Treat it like a .env file: transmit it deliberately and delete it once the import is done. Import decrypts nothing itself — it hands the plaintext to the ordinary create path, which encrypts it under the target's key.

Import without secrets still works. Each connection or authenticator that ends up missing credentials is listed under credentialsRequired in the import's output; supply them before using it:

o2idctl connections update <id> --field clientSecret=...

Application client secrets can never be exported — O2ID stores only their hash. Every application an import creates gets a freshly generated secret, returned once in the import's output under clientSecrets. An application that's overwritten keeps the secret it already had.

IDs and references

Imported resources get new IDs, and every reference in the dump is rewritten to match: an authenticator's connection, and the connection or authenticator a flow node names. A flow node's trigger needs no rewriting, so flows are imported in an order that puts a callee before its caller.

Application client IDs are kept as they are — a client ID is unique per tenant, it's what a flow's allowedClientIds names, and it's what deployed client configuration points at.

A flow whose definition doesn't validate in the target — most often a connection node whose connection arrived without credentials — is imported inactive and reported under flowValidation. Fix what the errors name, then activate it:

o2idctl flows validate <id>
o2idctl flows activate <id>

Conflicts

A resource in the dump collides when the target already has one with the same natural key: a resource server's identifier, a role's, connection's or flow's name, an authenticator's display name, an application's client ID.

--on-conflictBehavior
fail (default)Abort, naming the colliding resource. Nothing is created.
skipLeave the existing resource alone, and point everything in the dump that referenced it at that one instead.
overwriteUpdate the existing resource in place.

overwrite never deletes what the dump doesn't mention (an extra scope on a resource server stays), and never changes an application's client secret or whether it's confidential.

Tenant settings are a single row with no name to collide on, so they're always applied — except under skip, which leaves settings the target already has.

Promoting into a tenant that was created with an admin (--admin-username) usually wants --on-conflict skip, since both tenants have an Admin role:

o2idctl import --in dump.json --on-conflict skip

All or nothing

An import runs as a single database transaction. If any part of it fails — a collision under fail, an unknown connector type, a database error — nothing at all is created, and the target tenant is exactly as it was.

Output

import prints what it did, per resource type, with the old-to-new ID mapping:

{
"resources": {
"connections": {
"created": 1,
"ids": { "9f2c…": "41ab…" }
},
"flows": { "created": 2 }
},
"clientSecrets": { "storefront": "…" },
"credentialsRequired": [
{
"resourceType": "connections",
"id": "41ab…",
"name": "Corporate IdP",
"fields": ["clientSecret"]
}
]
}

Worked example: staging to production

# 1. Export staging, credentials and all.
o2idctl config use staging
o2idctl export --out prod-seed.json --include-secrets

# 2. Create the production tenant (on the production instance).
o2idctl config use production
o2idctl tenants create --slug acme --name Acme --admin-username admin

# 3. Import into it.
O2IDCTL_TENANT=acme o2idctl import \
--in prod-seed.json \
--secrets prod-seed.secrets.json \
--on-conflict skip

# 4. Record the new client secrets from the output, then delete the
# secrets file.
rm prod-seed.secrets.json

API

MethodPathScope
GET/t/{tenant}/exporttenant:export
GET/t/{tenant}/export/secretstenant:export
POST/t/{tenant}/importtenant:import

POST /import takes {"dump": …, "secrets": …, "onConflict": "fail"} and returns the summary above. A collision under fail is a 409.

tenant:export reaches every configured secret in the tenant, and tenant:import writes every resource type in it — grant both as carefully as you would *.