From mirrord
Configures mirrord.json for database branching to create isolated database copies for safe development and testing. Supports MySQL, PostgreSQL, MongoDB, Redis, DynamoDB, and more.
How this skill is triggered — by the user, by Claude, or both
Slash command
/mirrord:mirrord-db-branchingThe summary Claude sees in its skill listing — used to decide when to auto-load this skill
Generate and validate `mirrord.json` configurations for database branching:
Generate and validate mirrord.json configurations for database branching:
db_branches configs from natural language descriptionsDB branching is a Team / Enterprise feature. It spins up an isolated branch of a remote database so developers (and AI agents) can run schema changes, migrations, and experiments without affecting teammates or shared environments.
IMPORTANT: Follow these security rules for all operations in this skill.
value literal source, which the user must supply themselves — never invent one.credentials_path over credentials_json.Authoritative docs (fetch sub-pages for engine-specific detail):
Step 0: Load References
Read the reference files from this skill's references/ directory:
references/db-branches-schema.json — authoritative JSON Schema for db_branches (extracted from the mirrord schema). Config lives under feature.db_branches.references/troubleshooting.md — common issues and solutionsThe schema is derived from the official mirrord schema at: https://raw.githubusercontent.com/metalbear-co/mirrord/main/mirrord-schema.json
If using absolute paths, search for the schema using patterns like **/mirrord-db-branching/references/*.
Step 1: Verify Prerequisites Each engine has minimum operator, mirrord CLI, and Helm chart versions, and a per-engine Helm value that must be enabled. See Version Requirements below.
Step 2: Identify Connection Source The app must read its DB connection from environment variables (or Kubernetes Secrets). mirrord overrides those variables with the branch's connection details for the session. Confirm the exact variable name(s) the app uses.
Step 3: Validate Configuration After generating any config, ALWAYS run:
mirrord verify-config /path/to/config.json
db_branches is an array under the top-level feature object:
{
"feature": {
"db_branches": [
{
"id": "users-mysql-db",
"type": "mysql",
"version": "8.0",
"name": "users-database-name",
"ttl_secs": 300,
"creation_timeout_secs": 60,
"connection": {
"url": "DATABASE_URL"
},
"copy": {
"mode": "empty"
}
}
]
}
}
Common mistake: placing
db_branchesat the top level. It must be nested insidefeature.
| Database | type | Branch location | Copy modes | Notes |
|---|---|---|---|---|
| MySQL | "mysql" | Remote | empty, schema, all, filtered | IAM auth, migrations, dump_args |
| MariaDB | "mariadb" | Remote | empty, schema, all, filtered | IAM auth, migrations |
| PostgreSQL | "pg" | Remote | empty, schema, all, filtered | IAM auth, migrations, dump_args, connection_settings |
| MSSQL | "mssql" | Remote | empty, schema, all, filtered | migrations (no dump_args) |
| MongoDB | "mongodb" | Remote | empty, all, collection filters | schema-less (no schema mode) |
| Redis | "redis" | Remote or local | empty, all, patterns | name = DB index |
| DynamoDB | "dynamodb" | Remote (local emulator pod) | empty, all, table filters | iam_auth required for all |
| ClickHouse | "clickhouse" | Remote | empty, schema, all, filtered | |
| Google Spanner | "spanner" | Remote (emulator pod) | empty, schema, all, filtered | uses SPANNER_EMULATOR_HOST |
| Generic | "generic" | Remote | none (always empty) | any service, your own image |
| Field | Applies to | Description |
|---|---|---|
type | all | Database engine (see table above). |
connection | all (optional for DynamoDB) | How mirrord locates the source connection details. See Connection Modes. |
id | all | Reuse/share a branch: same id reattaches to an existing branch while its TTL hasn't expired. Use a unique value (e.g. a UUID) to avoid reusing someone else's branch. Ignored for local Redis. |
name | most | Source database name to clone. The override URL becomes .../<name>. If omitted, the URL points at the server and the app must select the DB. For Redis, name is the numeric DB index (default 0). Required when using migrations. |
version | all except generic | Engine image version (e.g. "8.0", "16"). For generic, the tag lives in image and version is not allowed. |
ttl_secs / ttl_mins | all | Branch time-to-live, counted from when no session is using it. Default 5 minutes; caps at 15 minutes. The two are mutually exclusive. |
creation_timeout_secs | all | How long to wait for the branch to become ready. Default 60. Unrecoverable pod failures (e.g. ImagePullBackOff, OOMKilled) fail immediately instead of waiting. |
copy | all except generic | How the branch is cloned. See Copy Modes. |
iam_auth | mysql, mariadb, pg, dynamodb | IAM auth for AWS RDS / GCP Cloud SQL. See IAM Authentication. |
migrations | mysql, mariadb, pg, mssql | Run schema migrations on the branch at creation. See Schema Migrations. |
connection_settings | pg | PostgreSQL session settings applied while reading the source (e.g. for RLS). |
emulator_host | spanner | Name of the env var mirrord sets to the emulator address (default SPANNER_EMULATOR_HOST). |
location | redis | "remote" (default) or "local". |
local | redis | Local Redis runtime config (see Redis). |
image / port / command / args / env / readiness | generic | See Generic Branches. |
Enable the matching Helm value on the operator chart, and meet the minimum versions:
| Engine | Operator | CLI | Helm chart | Helm value |
|---|---|---|---|---|
| MySQL | 3.129.0 | 3.160.0 | 1.37.0 | operator.mysqlBranching: true |
| PostgreSQL | 3.131.0 | 3.175.0 | 1.40.2 | operator.pgBranching: true |
| MSSQL | 3.150.0 | 3.195.0 | 1.57.0 | operator.mssqlBranching: true |
| MongoDB | 3.137.0 | 3.183.0 | 1.44.0 | operator.mongoBranching: true |
| Redis (remote) | 3.168.0 | 3.217.0 | 3.168.0 | operator.redisBranching: true |
| Redis (local) | — | 3.180.0 | — | none (runs on your machine) |
| DynamoDB | 3.179.0 | 3.228.0 | 3.179.0 | operator.dynamodbBranching: true |
| ClickHouse | 3.182.0 | 3.230.0 | 3.182.0 | operator.clickhouseBranching: true |
| Google Spanner | 3.182.0 | 3.230.0 | 3.182.0 | operator.spannerBranching: true |
| Generic | 3.183.0 | 3.232.0 | 3.183.0 | operator.genericBranching: true |
| Schema migrations | 3.182.0 | 3.230.0 | 3.182.0 | (per engine above) |
connection describes where mirrord reads the source connection details. The optional type controls where the env var is read from and defaults to "env":
"env" (default): a direct env entry in the target pod spec."env_from": from the pod's envFrom (secretRef / configMapRef).The simplest form — an env var name holding the full connection string:
{ "connection": { "url": "DATABASE_URL" } }
Equivalent explicit forms (all valid): { "url": { "type": "env", "variable": "DATABASE_URL" } } and { "type": "env", "url": "DATABASE_URL" }.
When the app stores host/port/user/password/database separately:
{
"connection": {
"params": {
"host": "DB_HOST",
"port": "DB_PORT",
"user": "DB_USER",
"password": "DB_PASSWORD",
"database": "DB_NAME"
}
}
}
Each param is individually optional; mirrord fills engine defaults for any not specified. Defaults — host: localhost for all; port/user: PostgreSQL 5432/postgres, MySQL 3306/root, MSSQL 1433/sa, MongoDB 27017/root, Redis 6379/default, ClickHouse 9000/default.
Any param (and, where noted, the url) can be sourced beyond a plain env var:
{ "secret": "rds-credentials", "key": "password", "env_var_name": "DB_PASSWORD" }{ "type": "gcp_secret_manager", "secret_ref": "projects/../secrets/../versions/latest", "env_var_name": "DATABASE_URL" }; param → { "gcp_secret_manager": "projects/../secrets/../versions/latest", "env_var_name": "DB_PASSWORD" }{ "env_var_name": "DB_PASSWORD", "value": "..." } — stored in a Secret by the CLI. Do not invent values.value_pattern): extract one part of a packed value, e.g. host and port from DB_SERVER=host:5432. Capture group name follows the param name ((?P<host>...)), or use (?P<value>...) / a single unnamed group. Must contain ≥1 capture group.url and each param accept an array. The first entry is used to locate/clone the source; every entry is rewritten to point at the branch (e.g. separate write/read URLs).{
"connection": {
"params": {
"host": { "env_var_name": "DB_SERVER", "value_pattern": "^(?P<host>[^:]+):\\d+$" },
"port": { "env_var_name": "DB_SERVER", "value_pattern": "^[^:]+:(?P<port>\\d+)$" },
"password": { "secret": "db-creds", "key": "password", "env_var_name": "DB_PASSWORD" }
}
}
}
copy.mode controls what is cloned. Default is "empty".
| Mode | What's cloned | Notes |
|---|---|---|
"empty" (default) | Nothing — empty DB | For apps that run migrations / init schema on startup |
"schema" | Table structures only, no data | Not available for MongoDB, Redis, DynamoDB |
"all" | Schema and all data | Small DBs only — large copies are slow and storage-heavy |
Copy schema plus filtered rows per table. Combine with "empty" to copy only the listed tables. Not compatible with "all" (the tables map is ignored if mode is all).
{
"copy": {
"mode": "schema",
"tables": {
"users": { "filter": "name = 'alice' OR name = 'bob'" },
"orders": { "filter": "created_at > 1759948761" }
}
}
}
collectionsMongoDB and DynamoDB use collections instead of tables and support only empty / all.
"{\"name\": {\"$in\": [\"alice\", \"bob\"]}}".Scan FilterExpression string, e.g. "active = true". It cannot use ExpressionAttributeValues/Names placeholders. An empty {} copies the table in full.{ "copy": { "mode": "all", "collections": { "users": { "filter": "active = true" }, "orders": {} } } }
With "empty" + filters, only the listed collections/tables are created.
patternsRedis supports empty / all (remote only; local always starts empty). Narrow all with SCAN MATCH glob patterns:
{ "copy": { "mode": "all", "patterns": ["user:*", "session:*"] } }
dump_args) — MySQL & PostgreSQL onlyCustomize mysqldump / pg_dump. Available in all copy modes. MSSQL, MongoDB, ClickHouse do not support dump_args.
--opt defaults). Listed args are passed as-is; [] removes defaults.dump_args replaces defaults entirely (defaults are --no-owner --no-acl); include them if you want to keep them; [] removes all.{ "copy": { "mode": "schema", "dump_args": ["--no-owner", "--no-acl", "--exclude-table=audit_logs"] } }
migrations runs your schema migrations against the branch at creation, before it becomes ready — so the branch matches the schema your working tree expects. Supported for MySQL, MariaDB, PostgreSQL, MSSQL. Requires the branch name to be set. Failure aborts the session (the app never starts against a half-migrated branch).
{
"migrations": {
"flavor": "flyway",
"path": "./migrations",
"image": "flyway/flyway:12"
}
}
flavor: migration tool. Currently flyway.path: local migrations directory, relative to the working directory.image: optional runner image override (default flyway/flyway:12).Authenticate to the source database with IAM instead of a password. Credentials are read from the target pod's environment (not your local shell). Supported for MySQL, MariaDB, PostgreSQL (AWS RDS + GCP Cloud SQL) and DynamoDB (AWS; iam_auth is required for copy.mode: all).
{ "iam_auth": { "type": "aws_rds" } }
Default env vars from the target pod: AWS_REGION/AWS_DEFAULT_REGION, AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY, AWS_SESSION_TOKEN. Override only for non-standard names:
{
"iam_auth": {
"type": "aws_rds",
"region": { "type": "env", "variable": "MY_CUSTOM_REGION" },
"access_key_id": { "type": "env", "variable": "MY_ACCESS_KEY" },
"secret_access_key": { "type": "env", "variable": "MY_SECRET_KEY" }
}
}
DynamoDB reuses the
aws_rdstype name:{ "iam_auth": { "type": "aws_rds" } }.
Requires TLS — the connection URL must include sslmode=require.
{ "iam_auth": { "type": "gcp_cloud_sql" } }
Defaults: credentials_path ← GOOGLE_APPLICATION_CREDENTIALS; project ← GOOGLE_CLOUD_PROJECT/GCP_PROJECT/GCLOUD_PROJECT. Override with either credentials_path or credentials_json (not both), each pointing at an env var. Prefer credentials_path.
Google Spanner does not use
iam_auth. It authenticates as the target pod's own Google identity via Application Default Credentials; grant that identity read access (e.g.roles/spanner.databaseReader).
Redis is the only engine that runs remotely or locally.
{
"feature": { "db_branches": [ {
"type": "redis", "version": "7.2", "name": "0",
"connection": { "url": "REDIS_URL" },
"copy": { "mode": "empty" }
} ] }
}
name is the numeric DB index (default 0).
Spawns a Redis instance on your machine and redirects the app's Redis traffic to it. Always starts empty; copy modes don't apply; id is ignored.
{
"feature": { "db_branches": [ {
"type": "redis",
"location": "local",
"connection": { "host": { "type": "env", "variable": "REDIS_ADDR" } },
"local": {
"port": 6379,
"runtime": "container",
"container_runtime": "docker"
}
} ] }
}
local.runtime: "container" (default), "redis_server", or "auto".local.container_runtime: "docker" (default), "podman", or "nerdctl".local.port: sessions on the same port share one local Redis DB; a new session on that port replaces it.For any stateful service mirrord has no built-in engine for (InfluxDB, Valkey, Cassandra, an internal service, …). A generic branch runs your container image and always starts empty — no copy modes, no IAM, a single redirected port. Prefer a first-class engine when one exists.
You declare the params your service needs under connection.params (the fixed slots plus any custom key like token, org). The operator resolves each from the target pod and injects it into the branch container as MIRRORD_PARAM_<NAME> (Secret-backed params arrive as a secretKeyRef; the operator never reads the value). Reference them in command/args/env with Kubernetes' $(VAR) syntax so the branch bootstraps with the same values the app uses. Two built-ins are always available: MIRRORD_BRANCH_ID and (when name is set) MIRRORD_DATABASE_NAME. Use $$(...) for a literal $(...).
| Field | Required | Description |
|---|---|---|
image | Yes | Full image reference including tag. version is not allowed. |
port | Yes | Port the service listens on — default readiness target and the redirected port. |
command / args | No | Entrypoint override; may reference $(MIRRORD_PARAM_<NAME>). |
env | No | Extra env vars; same references. Keys must not start with MIRRORD_PARAM_. |
readiness | No | Readiness probe. Defaults to a TCP probe on port. |
Readiness types: { "type": "tcp" } (default), { "type": "http_get", "path": "/health", "port": 8086 }, { "type": "exec", "command": ["redis-cli", "ping"] }. Prefer a probe that proves the service is usable, not just that the process started.
Connection must use params mode (URL mode is rejected; extract host/port from URL-shaped vars with value_pattern). gcp_secret_manager sources are not supported for generic branches. Declaring only a host param (no port) redirects every port on the branch pod — useful for multi-port services that derive URLs from one hostname var.
{
"feature": { "db_branches": [ {
"type": "generic",
"id": "my-valkey-branch",
"ttl_secs": 600,
"image": "valkey/valkey:8-alpine",
"port": 6379,
"connection": {
"params": {
"host": { "env_var_name": "VALKEY_ADDR", "value_pattern": "^(?P<host>[^:]+):" },
"port": { "env_var_name": "VALKEY_ADDR", "value_pattern": ":(?P<port>[0-9]+)$" },
"password": "VALKEY_PASSWORD"
}
},
"args": ["valkey-server", "--requirepass", "$(MIRRORD_PARAM_PASSWORD)"]
} ] }
}
Security & ops notes: generic branching is off by default and lets branch creators run arbitrary images — admins gate it (operator.genericBranching) and can restrict images via an allowedImages glob list in genericBranchConfig. Branch pods run under the namespace default service account with no API token mounted. Never inline secrets into args (visible in the pod spec) — use $(MIRRORD_PARAM_*). Heavy images (Elasticsearch, Cassandra, Couchbase) OOM at the 512Mi default; admins raise it via dbPod.resources.
Run your app with mirrord and the config above. mirrord creates (or reuses, by id) the branch, overrides the connection env var(s) to point at it, and destroys the branch when the TTL elapses with no active session. While a session is active, mirrord also sets up portforwards to the branch pod (usable from a GUI client like DBeaver/DataGrip).
# Show status of running branches (a namespace, or -A for all)
mirrord db-branches [-n <namespace>] status [name...]
mirrord db-branches -A status
# Destroy branches
mirrord db-branches [-n <namespace>] destroy <name...>
mirrord db-branches [-n <namespace>] destroy --all
mirrord db-branches -A destroy --all
# List active DB branch portforwards (only while a session is running)
mirrord db-branches connections
| Issue | Solution |
|---|---|
db_branches ignored | It must be nested under feature, not at the top level |
| Connection timeouts | Branch DBs disable SSL by default; verify the client isn't forcing SSL |
| GCP Cloud SQL fails | Ensure the connection URL includes sslmode=require |
| Branch creation slow | "mode": "all" on a large DB; switch to "schema"/"empty" or filter |
| Branch not reused | Set a matching id and ensure TTL (≤15 min) hasn't expired |
| Wrong database connected | Verify the connection variable(s) match the app's actual env vars |
DynamoDB all fails | iam_auth is required for copy.mode: all |
| Filters silently dropped | Table/collection filters are incompatible with "mode": "all" |
migrations rejected | name must be set, and the engine must be MySQL/MariaDB/PostgreSQL/MSSQL |
| Generic branch never ready | Use an http_get/exec readiness probe; plain TCP can pass before the service is usable |
If the request is under-specified, ask for ONE detail:
Otherwise, provide safe defaults and note assumptions.
{
"feature": { "db_branches": [ {
"id": "migration-test",
"type": "mysql",
"version": "8.0",
"name": "myapp_production",
"ttl_secs": 300,
"connection": { "url": "DATABASE_URL" },
"copy": { "mode": "schema" }
} ] }
}
{
"feature": { "db_branches": [ {
"type": "pg",
"version": "17",
"name": "app_db",
"connection": { "url": "DATABASE_URL" },
"copy": { "mode": "empty" },
"migrations": { "flavor": "flyway", "path": "./migrations" }
} ] }
}
{
"feature": { "db_branches": [ {
"type": "pg",
"version": "16",
"name": "app_db",
"connection": { "url": "PG_CONNECTION_STRING" },
"copy": { "mode": "empty" },
"iam_auth": { "type": "aws_rds" }
} ] }
}
{
"feature": { "db_branches": [ {
"id": "test-data-branch",
"type": "pg",
"version": "15",
"name": "production_db",
"connection": { "url": "DATABASE_URL" },
"copy": {
"mode": "schema",
"tables": { "users": { "filter": "email LIKE '%@test.com'" } }
}
} ] }
}
{
"feature": { "db_branches": [ {
"type": "mongodb",
"version": "7.0",
"name": "app_database",
"connection": { "url": "MONGODB_URI" },
"copy": {
"mode": "all",
"collections": { "users": { "filter": "{\"role\": \"admin\"}" } }
}
} ] }
}
{
"feature": { "db_branches": [ {
"id": "users-dynamodb",
"type": "dynamodb",
"version": "latest",
"iam_auth": { "type": "aws_rds" },
"copy": {
"mode": "all",
"collections": { "users": { "filter": "active = true" }, "orders": {} }
}
} ] }
}
{
"feature": { "db_branches": [ {
"id": "users-spanner-db",
"type": "spanner",
"version": "1.5.23",
"connection": {
"params": {
"project": "SPANNER_PROJECT_ID",
"instance": "SPANNER_INSTANCE_ID",
"database_id": "SPANNER_DATABASE_ID"
}
},
"copy": { "mode": "schema" }
} ] }
}
{
"feature": { "db_branches": [ {
"type": "redis",
"location": "local",
"connection": { "url": "REDIS_URL" },
"local": { "runtime": "container", "container_runtime": "docker", "port": 6379 }
} ] }
}
db_branches always lives under feature.type string."empty" copy mode to avoid long creation times.mirrord verify-config.npx claudepluginhub metalbear-co/skills --plugin mirrordCreates, edits, and verifies skills using a test-driven development approach with pressure scenarios and subagents.