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
| Exported | Not exported |
|---|---|
| Resource servers and their scopes | Users and user types |
| Roles | Agents and mandates |
| Connections | Verifiable credentials, offers, issued credentials |
| Authenticators | Application shares and tenant grants |
| Applications, with their resource-server authorizations | Tenant signing keys |
| Flows | Sessions, tokens, consent, flow runs |
| Tenant settings | Secret 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-conflict | Behavior |
|---|---|
fail (default) | Abort, naming the colliding resource. Nothing is created. |
skip | Leave the existing resource alone, and point everything in the dump that referenced it at that one instead. |
overwrite | Update 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
| Method | Path | Scope |
|---|---|---|
GET | /t/{tenant}/export | tenant:export |
GET | /t/{tenant}/export/secrets | tenant:export |
POST | /t/{tenant}/import | tenant: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 *.