Skip to content

CLI reference

The tendb CLI ships in the npm package @10play/tendb. It requires Node.js 20 or newer.

Terminal window
npm install -g @10play/tendb
tendb --version

Every command talks to the DBLab Engine API on your engine host — through the platform’s native tunnel (AWS SSM by default; IAP on GCP, Bastion on Azure, plain loopback locally — no open ports, no SSH keys), or directly with --api-url. Configuration comes from flags, TENDB_* environment variables, and tendb.json — see the configuration reference.

These flags work on every leaf command:

FlagDefaultMeaning
--env <name>Select an environment block from tendb.json
--platform <name>awsPlatform adapter: aws, gcp, azure, or local
--region <region>AWS/GCP region
--profile <profile>AWS profile (loaded via the shared credentials file)
--ssm-prefix <prefix>/tendbSSM parameter prefix
--instance-id <id>DBLab host instance id (skips the SSM instance-id lookup)
--api-url <url>Direct DBLab API URL — no AWS/SSM at all; requires a configured token
--config <path>Explicit path to tendb.json
-o, --output <format>tableOutput format: table or json
--quietoffSuppress progress output on stderr

The CLI keeps stdout clean for results and sends all progress chatter to stderr:

  • Results — connection URIs, tables, JSON — go to stdout.
  • Progress lines (dimmed) and warnings go to stderr. --quiet silences progress but not warnings.
  • -o json prints pretty-printed JSON to stdout.
  • Passwords are masked in progress lines, but result URIs on stdout contain the real password — mask them in CI logs (see tendb ci).

Every command that takes a branch name applies the same rules:

  • A bare number N becomes pr-N (CI shorthand): tendb branches create 42 creates pr-42.
  • Names must match [a-z0-9][a-z0-9-]*, max 63 characters — lowercase alphanumerics and dashes, starting with an alphanumeric. Anything else exits 2 with invalid branch name.

The name does triple duty: it is the DBLab branch name, the clone id, and (with dashes turned into underscores) the Postgres role name inside the clone.

CodeMeaning
0Success (including ci delete of an absent branch or a down platform)
1Generic/API error, unexpected errors
2Usage error (bad name, bad flag value, missing token in direct mode, …)
3Branch/clone not found
4Timeout, or a clone entered a FATAL state
5Missing dependency (session-manager-plugin, terraform, or psql not on PATH)
10Platform down (the SSM instance-id or token parameter is missing)
42Clone capacity exhausted (port pool full)

Errors print as error: <message> plus an optional hint: line on stderr. psql, tunnel, and migrate propagate the child process’s exit code.

Scaffold a Terraform deployment plus tendb.json into the current project — the npm-native way to stand the infrastructure up anywhere.

Terminal window
npx @10play/tendb init # interactive
npx @10play/tendb init --platform aws --yes \
--region us-east-1 --pg-version 16 \
--source-secret-arn arn:aws:secretsmanager:... # scripted

Prompts for the platform (aws / gcp / azure / local) and its required inputs, then writes:

  • tendb/ (override with --dir) — main.tf, variables.tf, outputs.tf, a generated terraform.tfvars, a README.md of next steps, and a .gitignore for state files. The local platform adds seed/seed.sql and scripts/host-setup.sh. It is plain Terraform you own — edit freely.
  • tendb.json at the project root — created, or merged into an existing one (your keys win; switching an existing config to a different platform needs --force).

Every prompt has a flag twin, so --yes makes init fully scriptable:

FlagPlatformMeaning
--dir <path>allDeployment directory (default ./tendb)
--name <name>allResource-name prefix (default tendb)
--size <size>allsmall | medium | large | xlarge (default small)
--pg-version <major>allPostgres major of the source — must match or restore fails
--ref <git-ref>allPin the Terraform module sources to a tendb version
--modules-source <path>allUse a local modules checkout instead of git (development)
--source-secret-arn <arn>awsSecrets Manager ARN of the source URL (blank → fill in later)
--region <region>aws, gcpRegion (global flag)
--project <id> / --zone <zone>gcpProject and zone
--source-secret-id <id>gcpSecret Manager id of the source URL
--location <loc>azureAzure location (default northeurope)
--subscription-id <id>azureSubscription (blank → ARM_SUBSCRIPTION_ID)
--ssh-public-key <path|key>azureVM admin key — a path is read, a literal is used as-is
--source-secret-name <name>azureKey Vault secret name for the source URL
--source-url <url>localSource Postgres URL (blank → seeded demo container)
--state-dir <path>localParams dir (default ~/.tendb/local)
  • --yes (or a non-TTY) accepts defaults and errors, listing any answer that has no default and no flag.
  • A non-empty --dir is refused without --force; --force overwrites only scaffold-owned files and never deletes (state files and your own files survive).

