Skip to content

Architecture

tendb is a thin layer of Terraform, AWS plumbing, and CLI ergonomics around DBLab Engine (Database Lab Engine, by Postgres.ai) — the open-source engine that does the actual heavy lifting of snapshotting and thin-cloning Postgres on ZFS. One EC2 host in your AWS account syncs from your source database and serves copy-on-write branch databases. There is no control plane, no tendb service, and no state anywhere except the host itself.

YOUR LAPTOP / CI RUNNER tendb CLI AWS SDK SSM API (GetParameter, StartSession) session-manager-plugin — local port forwards outbound TLS to AWS SSM only no inbound ports on the host AWS ACCOUNT EC2 engine host SSM agent — receives the port-forward sessions DBLab Engine (Docker — postgresai/dblab-server) API :2345 UI 127.0.0.1:2346 loopback clone :6000 clone :6001 ZFS pool dblab_pool (dedicated gp3 EBS volume, lz4) restored source data + logical dump + copy-on-write clone datasets — one per branch outbound only — pg_dump source Postgres Neon · Aurora · RDS · any URL
One SSM-tunneled path in, one outbound sync path out — the host accepts no inbound connections.

The Terraform engine module builds a single EC2 instance (Ubuntu 24.04 — chosen because ZFS is one apt-get away) with two volumes: a small gp3 root volume and a dedicated gp3 data volume that becomes a ZFS pool. At boot, the host installs Docker and ZFS, creates the pool, pulls the source database URL from Secrets Manager, writes a DBLab Engine config, and starts the dblab_server container.

Three sets of ports matter, and all of them stay inside your VPC:

PortWhatReachable how
2345DBLab Engine REST APISSM port-forward (or direct TCP if you open the security group)
2346DBLab embedded UIBound to 127.0.0.1 on the host — SSM tunnel only, via tendb ui
60006009+Clone Postgres port poolSSM port-forward per clone (or direct TCP if opened)

Every branch database is a Postgres container listening on one port from the pool, so the pool width is the hard cap on concurrent branches. The default range depends on the module’s size preset (10 ports for small, up to 50 for xlarge); grow it with the clone_port_range variable. tendb status reads the pool width from SSM and shows it as clones n / cap.

The restored source data lives in a ZFS dataset. Creating a branch takes a ZFS snapshot-and-clone of that dataset and starts a fresh Postgres container on top of it. Copy-on-write means:

  • Branch creation takes seconds, regardless of database size. Nothing is copied — the clone initially shares every block with its parent snapshot.
  • A new branch consumes near-zero disk. The pool only grows by the blocks a branch actually writes (its “CoW delta”). Ten branches of a 100 GB database do not cost 1 TB; they cost 100 GB plus whatever each branch changes.
  • Resetting a branch is cheap. tendb branches reset throws the clone away and re-clones from the branch’s snapshot — again in seconds.

Each clone is a full, writable, isolated Postgres. Writes on one branch never affect another branch or the source data.

tendb does not replicate from your source database by default. On a schedule (refresh_cron, default 0 2 * * * — nightly at 02:00), the host runs a full logical refresh: pg_dump of the app database over the network into the ZFS pool, then pg_restore into the pool’s data directory, then a fresh snapshot.

Consequences of the dump/restore model:

  • It works against any Postgres you can dial with a URL — Neon, Aurora, RDS, self-hosted. No replication slots, no superuser, no logical decoding permissions on the source.
  • Branches are point-in-time copies “as of the last refresh,” not live followers. tendb branches list shows each branch’s DATA STATE AT timestamp.
  • A refresh is a full dump plus a full restore, so the data volume holds roughly the dump and the restored data (budget ~2.5× the source size).
  • DBLab Engine skips a scheduled refresh non-destructively while clones exist, so a long-lived branch never gets yanked out from under you.

For sources where nightly staleness is not acceptable, the engine module also supports a streaming mode (streaming_snapshots = true) that keeps a live sync target on the host and takes O(1) ZFS snapshots of it instead of dump/restore cycles. See Data refresh for both modes in detail.

