# Import / Export & Migration

Move a realm's **entire configuration** between environments as a single file — export it, commit it, and apply it elsewhere — and migrate in from an existing identity provider.

## What it is

HelixIAM treats a realm's configuration as portable, version-controllable data. You can **export** a realm as one self-contained document and **import** it into another — to promote config from staging to production, seed a new tenant, or keep configuration in git. Secrets are never written to the file as literals: they appear as **`${ENV_VAR}` placeholders** and are resolved from the environment on import. The same document can be **dropped into a folder** that the server reads on startup (config-as-code).

| Endpoint / mechanism | Purpose |
| --- | --- |
| `GET /admin/realms/{realm}/export` | Export the realm as a JSON document, secrets as `${ENV_VAR}` placeholders |
| `POST /admin/realms/{realm}/import?onConflict=…` | Apply a realm document — overwrite, skip, or fail on conflicts |
| `GET /admin/realms/{realm}/export?includeAssets=true` · `POST /admin/realms/{realm}/import` (`application/zip`) | The same as an archive that also carries the theme's fonts and images — see [Archives with theme files](#archives-with-theme-files) |
| **drop-in folder** (`/realms`) | Apply `*.json` / `*.yaml` realm files on startup, idempotently |
| migration import | Translate a realm-export from a previous provider — see [Migrate](#migrate-from-another-provider) |

## The file format (v2)

A single self-describing document per realm. The **realm id comes from the path or file name**, never the document — so an export re-homes into any realm. Unset slices may be omitted; unknown fields are ignored (forward-compatible).

```json
{
  "formatVersion": 2,
  "realm":  { "displayName": "Gov", "captchaSecretKey": "${HELIX_GOV_CAPTCHA_SECRET}" },
  "roles":  [ { "name": "ledger-writer" } ],
  "clientScopes": [ { "name": "billing", "claims": [ { "key": "dept", "mandatory": false } ] } ],
  "clients": [ { "clientId": "billing", "grantTypes": ["authorization_code","client_credentials"],
                 "redirectUris": ["https://billing.example.com/callback"], "scopes": ["openid","billing"] } ],
  "identityProviders": [ { "alias": "google", "protocol": "oidc",
      "config": { "clientId": "…", "clientSecret": "${HELIX_GOV_IDP_GOOGLE_CLIENTSECRET}" } } ],
  "flows": [ { "alias": "browser", "executions": [ /* … */ ] } ],
  "samlClients": [ /* … */ ], "organizations": [ /* … */ ],
  "requiredEnv": [ "HELIX_GOV_CAPTCHA_SECRET", "HELIX_GOV_IDP_GOOGLE_CLIENTSECRET" ]
}
```

- **`requiredEnv`** lists every environment variable the file references — set these before importing elsewhere.
- **Excluded by design:** realm signing keys (each environment mints its own) and user password / MFA secrets.
- Both **JSON and YAML** are accepted by the drop-in folder loader; the export endpoint returns JSON.

## Export a realm

```bash
curl https://auth.example.com/admin/realms/acme/export \
  -H "Authorization: Bearer $ADMIN_TOKEN" > acme-realm.json
```

The document captures the realm's configuration. Every secret is emitted as a `${ENV_VAR}` placeholder — never the literal value — so the export is safe to review, commit, and share. The `requiredEnv` array tells you exactly which variables to provide on the target.

### What's included

A realm export bundles every configurable slice the admin console manages:

- **Realm settings** — login/token/session policy, branding, CAPTCHA, password policy
- **Applications**, their **OIDC clients**, and **SAML relying parties**
- **Per-client OIDC detail** — **protocol mappers**, **client roles**, **service-account role grants**, **resource-indicator allow-lists** (RFC 8707), and **authorization services** (UMA resource server: resources, scopes, policies, permissions)
- **Roles**, **client scopes** (with claim mappings), and **groups** (hierarchy + role mappings)
- **Identity providers** / federation brokers and **authentication flows**
- **Organizations** and **admin-RBAC** role grants
- **Webhooks**, **SCIM targets**, **messaging providers** + **message templates**, and **workload-identity** credentials
- **Users** — profiles and their realm-role / group assignments

Per-client slices carry the **OAuth client id** as their key, so they re-home into any realm: on import each is matched to the target realm's client and applied (a slice whose client isn't present in the target is skipped, never orphaned).