Exit codes: 0 scaffolded · 1 cancelled at a prompt · 2 usage (missing required flags under --yes, platform mismatch without --force, non-empty dir).

Bring the scaffolded deployment up: preflight → terraform init + terraform apply → fold the stack’s cli_discovery output back into tendb.json.

Terminal window
tendb up # interactive terraform approval
tendb up --yes # -auto-approve
  • The deploy dir resolves from --dir, else deployDir in tendb.json, else ./tendb.
  • Preflight (skippable with --skip-preflight) catches what a clean apply would hide: the source secret must exist (Secrets Manager on AWS, gcloud secrets describe on GCP), and on the local platform it runs scripts/host-setup.sh (colima VM + zpool) and points terraform at the colima Docker socket.
  • On Azure the first apply is two-phase — up targets the Key Vault first, prompts for the source URL (stored via az keyvault secret set), then applies the rest.
  • --no-init skips terraform init; a re-run is a plain re-apply. --env <name> writes the discovery outputs into that environments block instead of the top level (the block must already exist).

Exit codes: 0 up and wired · 1 terraform/preflight failure (message + hint on stderr) · 2 no deployment found · 5 terraform not on PATH (with an install hint).

terraform destroy the deployment. Asks for confirmation (or --yes); refuses without --yes when non-interactive. Keeps tendb.json, the state file, and (locally) the colima VM — colima delete reclaims that. --dir <path> overrides the deployment directory, same resolution as up.

Terminal window
tendb down --yes

Exit codes: 0 destroyed · 1 cancelled or terraform failure · 2 no deployment found, or non-interactive without --yes · 5 terraform not on PATH.

Manage branch databases — DBLab branches plus their copy-on-write clones.

Terminal window
tendb branches create <name> [--from <branch>] [--fresh]
FlagDefaultMeaning
--from <branch>mainBase branch — honored only when the branch does not already exist
--freshoffSnapshot the streaming sync target first, so the branch is “main as of now” rather than “as of the last snapshot”

Creation is idempotent: an existing healthy clone is reused as-is; a wedged clone (any state other than OK) is deleted and recreated. The command waits for the first pool snapshot to exist (up to snapshotTimeoutSeconds, default 900 s), creates the DBLab branch if absent, creates the clone, and waits for it to reach OK (up to cloneTimeoutSeconds, default 120 s).

Terminal window
tendb branches create my-feature
tendb branches create 42 # creates pr-42
tendb branches create rehearse --from staging --fresh

Output: table mode prints branch <name> ready on stderr and the connection URI as the only stdout line. JSON mode prints { name, state, port, uri }.

Exit codes: 0 success; 2 bad name; 4 snapshot timeout or clone failure; 42 capacity exhausted; 10 platform down.

Terminal window
tendb branches list

Fetches branches and clone status in parallel and joins them. The table shows BRANCH STATE PORT DATA STATE AT AGE and excludes main; a N clone(s) running summary goes to stderr.

JSON mode prints { branches, clones } with the raw engine objects — including main, unlike the table.

Terminal window
tendb branches get <name>

Looks up the branch’s clone. Table mode prints a one-row table plus the connection URI on stdout; JSON mode prints { name, state, port, createdAt, uri }. Exits 3 if the clone does not exist.

Terminal window
tendb branches delete <name>

Deletes the clone (tolerating “already gone”), polls up to 60 s for it to disappear, then deletes the branch. Exits 0 even when the branch never existed; exits 4 if the clone is still present after deletion. Table mode prints nothing on stdout; JSON mode prints { name, deleted: true }.

