Zero-downtime database migration keeps old and new application versions readable against one shared schema while you change it. Expand-contract steps, dual-write pitfalls, safe cutover, and how Encore applies Postgres up migrations automatically.
A zero-downtime database migration changes the schema of a live database without taking the application offline. Requests keep succeeding while old and new code share one Postgres (or other relational) instance, which means every migration step has to stay readable by both revisions until the cutover finishes.
This is separate from zero-downtime deployment, which covers rolling, blue-green and canary releases of compute. Here the shared state is the problem: a breaking ALTER in the same release as the code that needs it removes the rollback path and drops requests during the overlap window.
Expand is additive: ADD COLUMN with a default or nullability the old binary accepts, new tables for a renamed concept, or a new index built concurrently where the engine allows it. PostgreSQL's CREATE INDEX CONCURRENTLY avoids a full write lock for many index builds; plain ALTER TABLE ... ALTER COLUMN TYPE often does not.
Contract is the cleanup release after metrics show no traffic on the old path and every old task is gone. Dropping a column in the expand deploy is the classic outage: the previous revision still selects that column during the rolling update.
On MySQL, online schema change tools such as gh-ost or pt-online-schema-change help when a rewrite would lock a large table for too long. On Postgres, most additive expands are ordinary migrations plus CREATE INDEX CONCURRENTLY where needed; the discipline is still compatibility across two app versions, not the tool brand.
Dual-write means the application updates the old and new representations in one request path. It looks like insurance and often creates silent divergence:
A safer sequence is dual-read (new code prefers the new column, falls back to the old), then dual-write with an idempotent reconciliation job, then single-write to the new shape, then contract. For renames, a generated column or a view can bridge without two writers.
Amazon RDS and similar managed Postgres services do not remove this application-level choreography; they only host the engine you migrate.
Encore works alongside the frameworks and services you already run, and the Postgres database you declare is infrastructure from code. The platform provisions and deploys into your own AWS or GCP account: you declare a Postgres database next to the handlers that use it, keep schema changes as numbered SQL files, and Encore provisions the managed database in your own cloud when you connect the account.
Per the TypeScript database docs (and the matching Go database docs), Encore automatically applies up migrations and requires down migrations to be run manually. That matches expand-contract practice: forward steps are part of deploy; undo of schema is an explicit operation, not a side effect of redeploying an old image.

Run in your terminal to get started locally.
Declare the database and keep an expandable migration for a column rename that must not drop traffic:
// catalog/items.ts
import { api } from "encore.dev/api";
import { SQLDatabase } from "encore.dev/storage/sqldb";
const db = new SQLDatabase("catalog", { migrations: "./migrations" });
export const getItem = api(
{ expose: true, method: "GET", path: "/items/:id" },
async ({ id }: { id: number }): Promise<{ title: string }> => {
// Dual-read during expand: prefer the new column, fall back to the old name.
const row = await db.queryRow<{ title: string | null; name: string }>`
SELECT title, name FROM items WHERE id = ${id}
`;
return { title: row?.title ?? row?.name ?? "" };
},
);
Add the expand migration in the same change, without dropping name yet:
-- catalog/migrations/2_add_title.up.sql
-- Expand only: old code still selects "name".
ALTER TABLE items ADD COLUMN IF NOT EXISTS title TEXT;
-- Backfill separately in resumable batches after this migration,
-- not as one UPDATE over the whole table.
encore run starts Postgres locally and applies pending up files before your APIs serve. On deploy to AWS, Encore creates an RDS for PostgreSQL instance by default (or Aurora, imported RDS, or Neon when you choose those in the environment), runs the migrations, and gives each service a database role scoped to the databases it uses, as listed in the AWS infrastructure docs. Encore provisions an IAM role for the service. A later 3_drop_name.up.sql that drops name belongs in a contract release after dual-read is gone, not in the expand deploy.
An AI coding agent can add the expand migration and dual-read handler in one pull request, validate with encore run, and leave production cutover to a reviewed deploy. Secret values stay out of the repo via encore secret set when the service needs credentials beyond the injected database role.
Best fit: Teams that want infrastructure from code for Postgres in their own cloud, with migrations that stay backward compatible across rolling deploys, working alongside whatever HTTP framework or service layout they already use.
A zero-downtime database migration changes the schema while production keeps serving requests on the shared database. Old and new application versions must both work against every intermediate schema, which is why teams use expand-contract instead of a single breaking ALTER in the same deploy as the new code.
Expand adds new columns, tables or indexes that the old code can ignore. You then migrate or dual-write data and deploy code that uses the new shape. Contract removes the old columns or dual paths only after every instance of the old revision is gone.
Dual-write fails when only one path is updated on retries, when readers still trust a single store during the cutover, or when you drop the old column before the last old instance exits. Prefer dual-read first, then dual-write with reconciliation, then a single writer before contract.
Declare a SQLDatabase with a migrations folder of numbered .up.sql files. Encore applies up migrations automatically on local encore run and on deploy, and down migrations must be run manually. Keep each up step backward compatible so rolling deploys and redeploys of the previous commit stay safe.
Deploy to your cloud on AWS and Google Cloud
without managing infrastructure
Encore lets developers and agents define application infrastructure directly in code, then automatically provisions it from local development to production in your AWS or GCP account.