The CLI has no configuration ceremony because the Terraform module and the host publish everything a client needs as SSM parameters under a prefix (default /tendb). This is the discovery contract:

ParameterTypePublished byContent
<prefix>/instance-idStringTerraformEC2 instance id of the engine host. Its absence means “platform down” — the CLI exits 10 and tendb ci delete treats it as “nothing to delete”.
<prefix>/hostStringTerraformThe host’s private IP.
<prefix>/verification-tokenSecureStringTerraform (write-only value)DBLab API token; also the input to clone password derivation.
<prefix>/dbnameStringThe host, at bootThe source database name parsed from the source URL. Terraform creates the parameter as a placeholder and ignores value changes, so terraform destroy still cleans it up.
<prefix>/port-poolStringTerraformThe clone port range as "6000-6009" — clients derive clone capacity from its width.

A second group of parameters under the same prefix drives optional features; these are written by clients (the CLI, the console, or your Terraform) rather than being part of boot:

ParameterWritten byPurpose
<prefix>/snapshots/config, <prefix>/snapshots/requesttendb snapshots / consoleSnapshot schedule and “snapshot now” nonce (streaming mode).
<prefix>/schema/config, <prefix>/schema/sync-requesttendb schema / consoleSchema auto-heal flag and “full sync now” nonce (streaming mode).
<prefix>/replication/publisher-url, <prefix>/replication/subscriber-urloperatorReplication endpoints for sync status and tendb checkup.
<prefix>/alerts/slack-webhook, <prefix>/console-urlconsoleSlack alerting for the console.

Transport: SSM Session Manager port-forwards

Section titled “Transport: SSM Session Manager port-forwards”

The host accepts no inbound connections — by default its security group has zero ingress rules. Every byte between a client and the host travels through AWS Systems Manager Session Manager:

  1. The CLI reads <prefix>/instance-id and calls StartSession with the AWS-managed document AWS-StartPortForwardingSession, targeting the host and a remote port (2345 for the API, a pool port for a clone).
  2. It spawns the local session-manager-plugin binary (the same one the AWS CLI uses), which holds the WebSocket to AWS and listens on a local port.
  3. The CLI polls the local port until it accepts TCP, then talks plain Postgres or HTTP through it.

One API tunnel (to 2345) opens per CLI session; clone tunnels open on demand — tendb psql, tendb migrate, and tendb tunnel each forward the specific clone port they need. Authorization is pure IAM: ssm:StartSession on the instance (tag-conditioned) and on the port-forwarding document. No VPN, no bastion, no security-group changes.

Direct TCP is available as an opt-in for in-VPC clients (CI runners in the same VPC, the hosted console): the module’s allowed_security_group_ids / allowed_cidr_blocks variables open 2345 and the clone port range, and the CLI’s --api-url mode skips AWS entirely.

tendb keeps no state — no local database, no session files, no server-side registry of branches. Every command reconstructs the world from two sources: the SSM parameters above and the DBLab Engine API. That design shows up everywhere:

  • Deterministic credentials. A clone’s Postgres password is derived as sha256("<token>:<branch>") truncated to 32 hex characters, and its username is the branch name with dashes turned to underscores. Any machine with IAM access to read the token can compute the connection string for any branch — nothing to look up, nothing to store. (See Security for the implications.)
  • Idempotent workflows. tendb branches create is create-or-reuse: an already-running clone short-circuits, a wedged one is deleted and recreated. tendb ci delete exits 0 when the branch — or the whole platform — is already gone.
  • Interchangeable clients. The CLI, the SDK, the console, and CI all speak the same contract, so a branch created in CI is immediately visible and connectable from a laptop.
  • tendb.json is configuration, not state. It only holds pointers (SSM prefix, region, profile, environments) and can be reconstructed from scratch.

Names are load-bearing in this model: a branch name is simultaneously the DBLab branch name, the clone id, and (dash→underscore) the Postgres role name, so names must match [a-z0-9][a-z0-9-]* (max 63 chars). A bare number is CI shorthand: tendb branches create 42 creates pr-42.