Terminal window
tendb branches reset <name>

Deletes and recreates the clone on its existing branch — a fresh copy-on-write from the branch snapshot, discarding all changes made on the branch. Exits 3 if the branch has no clone.

Output: branch <name> reset on stderr and the URI on stdout; JSON mode prints { name, state, port, uri }.

Terminal window
tendb psql <name> [-- <psqlArgs...>]

Opens an SSM port-forward to the branch’s Postgres port (on a random local port) and launches psql connected to it, fully interactive. Extra arguments after -- are passed through to psql — they are placed before the connection string, because psql ignores trailing options.

Terminal window
tendb psql pr-42
tendb psql pr-42 -- -c 'select count(*) from users'

The CLI exits with psql’s exit code. If psql is missing from PATH, it exits 5 with an install hint (brew install libpq or postgresql). Not available in direct --api-url mode (exits 2 — port forwarding needs a platform session).

Terminal window
tendb connection-string <name> [--local]

Prints the branch’s connection URI — and nothing else — on stdout, neonctl-style. It opens no tunnel itself. -o json has no effect on this command.

FlagMeaning
--localRewrite the host to 127.0.0.1 and the port to the clone’s remote port, for use through an already-open tendb tunnel <name>
Terminal window
tendb connection-string pr-42
tendb connection-string pr-42 --local # pair with: tendb tunnel pr-42
Terminal window
tendb tunnel [name] [--port <port>] [-- <cmd...>]
Argument / flagDefaultMeaning
[name]Branch to forward; omit it to forward the DBLab API port (2345) instead
--port <port>same as the remote portLocal port
[cmd...]Command to run with DATABASE_URL exported (write it after --)

Foreground mode (no command): prints forwarding localhost:<local> → <remote> (ctrl-c to stop) and, when a branch was named, a ready-to-copy DATABASE_URL=... line — both on stderr. Blocks until Ctrl-C (exit 0). If the SSM session dies on its own (session duration limits), the command exits 1 so scripts notice — respawning is your job.

Exec mode (command after --): opens the tunnel, runs the command with DATABASE_URL set to the tunnel-localized URI and stdio inherited, then exits with the command’s exit code.

Terminal window
tendb tunnel pr-42 # forward pr-42's Postgres port
tendb tunnel pr-42 --port 5433 # pick the local port
tendb tunnel pr-42 -- npm test # run tests against the branch
tendb tunnel # DBLab API on localhost:2345
Terminal window
tendb status

Checks engine health, fetches /status, reads clone capacity from the SSM port-pool parameter, and (when a replication publisher is configured) queries the publisher’s replication slots — all in parallel. The table shows:

RowContent
healthok or UNREACHABLE
engineDBLab Engine version
transportssm (i-…), iap, bastion, local, or direct
sync mode / sync statusRetrieval mode and state
last refresh / next refreshRefresh timestamps
data state atData-state timestamp of the first pool
diskX.XG used / Y.YG (Z.ZG free)
clonesn / cap when the port-pool parameter exists, else bare n

On streaming deployments a second table follows, one row per logical replication slot: replication slot | state | behind | wal retained. behind is WAL the subscriber hasn’t confirmed yet (confirmed_flush_lsn → head); wal retained is WAL the publisher keeps on disk for that slot (restart_lsn → head) — the number that grows without bound while a slot is stalled.

Running clone ids are listed on stderr. JSON mode prints { healthy, transport, instanceId, engineVersion, retrieving, pools, clonesUsed, cloneCapacity, replication } (cloneCapacity is null when unknown; replication is null when no publisher is configured, and each slot carries lagBytes and walRetainedBytes).

Terminal window
tendb migrate [branch] [--scratch] [--from <branch>] [--keep] [--fresh] -- <cmd...>

Runs a migration command with DATABASE_URL pointed at a branch, then exits with the command’s exit code. This is the migration face of the CI contract.

FlagDefaultMeaning
--scratchoffCreate an ephemeral branch (migrate-<timestamp>), run, then delete it — even on failure, so reruns start pristine
--from <branch>mainBase branch for --scratch
--keepoffKeep the scratch branch after the run
--freshoffSnapshot the streaming sync target first — rehearse on data as of now

