Zero Downtime Schema Changes with Vercel and Xata
Discover how Xata’s pgroll‑powered platform plus Vercel preview deployments enable zero‑downtime Postgres schema changes with instant branches.
Author
Divyendu SinghDate published
Modern deployment platforms like Vercel pioneered preview deployments with immutable URLs. In this post we will explore leveraging that and making schema changes easier in the whole deployment pipeline.
Zero downtime schema changes are hard. The following issues are usual in any (or all) schema change stories:
- Some migrations require a table lock and you accidentally block writes in production. Usually, because the environment where the migration is tested doesn’t have the same scale as the production in terms of traffic and data.
- Zero downtime is hard. Your application can have several instances running for redundancy and any changes in the underlying schema changes means either you need to atomically stop and start all instances or if they roll over, some instances of the application are not compatible with the new schema changes temporarily.
- The solution to the above problem is expand contract pattern but because of the nature of this pattern, you have to split a feature into several pull requests, doubling the amount of reviews, changes in the database schema and therefore time.
- Backfilling further complicates schema changes, for larger databases, backfilling might take a while further slowing down the development cycle.
Better Schema Changes with Xata
At Xata we have experience in deploying schema changes at scale and we distilled our learning into an open source tool called pgroll. Xata platform integrates with pgroll using a simple xata roll
command.
Let’s talk about the features of the platform at a higher level before diving into the code. I have mentioned some commands in this section but don’t worry too much about understand it all already. The examples later will make things clearer.
Xata platform solves the problems mentioned above by combining powerful features designed for modern development workflows.
Instant Postgres branches with xata branch create
command. The branch inherits schema and data (can be anonymized with xata clone
command) from a parent branch, not very different from how git
works:
- This allows one to test schema changes for potential locks on realistic data size as production.
- It uses CoW (copy-on-write) for branch creation, that means the new branch only stores a diff of the changes made to it. So for N branches you don’t pay N times the amount for storage but a fraction of the cost more than a single database.
Note: You will still be billed hourly for the compute time of the branches but as we will see in the examples, we will remove the branches after the PR merge cycle. Additionally, soon we will have scale to zero. Which means compute of an unused branch will shut off automatically when unused and restart automatically when a new request comes in.
- Better yet, if you are using
xata roll
to create migrations, all your migrations will be lock free by default.
In addition to better locks xata roll
also does the following:
- Multi version schemas, instead of changing the database schema in place.
xata roll
uses database views to have two views of the same database available at a time. This means that rolling over pods “see” the version of the database schema that they are compatible with. - Once all the instances have rolled over, you can use
xata roll complete
command to consolidate back to a single view. This is made clear by the following image:
pgroll multiple active versions, client applications rollout
- Multiple schemas combined with
pgroll migration
format gives you a declarative way to implement the expand contract pattern. Because we have the ability to make different version of the app “see” different schemas, we don’t need to split our expand and contract pattern into multiple pull requests. - The declarative migration format of pgroll also allows you to define the backfills declaratively. This means no longer writing backfill scripts and more importantly no longer patching holes for developers in your infrastructure to run these backfilling scripts.
Implementing Zero Downtime Schema Changes with Xata
Enough talking, let’s talk about how this really looks like in a real world use case application. We will show the use case where an application is leveraging Vercel’s preview deployments via the Vercel app integration.
First you need to create a xata project from xata.io UI or the Xata CLI:
Then add the following environment variables to the Vercel UI:

You can use the following Xata CLI commands to get these values:
Additionally, XATA_BRANCHNAME
would usually be main
(by default) and XATA_DATABASENAME
would be app
(by default). But you can always change these values.
With these environment variables you allow Xata CLI running in Vercel’s build environment to:
- Authenticate and create a new branch
- Use the provided branch as parent and make copies of it for the PRs.
Secondly, we will change the vercel’s build command to a custom script in Vercel’s dashboard. Note that you can also make this change in code using the vercel.json config file.

Let’s examine the contents of build.sh command:
It simply invokes build.ts
via bun (alternative to node) that does the heavy lifting of creating a xata branch and setting the correct URL in the DATABASE_URL
environment variable.
Let’s examine the contents of build.ts
file:
In build.ts
we do the following:
- Download the latest xata CLI
- Delete/create a xata branch, this will be an instant copy of the
main
branch that you configured via the env vars (only for branches) - We use the
xata branch wait-ready
for this branch to be ready (usually in seconds) - Then we use the
xata branch checkout
command to “checkout” the newly created branch - Then we use
xata roll migrate
to start a migration in our preview branch to test the changes in CI and from the preview deploy - Then we extract the connection string of this branch using
xata roll url
. This command gets an augmented connection string that sets thesearch_path
to the version the current application supports (by looking at the versioned migrations folder). If you will run the same command from themain
branch of the codebase, it will use thesearch_path
corresponding to that codebase and the deployed application will always “see” the schema it is compatible with.
This is all we need to have the multi version schemas and preview branches with realistic data. Additionally, we (optionally) need the following actions that are documented here:
check-merge-readiness.yml
- We started the migration when we merge to main but we never complete it. Other PRs should see this and at one time, we should only have one migration running in themain
branch. This action simply checks if something is running on the main branch and blocks the current PR from merging. You will have to executexata roll complete
against main (and you can write another action to do that that you can execute on demand)xata-clone.yml
- you might be using Xata platform for staging and production environments. In that case, you can usexata clone
command to create a anonymized copy of your production database on a nightly schedule. Additionally, you will need a way to run and complete migrations against your production database. Something that automatically happens in thebuild.ts
shown above if you host your production with xata platform.
Conclusion
In this post, we explored the challenges of schema migrations and how the Xata platform helps streamline testing and deploying changes at scale.
Thank you for reading! We look forward to having you try the platform. If you'd like early access, you can join our private beta today.
Related Posts
Anatomy of Table-Level Locks in PostgreSQL
This blog explains locking mechanisms in PostgreSQL, focusing on table-level locks that are required by Data Definition Language (DDL) operations.
Xata: Postgres with data branching and PII anonymization
Relaunching Xata as "Postgres at scale". A Postgres platform with Copy-on-Write branching, data masking, and separation of storage from compute.