**Never exported** — regenerate or provision these on the target: realm **signing keys** and their certificates (re-import your own key on the target, see [Realm keys](../operations/realm-keys.md#realm-export-and-import)), user **passwords and MFA secrets**, **GDPR consent ledgers** and **DSAR** (data-subject access request) records, and all runtime state (sessions, tokens, audit log). These are per-user runtime data, not realm configuration. Every secret-bearing config field above is emitted as a `${ENV_VAR}` placeholder, not a literal.

### Archives with theme files

The JSON document references uploaded theme fonts and images by id but does not carry them. To move a branded realm to another realm or instance, use the **archive**: `GET /admin/realms/{realm}/export?includeAssets=true` returns a zip with `realm-export.json`, `theme-assets/manifest.json` and the asset files.

```bash
curl -s -b cookies.txt -o acme-realm.zip "$HELIX_URL/admin/realms/$REALM/export?includeAssets=true"

curl -s -b cookies.txt -c cookies.txt "$HELIX_URL/admin/realms/$REALM/settings" >/dev/null
CSRF=$(awk '($6=="__Host-XSRF-TOKEN"||$6=="XSRF-TOKEN")&&$7~/^r1\./{t=$7} END{print t}' cookies.txt)
curl -s -b cookies.txt -X POST -H "X-XSRF-TOKEN: $CSRF" \
  -H "Content-Type: application/zip" --data-binary @acme-realm.zip \
  "$HELIX_URL/admin/realms/$REALM/import"
```

On import every asset is uploaded again through the normal upload rules (its SHA-256 checked against the manifest), the asset URLs in the themes are rewritten to the new ids, and then the document is imported as by the JSON endpoint. A malformed, oversized or inconsistent archive is a `400` and imports nothing. Limits: the document at most 16 MB, the archive at most 40 MB compressed and uncompressed. A refused organization theme names its organization.

In the console, **Import / export** offers both formats with the difference explained; it checks an archive (`.zip`, its signature, 40 MB) before uploading, and the result table lists the theme slices and any `failed` entries from a `422`.

## Import a realm

```bash
# overwrite (default): upsert — existing resources are updated in place
curl -X POST "https://auth.example.com/admin/realms/acme/import?onConflict=overwrite" \
  -H "Authorization: Bearer $ADMIN_TOKEN" -H "Content-Type: application/json" \
  --data @acme-realm.json
```

Import is an **idempotent upsert by natural key** (client `clientId`, role `name`, scope `name`, IdP `alias`, …): re-importing converges the target realm to the document's state without duplicating. The response summarises, per slice, how many entries were `created`, `updated`, or `skipped`.

### Conflict modes

`onConflict` controls how an entry whose key already exists is treated:

| `onConflict` | Behaviour |
| --- | --- |
| `overwrite` *(default)* | Update existing resources in place. |
| `skip` | **Block** — leave existing resources untouched; only create new ones. |
| `fail` | Like `skip`, but each collision is listed in the response's `conflicts` array so you can reject the apply. |

### Secret resolution

A `${VAR}` placeholder is resolved from the server's environment. Set the variables listed in `requiredEnv` before importing:

```bash
export HELIX_GOV_CAPTCHA_SECRET=…              # value from your secrets manager
export HELIX_GOV_IDP_GOOGLE_CLIENTSECRET=…
```

If a referenced variable is missing the secret is **left unset** (the rest of the import still applies). Strict mode — failing when any required variable is missing — is available for the folder loader below.

## Config-as-code: the drop-in folder

Mount a directory of realm-config files and Helix applies them on startup — one file per realm, the **file name is the realm id**:

```text
realms/
  gov.json        # → realm "gov"
  partner.yaml    # → realm "partner"
```

The server scans the folder once at startup (`*.json`, `*.yaml`, `*.yml`, in name order). If the folder does not exist it logs that it is skipping the load and carries on. Configure it with:

| Variable | Default | Meaning |
| --- | --- | --- |
| `HELIX_REALMS_LOCATION` | `/realms` | Folder scanned on startup |
| `HELIX_REALMS_ON_CONFLICT` | `skip` | `skip` (never clobber a live realm) / `overwrite` / `fail` |
| `HELIX_REALMS_REQUIRE_ENV` | `false` | `true` = fail a file whose required secret env var is missing |

The loader is **idempotent and non-fatal**: a restart re-applies cleanly (default `skip` makes it a no-op against an unchanged realm), and a malformed or partially-failing file is logged and skipped without stopping startup.

Neither the bundled `docker-compose.yml` nor the Helm chart mounts a realm folder for you. Mount one yourself, for example in your own compose override:

```yaml
services:
  server:
    volumes:
      - ./realms:/realms:ro
    environment:
      HELIX_GOV_WEBHOOK_AUDIT_SECRET: ${HELIX_GOV_WEBHOOK_AUDIT_SECRET}   # the placeholder an export emits for webhook "audit" in realm "gov"
```

On start the server logs one line per file, e.g. `Helix realm-config: applied 'gov.json' → realm 'gov' {… webhooks=SliceSummary[created=1, …]}`.

On Kubernetes, the chart's `realmConfig` values mount the folder from a ConfigMap (`realmConfig.configMap.name`) or another volume (`realmConfig.existingVolume`) and set `realmConfig.onConflict` and `realmConfig.requireEnv`; pass the secrets with `config.extraEnv`. Charts before these values had none: there, `POST` the documents to `/admin/realms/{realm}/import` from your pipeline. See [Hosting municipalities](../operations/municipality-hosting.md#1-create-the-realm) for an example.

!!! warning "`skip` and new realms"
    Up to and including 1.0.0-rc.12, an import with `skip` (the folder's default) does not create a realm that does not exist yet: the realm settings are skipped and the rest of the file lands in a realm with no settings. It is fixed in 1.0.0-rc.13. On those older releases create the realm first (`PUT /admin/realms/{realm}/settings`) or use `overwrite`.

!!! tip "Promote config like code"
    Keep the exported file in git, store the `requiredEnv` secrets in your secrets manager, and either `POST /import` from your pipeline or drop the file in the mounted folder. Because every path is idempotent, the same document converges any target realm to the same state.

## Migrate from another provider

Moving to Helix from an existing identity provider is a paste-and-go step. In the console's **Import / export** screen, choose the migration import and upload (or paste) a standard realm-export JSON from your current system — Helix translates the portable slices and upserts them into the target realm: **OIDC clients** (grant types derived from the source), **realm roles**, and **identity providers**. The exact request shape is in the live [API reference](api-reference.md).

!!! note "Users in a migration come across separately"
    Password hashes from another provider are not portable, so user **credentials** are not brought in by the *migration* import. Provision users via [SCIM](scim.md), [LDAP federation](../federation/ldap.md), or bulk user import — then decommission the old system. (A native Helix realm export/import *does* carry user **profiles and role/group assignments** — never passwords or MFA secrets.)

!!! note "Imported users have no password yet"
    Because exports never carry credentials, a user brought in by a realm import (or the drop-in folder) is created **without a password** and flagged with the `UPDATE_PASSWORD` required action. The account exists with all its roles and group memberships, but the person must **set a password** — via a reset link, the [account console](../manage/users.md), an admin, or a passwordless / federated login — **before their first sign-in**. This is standard config-as-code behaviour: profiles ship in the file, secrets are established per environment.

## See also

- [SCIM provisioning](scim.md)
- [LDAP federation](../federation/ldap.md)
- [Terraform provider](terraform.md)
- [Manage realms](../manage/realms.md)
