Database Branching & Zero-Friction Workflows
In modern application development, managing database states alongside code changes is one of the biggest bottlenecks.
Nanokit Database Branching solves this by introducing a unified, context-aware database orchestration layer. It ensures that your data layer always follows your application’s state—automatically aligned with your active Git branches.
What is Database Branching?
Just as Git allows you to branch your code to work on features in isolation, Nanokit allows you to branch your database.
When you switch Git branches, Nanokit dynamically redirects your microservices to an isolated, branch-specific database instance containing a copy of your parent data. You don’t need to manually import dumps, write custom scripts, or edit connection strings.
┌─── [ Git: main ] ────────► DB: main_db (Production/Staging)
│
Git Branch Switch ┼─── [ Git: feat-login ] ──► DB: main_db-feat-login (Isolated Copy)
│
└─── [ Git: feat-billing ] ─► DB: main_db-feat-billing (Isolated Copy)Key Benefits
Implementing database branching inside your team’s workflow unlocks game-changing advantages:
- ⚡ Instant Velocity: Instantly clone a production-like database with millions of rows in seconds. Say goodbye to waiting hours for manual database backup restoration.
- 🛡️ Absolute Safety: Test destructive schema migrations, execute experimental queries, or seed test data without any risk of polluting the production database or interrupting other developers.
- 🔌 Zero-Config Autopilot: Connection strings (like
DATABASE_URLorNANOKIT_DB_POSTGRES_URL) are automatically resolved and injected at runtime based on the active Git branch. The application code remains completely agnostic. - 🛠️ Parallel Development: Multiple developers can run different database migrations (e.g.
prisma migrate,knex migrate) concurrently. There are no conflicts on shared staging environments. - 🧪 Deterministic CI/CD Pipelines: Spin up an atomic, pre-seeded database clone for each Pull Request, run integration tests, and automatically tear it down once the PR is merged.
How it Works
Nanokit manages database branches seamlessly across both cloud-native platforms and local development environments:
1. Cloud-Native Managed Branching (Cloud Mode) — planned (roadmap.v3)
Managed cloud-provider branching via native platform APIs is parked pending real-account validation (roadmap.v3) and is not available today:
- Neon (Postgres): would use Neon’s Copy-on-Write (CoW) technology — instant branches, zero additional storage.
- Turso (SQLite/LibSQL): would use the LibSQL seed/fork API to copy structure and data.
The near-term in-philosophy target is a self-hosted libSQL (
sqld) container engine (roadmap.v2), branched by Nanokit’s own volume-clone strategy (below) rather than a cloud API. See Providers → Roadmap.
2. The Local Logical Overlay (Local Mode)
For local development or standard VPS deploys, Nanokit simulates cloud branching behaviors using Docker-level optimizations:
- Volume Capture: It detects the parent data volume (e.g.,
nk-postgres-data-production). - Atomic Clone: It spawns an optimized, temporary
alpinebridge container to clone the volume data atomically and quickly. - Container Isolation: It boots up a new database container (e.g.,
nk-<project>-local-<dbName>-<branchName>) mounted to the cloned volume. - Dynamic Network Mapping: The local gateway routes traffic for that specific service to the isolated container automatically.
Configuration
To enable branching for a database, simply configure the branching block in your nanokit.yml:
databases:
nk_db_postgres:
engine: postgres
mode: local # or 'cloud'
rootPassword: ${DB_PASSWORD_POSTGRES}
branching:
enabled: true # Enables dynamic branch context switching
strategy: snapshot # 'snapshot' clones data; 'empty' starts with fresh schemaNaming Conventions & Isolation
To maintain environment-agnostic code:
- Logical Name: The name defined in
nanokit.yml(e.g.nk_db_postgres). Your services query this name. - Physical Name: Automatically resolved as
<LogicalName>-<BranchName>(e.g.nk_db_postgres-feat-login).
Nanokit dynamically translates the logical host inside your connection strings to point to the physical branch container.
CLI Lifecycle Commands
You can manage the lifecycle of your database branches directly from your terminal using the Nanokit CLI:
Forking a Database
To create a physical branch and clone the data from the parent environment:
nkapp db fork <db-name>Switching Branch Contexts
If you want to manually point your environment to a specific database branch:
nkapp db switch <db-name> --branch <target-branch-name>Note: Your application service environment variables will automatically update and bind on the next nkapp up.
Resetting a Branch
To discard all experimental data changes on a branch and re-clone fresh data from the parent:
nkapp db reset <db-name>Reset Warning: Resetting a database branch will permanently destroy all changes made on that specific branch. Make sure to commit or backup any critical schema migrations before executing this command.
Troubleshooting: “My data disappeared after switching branches”
This is the single most common point of confusion with branching, and almost always a false alarm: your data is safe, you are simply looking at a different branch’s database.
Symptom
You were working on acme-shop, signed in as jane.doe@example.com, then checked
out another Git branch (say feature-checkout), ran nkapp up, and now your app
reports the account as missing, the users table is empty, or seeded records are
gone.
Why it happens
Each branch gets its own isolated data volume. Switching Git branches does not
migrate data between them, and the running database container always follows the
branch of your last nkapp up. So if your account was created on main but
the container currently running is the feature-checkout clone (created fresh, or
cloned at a point before the account existed), your services query an isolated DB
that genuinely does not contain that row.
A typical local layout for the app_db logical database looks like this:
| Volume | Branch | Contents |
|---|---|---|
nk-postgres-data-app_db | base / main | ✅ jane.doe@example.com |
nk-postgres-data-app_db-feature-checkout | feature-checkout | ✅ jane.doe@example.com (cloned) |
nk-postgres-data-app_db-experiment-ui | experiment-ui | ❌ empty (started with strategy: empty) |
If the container mounted right now is the experiment-ui one, the account looks
“gone” — it just lives in a different volume.
Diagnose
List the per-branch volumes and check which one the running container actually mounts:
# All data volumes for the logical database
docker volume ls --format '{{.Name}}' | grep app_db
# Which volume the currently-running container is bound to
docker inspect nk-acme-shop-local-app_db-experiment-ui \
--format '{{range .Mounts}}{{.Name}} -> {{.Destination}}{{println}}{{end}}'If the mounted volume’s branch suffix doesn’t match the branch where you created your data, that is the whole issue.
Fix
Bring the database back in sync with the branch that holds your data. From the
branch you want (e.g. main):
nkapp up -e localThis reconciles the running container onto that branch’s volume. To point at a specific branch without checking it out, use:
nkapp db switch app_db --branch mainDo not run clean, destroy -v, or db prune while looking for “lost” data.
Those commands remove volumes — and the branch volumes are exactly where your
records still live. Confirm which volume holds your data (steps above) before any
destructive operation.
Populating an empty database branch
A branch can legitimately start empty: it was created with strategy: empty, the
parent had no data when it was cloned, or you reset it. The canonical way to seed a
branch in Nanokit is nkapp db pull — it streams real data from another
environment into the active branch.
Seed with db pull (recommended)
db pull is the dedicated seed command. Point it at a source environment
(staging, production, …) and it populates the local branch with that data:
nkapp db pull app_db --from staging --branch feature-checkout--from defaults to production; --branch targets a specific local branch (the
active one if omitted).
db pull overwrites the target branch’s data with the source. Use --force
only when you intend to drop whatever is currently in the local branch.
Alternative: clone from the parent (snapshot)
If the parent branch already holds the data you want, re-clone it locally without
touching a remote. db reset discards the active branch’s contents and pulls a
fresh copy from its parent (switch to the branch first with db switch if needed):
nkapp db switch app_db --branch feature-checkout
nkapp db reset app_dbTo make new branches inherit parent data automatically, set the snapshot strategy
in nanokit.yml (the default for new branches) instead of empty:
databases:
app_db:
engine: postgres
branching:
enabled: true
strategy: snapshot # clones parent data into each new branchAlternative: fork an environment into a fresh branch
db fork creates the branch and clones a source environment’s data in one step
(add --skipPull to create it schema-only, with no data copy):
nkapp db fork app_db --sourceEnv productionInserting rows by hand
For ad-hoc records, open an interactive shell straight into the database container.
Nanokit auto-selects the right client (psql for Postgres, mongosh for Mongo,
redis-cli for Redis, …):
nkapp shell app_db --db
# then, inside psql:
# INSERT INTO users (email) VALUES ('jane.doe@example.com');Whichever method you choose, the data lands in the active branch’s isolated volume only. Other branches stay untouched, which is exactly the isolation guarantee branching is built to provide.