Appearance
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-bundleRequires 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|falseRequires 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.
| Section | Natural key |
|---|---|
| Virtual Models | name |
| Policies | provider_id |
| Budgets | scope_type + scope_id + window |
| Guardrail rules | name |
| Webhooks | url |
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.jsonBoth subcommands run against the CLI's active context — local (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.jsonCaveats
- 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_idare 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.
Related
- Backup & restore — whole-instance snapshot (settings, connections, key metadata, proxy pools).
- Virtual models & fallback
- Budgets & spend caps
- Guardrails
- Webhooks
- Remote servers — the CLI's
--context/merido serverplumbing config bundles run over.