Name a branch, or pass --scratch for an ephemeral one; either way, the command comes after --. Omitting both the branch and --scratch, or omitting the command, exits 2 with a usage error.

Terminal window
tendb migrate my-branch -- npx prisma migrate deploy
tendb migrate --scratch -- npx prisma migrate deploy # rehearse + clean up
tendb migrate --scratch --keep --fresh -- ./migrate.sh # rehearse on fresh data, keep the evidence

On platform sessions the branch URL is tunneled per call; in direct mode the clone URI is dialed as-is (assumes in-VPC reachability).

Output: table mode prints one stderr line — ok on <branch> in <ms>ms or failed (exit N) on <branch> in <ms>ms, plus — scratch branch removed when applicable. JSON mode prints { ok, exitCode, durationMs, branch, kept } on stdout.

Script-friendly verbs with a strict machine contract, designed as a drop-in for branch-per-PR shell scripts (see CI previews):

  • The connection URI is the last line on stdout; everything else goes to stderr.
  • A bare number N means branch pr-N.
Terminal window
tendb ci ensure <id>

Ensures the branch and clone exist and are ready (same idempotent semantics as branches create, without --from/--fresh), then prints the URI on stdout. Exit codes: 0, 42 (capacity), 4 (timeout/FATAL), 10 (platform down), 1.

Terminal window
tendb ci url <id>

Prints the URI of an existing branch database. Exits 3 when it does not exist.

Terminal window
tendb ci delete <id>

Deletes the branch database. Exits 0 both when the branch is already gone and when the platform is down (DBLab host absent — nothing to delete on stderr) — so PR-close cleanup jobs never fail spuriously. Other errors still propagate.

Mask the URI in GitHub Actions logs:

Terminal window
URI=$(tendb ci ensure "$PR_NUMBER" | tail -1)
echo "::add-mask::$URI"
Terminal window
tendb checkup [--strict]

Runs the full health rule set: engine health, disk usage, clone capacity, data staleness (measured from the newest pool snapshot in streaming mode), logical replication health on both publisher and subscriber, and schema drift. Cron-able — see operations.

FlagMeaning
--strictExit non-zero on warnings as well as criticals

Finding codes: engine-unreachable, sync-alerts, data-stale, disk-usage, clone-capacity, replication-publisher, replication-subscriber, subscription-disabled, replication-errors, slot-inactive, replication-lag, replication-stale, schema-drift. Disk and capacity findings escalate from warning to critical; replication connectivity findings are critical.

Default thresholds:

ThresholdDefault
dataStaleHours26 — drops automatically to 2 when streaming replication is configured (unless explicitly overridden)
diskWarnRatio / diskCriticalRatio0.8 / 0.92
capacityWarnRatio0.8
replicationLagBytes52428800 (50 MiB)
replicationStaleSeconds300

Output: health ok — no findings, or a severity code message table. JSON mode prints { ok, findings, measuredAt }. Exit: 1 if any critical finding (or, with --strict, any finding at all); else 0.

Pool snapshots for streaming deployments. The engine host runs tendb-snapshotd, which CHECKPOINTs the sync target and takes an O(1) ZFS snapshot — seconds at any database size. The CLI drives it through two SSM parameters: <prefix>/snapshots/config (the schedule) and <prefix>/snapshots/request (a nonce meaning “snapshot now”). See data refresh.

Terminal window
tendb snapshots list

Lists pool snapshots, newest first (branch-head snapshots are excluded). Table: ID DATA STATE AT. JSON mode prints the raw snapshot array.

Terminal window
tendb snapshots create

Requests a snapshot now and polls until a new pool snapshot appears (default timeout 90 s; on timeout, exit 4 with the hint to check systemctl status tendb-snapshotd on the engine host). Table mode prints snapshot <id> ready in X.Xs on stderr and the snapshot id on stdout; JSON mode prints the snapshot plus durationMs.

Terminal window
tendb snapshots config [--interval-minutes <n>] [--retain <n>]
FlagConstraintMeaning
--interval-minutes <n>0–10080Minutes between scheduled snapshots; 0 = manual only
--retain <n>1–500Pool snapshots to keep (in-use ones are never pruned)

