Skip to content

Config bundles

A config bundle is a canonical, secret-free JSON snapshot of an org's Virtual Models, credential-policy overrides, budgets, custom guardrail rules, and webhook endpoints — for backing up config, promoting from staging to prod, or migrating a local profile into the cloud.

Not the same as backup & restore

Backup & restore (/api/backup/*) snapshots the whole gateway instance: settings, provider connections, gateway-key metadata, Virtual Models, and proxy pools. A config bundle is scoped to one org and covers a different set — Virtual Models, credential policy, budgets, guardrails, and webhooks — with a SHA-256 integrity hash and first-class CLI support. Virtual Models are the one section both cover; use whichever fits the operation (whole-instance disaster recovery vs. targeted org config promotion).

What's in a bundle

json
{
  "version": 1,
  "sha256": "…",
  "exported_at": "2026-08-19T00:00:00Z",
  "virtual_models": [ { "name": "fast-model", "strategy": "failover", "...": "..." } ],
  "policies": [ { "provider_id": "anthropic", "mode": "warn" } ],
  "budgets": [ { "scope_type": "org", "scope_id": 1, "window": "monthly", "...": "..." } ],
  "guardrails": [ { "name": "block-ssn", "action": "block", "...": "..." } ],
  "webhooks": [ { "url": "https://example.com/hook", "...": "..." } ]
}

No secrets, ever. Provider API keys and OAuth tokens are not part of any section. A webhook's signing secret is dropped on export — importing a webhook that doesn't already exist (matched by URL) mints a fresh secret, exactly like creating one from the dashboard.

sha256 is computed over the five sections (excluding sha256/exported_at themselves) with a fixed field order and every section sorted by its natural key, so two exports of an unchanged org are byte-identical. POST verifies this hash before touching anything — a hand-edited or corrupted bundle is rejected up front.

Export

GET /api/org/config-bundle

Requires the ExportData capability (Owner / Admin / Billing in multi-tenant mode; unrestricted in single-user/local mode).

Import

POST /api/org/config-bundle?dry_run=true|false

Requires the ManageOrg capability (Owner-only in multi-tenant mode — applying a bundle is as sensitive as any other org-lifecycle change).

Import is additive and idempotent: it upserts each section by its natural key and never deletes anything absent from the bundle.

SectionNatural key
Virtual Modelsname
Policiesprovider_id
Budgetsscope_type + scope_id + window
Guardrail rulesname
Webhooksurl

dry_run=true (or the dashboard's preview) returns the same diff without writing:

json
{
  "virtual_models": { "creates": [{ "name": "new-vm" }], "updates": [], "unchanged": 3 },
  "policies":       { "creates": [], "updates": [{ "provider_id": "openai" }], "unchanged": 1 },
  "budgets":        { "creates": [], "updates": [], "unchanged": 2 },
  "guardrails":     { "creates": [], "updates": [], "unchanged": 5 },
  "webhooks":       { "creates": [], "updates": [], "unchanged": 1 }
}

Omitting dry_run applies the bundle immediately — the flag is the opt-in safety net, not the default (matching the CLI's import <file> [--dry-run]).

A real apply is audit-logged (config_bundle.apply), and since credential-policy overrides are process-wide (see Credentials, vault & proxies), applying a bundle's policies section refreshes every instance in the fleet immediately, the same as PUT /api/policy/{provider}.

CLI

bash
# Export to a file
merido config export --out prod-config.json

# Preview an import (no writes)
merido config import prod-config.json --dry-run

# Apply it
merido config import prod-config.json

Both subcommands run against the CLI's active contextlocal (direct SQLite access, no running server needed) or a saved remote server (over /api/org/config-bundle with the saved mdm_ management token):

bash
# Promote staging's config to prod
merido --context staging config export --out staging.json
merido --context prod config import staging.json --dry-run
merido --context prod config import staging.json

Caveats

  • Budgets: a scope+window can hold both an overall budget and a per-model-glob budget side by side; the bundle format doesn't distinguish them by that glob (the read API doesn't surface it), so a second budget on the same scope+window is matched positionally rather than by its glob. The common case — one budget per scope+window — is unaffected.
  • Budget updates only ever touch the hard/soft limit and the enabled flag, mirroring PUT /api/budgets/{id} (the metric is immutable after create).
  • Virtual Model targets pinning an account_id are portable data (not secrets), but an account id that doesn't exist in the destination environment will fail at request time with the same "no connection or OAuth account configured" error a manually-created target would — the importer does not re-validate pins across environments.

© merido. All rights reserved.