With no flags, shows the current schedule (defaulting to {intervalMinutes: 0, retain: 24} when the parameter is absent). With any flag, merges with the current config and writes it — the on-host executor picks it up within seconds.

Terminal window
tendb snapshots config --interval-minutes 60 --retain 24

DDL never travels over logical replication, so the sync target’s schema drifts silently when you migrate the source. These commands surface and reconcile that drift, driven by the contract params <prefix>/schema/config and <prefix>/schema/sync-request. The same param-store requirement as snapshots applies.

Terminal window
tendb schema diff

Fingerprints every public table on the publisher and the subscriber (an md5 over each table’s ordered column/type/nullability list) and reports three buckets:

  • missing on sync target — tables that exist only on the publisher; the first replicated write to one of these pauses the stream
  • only on sync target — orphaned tables that will collide if recreated upstream
  • columns differ — tables whose column definitions diverge

Prints schema in sync when clean; JSON mode prints { missing, orphaned, mismatched, inSync }. Exits 1 when drifted, 0 when in sync — usable as a CI gate. Exits 2 with cannot compare schemas when either side did not answer.

Terminal window
tendb schema sync

Destructive full reconcile via the engine-host daemon: missing tables are created, orphaned tables are dropped, mismatched tables are rebuilt, the publication is refreshed, and error counters are reset. The CLI writes a sync-request nonce and polls the drift check every 3 s until clean (default timeout 120 s; on timeout, exit 4 with a hint to check journalctl -u tendb-snapshotd on the engine host).

Output: a single stderr progress line, schema in sync (X.Xs). This subcommand prints nothing on stdout, even with -o json.

Terminal window
tendb schema config [--auto-sync <on|off>]

Shows or sets auto-heal: with it on, the engine-host daemon fixes additive drift (new tables) on its own at roughly one-minute cadence. With no flag, shows the current setting (default off). Table output: auto-heal on|off; JSON: { autoSync }.

Terminal window
tendb console [--port <port>] [--no-open]
FlagDefaultMeaning
--port <port>4400Local port
--no-openopensDo not open a browser

Starts the tendb web console — a Neon-style dashboard served locally, bound to 127.0.0.1 only. The server proxies the DBLab API, injects the verification token server-side, runs SQL against clones through on-demand tunnels, and runs a 60 s alert loop with optional Slack notifications (webhook stored in SSM at <prefix>/alerts/slack-webhook). The token and your AWS credentials never reach the browser. With a streaming sync target configured, branch main is served live and read-only from the sync target.

Prints tendb console: http://localhost:<port> on stderr and blocks until Ctrl-C. See the console guide for what’s inside.

Terminal window
tendb ui [--no-open]

Opens the DBLab Engine’s own embedded UI through two fixed-port tunnels: UI on localhost:2346 and API on localhost:2345 (the UI’s browser code calls localhost:2345, so local ports must match the remote ones exactly). Blocks until Ctrl-C. Needs a tunneling platform session — in direct --api-url mode it exits 2, because the embedded UI is bound to the host’s loopback.

FlagMeaning
--no-openDo not open a browser (a failed auto-open prints the URL instead)

The verification token you need to paste into the UI’s auth field is printed on stdout.

MessageExitWhat to do
DBLab host not found (/tendb/instance-id missing — platform down?)10Bring the platform up (terraform apply)
session-manager-plugin not found on PATH5Install the AWS Session Manager plugin (the hint shows brew/dpkg commands)
psql not found on PATH5brew install libpq (or postgresql)
cannot reach DBLab API at <url>1Is the tunnel/engine up? Try tendb status
clone <name> not found3The branch has no clone — create it
clone capacity exhausted42Delete an idle branch (tendb branches list) or grow the terraform clone_port_range (see the engine module)
no snapshot after 15m4Check docker logs dblab_server and /var/log/dblab-init.log on the host
direct mode (--api-url) needs a token2Set token in tendb.json or TENDB_TOKEN
no new snapshot after 90s4Check systemctl status tendb-snapshotd on the engine host

AWS SDK credential errors (expired SSO, missing ssm:StartSession permission) are not wrapped — they surface as unexpected error: <stack> with exit 1.