· 33 min read · Zeabur Zeabur migration Supabase Cloudflare Workers
Zeabur Migration Guide (with a copy-paste AI prompt): Moving to Supabase and Cloudflare Workers
Zeabur shuts down on Dec 31, and refund claims close Nov 30. A step-by-step guide to moving databases, files, sites, APIs, cron jobs, n8n and domains to Supabase and Cloudflare Workers.
This guide is for anyone who still has services running on Zeabur and has to move them before the end of the year. For most projects, the simplest Zeabur migration is to move the database, files and auth to Supabase, and the website, API, scheduled jobs and domains to Cloudflare. If you are looking for a Zeabur alternative after the Zeabur shutdown announcement, this is the setup I run myself: kelvinwang.info is Astro on Cloudflare Workers with its data in Supabase, so every step below is written for that stack. Commands follow the official docs as I checked them on Nov 3, 2026.
I covered why Zeabur is shutting down and how the two breaches happened in part 1 and part 2. This article is only about how to migrate from Zeabur. It also looks at whether the self-hosted version Zeabur has open-sourced can serve as a replacement.
Copy-paste prompt: let an AI agent do the Zeabur migration with you
This article is long. If you don't want to read all of it, copy the prompt below into an AI coding agent that can read web pages and run commands on your machine (Claude Code or Codex, for example). It will walk you through the migration using this article and Zeabur's official guide, and it will ask before every step that changes anything. Keep the rest of this article as the reference for checking its work.
Help me migrate the services I currently run on Zeabur to Supabase (database, files, auth) plus Cloudflare Workers (website, API, scheduled jobs, domains).
Read these two documents first and follow their steps:
1. Zeabur's official migration guide: https://zeabur.com/blogs/migrate-from-zeabur.md
2. Full migration walkthrough: https://kelvinwang.info/en/blog/zeabur-migration-supabase-cloudflare/
Rules:
- Do not ask me to paste Zeabur, Cloudflare or Supabase tokens or passwords into this chat. Ask me to run npx zeabur@latest auth login and npx wrangler login in my own terminal, and use the logged-in CLIs.
- Start read-only: inventory every project's services, code source, environment variables (split into ones I set and ones Zeabur injects), database type and version, volumes, domains, scheduled jobs, webhooks and OAuth callback URLs, and show them to me as a table.
- Based on the inventory, propose where each service should go (Workers, Hyperdrive, Supabase Storage, R2, Cron Triggers, Containers or a VPS) and why, and wait for my approval before doing anything.
- Before every step that changes something (creating resources, importing data, changing DNS, deleting services), explain what you are about to do and wait for my confirmation.
- Generate new keys and passwords for the new environment. Do not reuse the old values from Zeabur. At the end, give me a list of old keys to revoke.
- Export each database separately with pg_dump. Do not restore Zeabur's backup data.sql directly. After importing into Supabase, revoke the public schema privileges from anon and authenticated, enable RLS, and compare row counts table by table.
- If there is n8n: migrate only the workflows, start the new instance with a new N8N_ENCRYPTION_KEY, and recreate every credential with new third-party keys.
- Put the new services on a test subdomain first. Switch the production domain only after I have tested them by hand.
- When done, give me a list of: each service's new location, what I still need to do by hand, and the refund and compensation tickets I have to submit before Nov 30, 2026.
Zeabur shutdown timeline: the dates that matter
From Zeabur's official announcements (the shutdown notice on Oct 17, and the Oct 22 post on migration, refunds and security claims):
| Date | What happens |
|---|---|
| Nov 30, 2026 | Zeabur's recommended date to finish migrating. Balance refunds and claims for the October security incident must both be filed through a Zeabur support ticket by this date. Late claims are not accepted; late refund requests go through email instead. |
| Dec 1, 2026 | Running services on the shared clusters are suspended. You can still resume them by hand to back up your data. Dedicated servers and paid plans stop renewing, and each one shuts down on its own expiry date. |
| Dec 31, 2026 | All services stop and their data is deleted. |
Watch out for dedicated servers: each one shuts down and deletes its data on its own expiry date, which can be earlier than Dec 31. You have to check that date in the Dashboard. The Zeabur CLI does not show it.
Zeabur went open source. Can I self-host it and keep going?
On Oct 24, 2026 Zeabur created the GitHub organization zeabur-oss and released, under Apache-2.0, a "self-hosted PaaS on your own Kubernetes". It includes Zeabur (the web UI), Zeabur Identity, Zeabur Builder, Zeabur Catalog and other projects, and the official landing page is oss.zeabur.com. As of Nov 3, the latest release is v0.1.2-beta.0 from Nov 2. The README labels it pre-alpha, says the API may still change and that it has been tried on few clusters, and warns you not to point it at a cluster you cannot afford to break.
In short, it is a tool for deploying services on your own servers with a Zeabur-like interface. You still have to move your data yourself, and you are responsible for the servers, backups and security. If you know Kubernetes and want to keep a similar workflow, it is worth a look. If you want someone else to run the infrastructure, moving to managed services as described here is less work.
It also does nothing for services running on Zeabur's hosted platform today. After Dec 31 those services still stop and their data is still deleted.
The target architecture in 30 seconds
- Supabase: Postgres database (daily backups on the Pro plan), file storage (Storage), and auth (Auth) if you need it.
- Cloudflare Workers: frontend static files (static assets), SSR sites, backend APIs, scheduled jobs (Cron Triggers), background jobs (Queues).
- Other Cloudflare services: DNS and domains, R2 (bulk files, no egress fees), Hyperdrive (fast Postgres connections from Workers), Workers Secrets (replacing Zeabur's environment variables).
- Long-running processes that don't fit in Workers: Cloudflare Containers, or a small VPS.
After the move, deploying is git push or npx wrangler deploy, Supabase handles backups, and Cloudflare takes care of domains and certificates automatically.
Zeabur to Supabase and Cloudflare: what replaces each component
Zeabur's official migration guide lists what Zeabur did for you that you will have to handle yourself after leaving. Mapped to Supabase + Cloudflare:
| On Zeabur | Replace with | Notes |
|---|---|---|
| Zeabur PostgreSQL | Supabase Postgres | Move with pg_dump / pg_restore |
| Zeabur MySQL / MariaDB | Supabase Postgres | Convert with pgloader, or use another MySQL host |
| Zeabur Redis | Upstash Redis, Workers KV, Queues, Durable Objects | Depends on use, see below |
| Volumes (mounted files) | Supabase Storage or R2 | Code must read and write files through an API |
| Automatic builds from Git, images in Zeabur's registry | Workers builds (Workers Builds / wrangler deploy), your own Dockerfile for Containers |
The registry goes offline at shutdown and images can no longer be pulled |
| Frontends, static sites | Workers static assets | Static asset requests are free |
| Backend APIs (Node, Express, etc.) | Workers (rewriting in Hono is recommended), Hyperdrive for the database | |
| Long-running services (n8n, bots, WebSocket servers) | Cloudflare Containers or a VPS | |
Environment variables, cross-service variable injection (${POSTGRES_HOST}) |
wrangler secret put and bindings |
Fill in the actual values |
.zeabur.internal private networking |
Service bindings, Hyperdrive, Containers bindings | |
*.zeabur.app domains |
Your own domain as a Workers Custom Domain | *.zeabur.app stops working at shutdown |
| Automatic HTTPS certificates | Handled automatically by Cloudflare | |
| Daily database backups | Supabase Pro daily backups (kept 7 days) | The Free plan has no backups |
| Scheduled jobs | Cron Triggers (UTC) | |
| One-click templates (n8n, etc.) | Official Docker image + Containers or a VPS | See the n8n section |
Before you migrate from Zeabur: inventory and rotate every key
Make the inventory first
For each Zeabur project, write these down in one document:
- Service name, code source (which repo and branch), runtime version (Node or Python version), start command.
- Each service's environment variables, split into "set by me" and "injected by another service" (database connection strings and the like). The Zeabur CLI's
variable listlists the two separately. Note that values in${VAR}form are expanded at deploy time, so what you export may still be unexpanded. - Database type, major version, data size.
- Volume mount paths and sizes.
- Domains: which are
*.zeabur.app, which are your own, and whether any were bought through Zeabur. - Scheduled jobs, webhook callback URLs, OAuth callback URLs, and URLs hard-coded in payment provider settings.
- Expiry dates of dedicated servers.
Zeabur also published a final checklist before leaving that uses CLI commands (such as zeabur project list, zeabur server list, zeabur domain list-registered) to find items still being billed and data you haven't exported yet. Run through it once before requesting your refund.
Rotate every key; don't carry old ones over
Zeabur's preliminary report on the October incident is explicit: about 3.21 million service environment variables (names and values) in the platform database were read, and the environment variables and secrets of every service on the shared clusters are in scope. Zeabur asks all users to rotate every credential in their environment variables, including API keys, cloud keys, database passwords, and signing and encryption keys. Keys you already rotated after the August incident need to be rotated again.
So the rule for the migration is simple: the new environment only gets newly generated keys. Create new keys at each third-party service (OpenAI, Stripe, AWS, Google OAuth and so on), put them in the new environment, confirm everything works, and then revoke the old ones. That way each key is rotated once and there is no downtime. When you replace a JWT or session signing key, every user gets logged out once. That is expected.
Two more things:
- Remove Zeabur's GitHub App only after everything has moved. Otherwise your services on Zeabur can no longer redeploy.
- If your PostgreSQL uses the username and password Zeabur gave you, treat that password as leaked. On Supabase you will use the new password Supabase generates. Don't reuse the old one anywhere.
Using an AI coding agent: keep tokens out of the chat
Zeabur's migration guide is written to be read by coding agents such as Claude Code and Codex. The officially suggested usage is to tell your agent:
Read https://zeabur.com/blogs/migrate-from-zeabur.md and help me migrate my services off Zeabur.
A few rules in that guide are worth following:
- Don't paste your Zeabur access token into the chat. In Zeabur's words, the token can access your entire account, so pasting it into a chat hands over the account. Run
npx zeabur@latest auth loginon your own machine instead and let the agent use the logged-in CLI. - On dedicated servers, the output of
kubectl get deploycontains expanded environment variables in plain text. Treat it as secret. - The same applies to the new environment: use
npx wrangler loginfor Cloudflare, and keep the Supabase database password in a local environment variable or an.envfile that is not in git, so it never shows up in a chat transcript.
Step 1: Move the database to Supabase
Create a Supabase project and get the connection string
You set a database password when you create the Supabase project. That is your new password. The connection strings are in the Connect dialog in the Dashboard, in three forms:
Direct: postgresql://postgres:[YOUR-PASSWORD]@db.[PROJECT-REF].supabase.co:5432/postgres
Session pooler: postgresql://postgres.[PROJECT-REF]:[YOUR-PASSWORD]@[POOLER-HOST]:5432/postgres
Transaction pooler: postgresql://postgres.[PROJECT-REF]:[YOUR-PASSWORD]@[POOLER-HOST]:6543/postgres
The differences that matter:
- Direct uses IPv6 by default. If your network is IPv4-only, it won't connect. Pro and higher plans can buy an IPv4 add-on.
- Session pooler (port 5432) works over IPv4, and it is what Supabase's Postgres migration guide uses for
pg_dump/pg_restore. If you are not sure your home network has IPv6, use this one. - Transaction pooler (port 6543) is for serverless environments with many short-lived connections.
Export from Zeabur PostgreSQL
Get the external connection details from the Instructions or Networking tab of the Zeabur service. If the service has no external access, the Zeabur CLI has zeabur service port-forward --enable to open it. From Dec 1, services on the shared clusters are suspended, and you have to resume them by hand before you can export.
Your local pg_dump must match the source database's major version (both the Zeabur and Supabase guides require this). Check the version first:
psql "$ZEABUR_DB_URL" -c "select version();"
Then export each database separately with pg_dump:
pg_dump "$ZEABUR_DB_URL" \
--format=custom \
--no-owner \
--no-privileges \
--no-subscriptions \
--file=zeabur.dump
--no-owner --no-privileges skips ownership and privilege settings so they don't clash with the roles Supabase manages. For large databases, Supabase's guide uses --format=directory --jobs=N for a parallel export. --jobs only works with the directory format.
Don't restore the data.sql from a Zeabur backup directly
A Zeabur PostgreSQL automatic backup unpacks to a data.sql produced by pg_dumpall. Velox Data tested it and found that the file starts with CREATE ROLE root; and ALTER ROLE root ... PASSWORD. After restoring it with psql, the root password is back to the old one from Zeabur. Zeabur's own docs say the same: restoring a backup overwrites existing data and user passwords.
That old password is within the leaked data, so restoring it carries the risk straight into your new database. Zeabur's migration guide also recommends exporting each database with pg_dump, because a pg_dumpall file can overwrite the passwords of new roles.
If data.sql is all you have, at least delete the role lines before restoring, and change the password right after:
grep -vE '^(CREATE|ALTER) ROLE' data.sql > data.clean.sql
Open the file and look before you restore. pg_dumpall output can also contain commands that create and switch databases, and you need to make sure the data ends up in Supabase's postgres database.
Import into Supabase
export SUPABASE_DB_URL="postgresql://postgres.[PROJECT-REF]:[YOUR-PASSWORD]@[POOLER-HOST]:5432/postgres"
pg_restore \
--dbname="$SUPABASE_DB_URL" \
--no-owner \
--no-privileges \
--verbose \
zeabur.dump 2>&1 | tee restore.log
psql "$SUPABASE_DB_URL" -c "VACUUM VERBOSE ANALYZE;"
According to Supabase's guide, a few things don't come across:
- Roles and privileges: you have to recreate them.
- Extensions: if the restore reports extension errors, check that the extension is listed in
pg_available_extensionson Supabase, runCREATE EXTENSION IF NOT EXISTS extension_name;, and restore again. - RLS settings: you have to enable them again table by table.
First thing after the import: lock down Data API access
Many people miss this step. Supabase exposes the public schema through its Data API, and the Supabase docs state that a table in an exposed schema without RLS can be read and written by any role that has privileges on it. In existing projects, new tables in public start with all privileges granted to roles such as anon and authenticated. In other words, the tables you brought over from Zeabur may be readable and writable by anyone holding your public publishable key.
If your code only reaches the database through your own backend (Workers + Hyperdrive) and you don't plan to use Supabase's Data API, the most direct fix is to revoke the privileges and enable RLS:
revoke all on all tables in schema public from anon, authenticated;
revoke all on all sequences in schema public from anon, authenticated;
revoke all on all functions in schema public from anon, authenticated;
-- Stop new tables, sequences and functions from being granted automatically
alter default privileges for role postgres in schema public revoke all on tables from anon, authenticated;
alter default privileges for role postgres in schema public revoke all on sequences from anon, authenticated;
alter default privileges for role postgres in schema public revoke all on functions from anon, authenticated;
do $$
declare r record;
begin
for r in select schemaname, tablename from pg_tables where schemaname = 'public' loop
execute format('alter table %I.%I enable row level security', r.schemaname, r.tablename);
end loop;
end $$;
The first three lines only cover objects that exist now; the alter default privileges lines keep new objects created by the postgres role from being granted to anon and authenticated automatically. If you later want the frontend to read data directly with supabase-js, add grants and policies for those specific tables.
Compare row counts
Zeabur's guide asks you to compare row counts table by table after the import. Run this on both sides and diff the results:
select table_name,
(xpath('/row/c/text()',
query_to_xml(format('select count(*) as c from %I.%I', table_schema, table_name), false, true, '')))[1]::text::bigint as rows
from information_schema.tables
where table_schema = 'public' and table_type = 'BASE TABLE'
order by table_name;
On MySQL: convert to Postgres with pgloader
Supabase's MySQL migration guide uses pgloader. Create config.load:
load database
from mysql://user:password@host/source_db
into postgres://postgres.xxxx:password@xxxx.pooler.supabase.com:5432/postgres
alter schema 'public' owner to 'postgres';
set wal_buffers = '64MB', max_wal_senders = 0, statement_timeout = 0, work_mem to '2GB';
Then run pgloader config.load. Use the Session pooler connection string as the target; the username is postgres.xxxx including your project ref. The file contains passwords in plain text, so delete it when you're done.
After the conversion, check:
- Which schema the tables landed in. Look with
\dnand\dt *.*. pgloader's examples includeALTER SCHEMA '...' RENAME TO '...'to adjust this. - Type conversions. By default pgloader turns
tinyint(1)into boolean and zero dates such as0000-00-00into NULL. Any code that checks for those values needs to change too. - SQL syntax. MySQL-specific syntax in your application (backticks,
LIMIT x, y,ON DUPLICATE KEY UPDATE) has to be rewritten for Postgres.
If rewriting the SQL is too much work, switching to another MySQL host is a reasonable choice. Hyperdrive on Workers supports MySQL as well.
Redis depends on what you use it for
There is no one-to-one replacement for Redis on Zeabur. Pick by use case:
| Use | Recommendation | Why |
|---|---|---|
| Caching, sessions, counters | Upstash Redis | In Workers, use @upstash/redis/cloudflare over HTTP; regular Redis clients (TLS) also work |
| Read-heavy config, caching | Workers KV | Eventually consistent; other locations can take 60 seconds or more to see updates; not suited to atomic operations or frequent writes to the same key |
| Background job queues | Cloudflare Queues | Producers and consumers need rewriting |
| Locks, rate limiting, strongly consistent state | Durable Objects |
If your background jobs are a long-running worker process built on a Redis queue library and you don't want to rewrite it, put that process on Containers or a VPS and switch Redis to Upstash.
Step 2: Move files and volumes to Supabase Storage or R2
Workers have no local disk, so files that used to go to a volume need to move to object storage:
- Supabase Storage: integrated with the database and auth, and you can use RLS to control who can read which file. Good for user-uploaded avatars and attachments.
- R2: free egress and 10 GB free storage per month. Good for large numbers of files, public downloads and images.
Both are S3-compatible, so you can move files with rclone. The rclone config for R2:
[r2]
type = s3
provider = Cloudflare
access_key_id = abc123
secret_access_key = xyz456
endpoint = https://<accountid>.r2.cloudflarestorage.com
acl = private
npx wrangler r2 bucket create my-uploads
rclone copy ./volume-backup r2:my-uploads/
Supabase Storage's S3 endpoint is https://<project_ref>.storage.supabase.co/storage/v1/s3, and you generate access keys in the Storage settings. Those keys have full access and bypass RLS, so keep them on the server only.
Getting the files out of Zeabur: on a dedicated server you can SSH in and copy from /var/lib/rancher/k3s/storage/. The shared clusters have no SSH, but you can run commands inside the container with zeabur service exec -- <command>, for example to pack the files and upload them directly. Which tools you can use depends on what is installed in the image.
Step 3: Move frontends and static sites to Workers
Cloudflare's docs say Workers cover more than Pages, and they provide a guide for migrating from Pages to Workers. The Astro Cloudflare adapter dropped Pages support in v13. For new projects, just use Workers. A purely static site needs only one wrangler.jsonc:
{
"name": "my-site",
"compatibility_date": "2026-11-03",
"assets": {
"directory": "./dist/"
}
}
For single-page apps (SPAs) such as React or Vue, add one line so unknown paths return index.html:
{
"name": "my-spa",
"compatibility_date": "2026-11-03",
"assets": {
"directory": "./dist/",
"not_found_handling": "single-page-application"
}
}
After building, run npx wrangler deploy and you're live. Static asset requests are free and unlimited, and they don't count toward your Workers requests.
Step 4: Move backend APIs to Workers, with Hyperdrive for the database
Two ways to connect to Supabase
- supabase-js only: if your code already uses the Supabase client, or you're willing to switch to it, Workers call Supabase over HTTP and need no database connection. This is how my site works.
- A SQL driver (pg, an ORM): go through Hyperdrive. Hyperdrive keeps a connection pool on Cloudflare's side, so Workers don't open a new connection on every request.
Create the Hyperdrive config. Cloudflare's Supabase example recommends the Direct connection string rather than the pooler, because Hyperdrive does its own pooling:
npx wrangler hyperdrive create my-db \
--connection-string="postgresql://postgres:[YOUR-PASSWORD]@db.[PROJECT-REF].supabase.co:5432/postgres"
Hyperdrive actually connects to the database once at creation time to verify the credentials. If that fails, try the Session pooler string instead.
Add the binding to wrangler.jsonc:
{
"name": "my-api",
"main": "src/index.ts",
"compatibility_date": "2026-11-03",
"hyperdrive": [
{ "binding": "HYPERDRIVE", "id": "<YOUR_HYPERDRIVE_ID>" }
]
}
Database drivers need Node.js compatibility mode. It is on by default for compatibility dates after 2026-08-04; with an earlier date you have to add "compatibility_flags": ["nodejs_compat"] yourself.
import { Client } from "pg";
export interface Env {
HYPERDRIVE: Hyperdrive;
}
export default {
async fetch(request, env, ctx): Promise<Response> {
const client = new Client({ connectionString: env.HYPERDRIVE.connectionString });
await client.connect();
const result = await client.query("select now()");
return Response.json(result.rows);
},
} satisfies ExportedHandler<Env>;
You can also connect from Workers to Supabase without Hyperdrive: use the Transaction pooler (port 6543), create the client at module level and reuse it, set the pool size to 1, turn off prepared statements, and require SSL.
Read the Workers limits first
| Item | Free | Paid (from $5/month) |
|---|---|---|
| Requests | 100,000 per day | 10 million per month, then $0.30 per million |
| CPU time per request | 10 ms | 30 seconds by default, configurable up to 5 minutes |
| Memory per isolate | 128 MB | 128 MB |
| Cron Triggers (per account) | 5 | 250 |
| Simultaneous outgoing connections per request | 6 | 6 |
The Free plan gives you only 10 ms of CPU per request, which an SSR site or a slightly heavy API will easily exceed. For production services, go straight to Paid.
What doesn't belong in Workers
- Processes that must stay running: long polling for Discord / Telegram bots, your own background worker loops.
- Programs that need a local file system or depend on native libraries.
- Jobs that need more than 128 MB of memory at once.
- WebSocket servers originally written in Node (Socket.IO, for example). On Cloudflare, a WebSocket server has to be rewritten as a Durable Object, and Cloudflare recommends its Hibernation WebSocket API.
These go on Containers or a VPS, covered next.
Step 5: Long-running services on Cloudflare Containers or a VPS
Cloudflare Containers became generally available on Apr 13, 2026 and requires the Workers Paid plan. A Worker controls the containers: requests arrive at the Worker, which forwards them to a container instance, and each container is backed by a Durable Object.
What to know up front:
- The disk is ephemeral by default. When a container sleeps and starts again, it gets a fresh disk rebuilt from the image, and anything written earlier is gone. Store data you need to keep in Supabase or R2. The file system snapshots released in September are still in public beta.
- Idle containers sleep.
sleepAftercontrols how long a container stays idle before sleeping; it starts again on the next request. - The image must run on
linux/amd64, and Docker must be running locally when you runwrangler deploy.
Example config (from the official Get started guide):
{
"containers": [
{ "max_instances": 10, "class_name": "MyContainer", "image": "./Dockerfile" }
],
"durable_objects": {
"bindings": [{ "name": "MY_CONTAINER", "class_name": "MyContainer" }]
},
"exports": {
"MyContainer": { "type": "durable-object", "storage": "sqlite" }
}
}
import { Container } from "@cloudflare/containers";
export class MyContainer extends Container {
defaultPort = 8080;
sleepAfter = "10m";
}
export default {
async fetch(request, env) {
return env.MY_CONTAINER.getByName("main").fetch(request);
},
};
Services on Zeabur are already Docker images, so the main thing you'll need for Containers is a Dockerfile. Zeabur builds automatically with zbpack. Projects without a Dockerfile need one written, with the runtime version and the install, build and start commands matching what zbpack used. Zeabur's guide also reminds you to pin floating tags such as latest to the version you are actually running now.
When to pick a VPS: the service has to run 24 hours a day (no sleeping), needs a persistent disk, or you already know Docker Compose. In that case a small VPS running Docker Compose, still connected to Supabase for the database, is usually simpler and cheaper. On a VPS you handle the firewall, TLS, automatic restarts and monitoring yourself.
Step 6: Environment variables become wrangler secrets
Within a project, Zeabur automatically injects variables into other services, such as ${POSTGRES_CONNECTION_STRING}, ${POSTGRES_HOST} (the hostname usually ends in .zeabur.internal) and ZEABUR_<PORT>_URL. Zeabur's guide names injected variables as one of the two most common points of failure. During the migration, replace every one of them with the actual value from the new environment.
On Workers, secrets go in as secrets, not in vars in wrangler.jsonc:
npx wrangler secret put OPENAI_API_KEY
To upload many at once, use a file in JSON or .env format (up to 100 per upload):
npx wrangler secret bulk .env.production
For local development use either .dev.vars or .env, and add it to .gitignore. In code, read it from env.OPENAI_API_KEY.
Once more: this .env.production should contain newly generated keys.
Step 7: Scheduled jobs become Cron Triggers
{
"triggers": {
"crons": ["0 1 * * *"]
}
}
export default {
async scheduled(controller, env, ctx) {
// Every day at 01:00 UTC, which is 09:00 Taiwan time (UTC+8)
},
};
Cron Triggers run on UTC, so subtract 8 hours from Taiwan time (UTC+8), or use your own offset. On the Paid plan, schedules that run more often than once an hour get up to 30 seconds of CPU per run, and schedules of one hour or longer get up to 15 minutes. For heavier work, have the cron push tasks into Queues and let a consumer process them in batches:
{
"queues": {
"producers": [{ "queue": "jobs", "binding": "JOBS" }],
"consumers": [{ "queue": "jobs", "max_batch_size": 10 }]
}
}
npx wrangler queues create jobs
Step 8: Replace the .zeabur.internal private network
Services on Zeabur talk to each other through xxx.zeabur.internal, and that DNS stops working at shutdown. The Cloudflare equivalents:
-
A Worker calling another Worker: use Service bindings, which don't go over the public internet.
{ "services": [{ "binding": "API", "service": "my-api" }] }In code, call it with
env.API.fetch(request). -
Connecting to the database: a Hyperdrive binding.
-
Connecting to a container: a Durable Object binding (
MY_CONTAINERin the Containers example above).
Find and replace every .zeabur.internal and .zeabur.app URL hard-coded in your code:
grep -rnE 'zeabur\.(internal|app)' --exclude-dir=node_modules .
Step 9: Switch domains and DNS with minimal downtime
Domains bought through Zeabur
Transfer them out from the Zeabur Dashboard to another registrar (Cloudflare Registrar, for example). Transfers take time, so start as early as you can.
Cutover steps
-
Lower the TTL ahead of time: Zeabur's guide recommends lowering your DNS records' TTL to 300 seconds in advance and writing down all current DNS records as your rollback reference.
-
Add the domain to Cloudflare: add the domain in Cloudflare, check the DNS records it detects, then change the nameservers at your registrar to the two Cloudflare gives you. This only moves DNS hosting. The records stay the same and the site still points to Zeabur.
-
Verify on a test subdomain first: for example, put the new Worker on
new.example.comand test it fully. -
Stop writes: once you're ready, suspend the application services on Zeabur (leave the database running) so the data stops changing.
-
Do one final data sync: run
pg_dump/pg_restoreagain and compare row counts. -
Switch: delete the CNAME pointing to Zeabur and add a Custom Domain to the Worker. Cloudflare's docs state that you can't create a Custom Domain on a hostname that already has a CNAME record, so delete it first:
{ "routes": [{ "pattern": "example.com", "custom_domain": true }] } -
Verify from outside: open the site over mobile data and test login, payments and webhook callbacks by hand.
Downtime falls between steps 4 and 6, and its length depends mostly on how long the final data sync takes. Time it once during testing and you'll know.
Quick notes by framework
Next.js
Cloudflare currently recommends vinext for running Next.js on Workers, and keeps OpenNext for existing projects that can't migrate yet. vinext is still in beta, so run the check before going to production:
npx vinext check
npx vinext init
App Router, Pages Router, Server Actions, ISR and middleware are on the supported list; image optimization is partially supported. If you use a coding agent, you can also run npx skills add cloudflare/vinext first and then ask the agent to do the migration.
Astro and purely static sites
A purely static Astro site doesn't need an adapter; the static assets config above is enough. For SSR:
npx astro add cloudflare
This adapter deploys straight to Workers, and v13 dropped Pages support. My site runs on this setup.
Nuxt, SvelteKit, Vue, React Router and others
Cloudflare's Workers framework guides have a page for each. Set up the adapter following the matching guide.
Node.js / Express
Express depends on Node's HTTP server. On Workers, rewriting it in Hono is recommended, and the routing style is very similar:
npm create hono@latest my-api
# pick the cloudflare-workers template
Environment variables and bindings are read from c.env. If the API is large and you can't finish the rewrite soon, package the whole Express service as a Docker image and put it on Containers first, then move it over gradually.
Python
Python Workers support pure Python packages and Pyodide packages, and the docs use FastAPI as their example. Packages that need C extensions or native libraries may not have a WebAssembly build, so for those projects it is more practical to run the original Docker image on Containers or a VPS.
Migrating n8n and other one-click templates
Does n8n work on this setup?
Yes, but n8n is a long-running server and can't go on Workers. The database can be Supabase Postgres, and n8n itself goes on a VPS or Containers.
I'd lean toward a VPS. n8n's schedules are triggered by the n8n process itself, so while a container sleeps the process isn't running and the schedules don't fire. To run it on Containers you'd have to keep it awake, and you'd pay for the full day (see the cost section below for an estimate).
Environment variables for connecting n8n to Supabase:
DB_TYPE=postgresdb
DB_POSTGRESDB_HOST=<POOLER-HOST>
DB_POSTGRESDB_PORT=5432
DB_POSTGRESDB_DATABASE=postgres
DB_POSTGRESDB_USER=postgres.<PROJECT-REF>
DB_POSTGRESDB_PASSWORD=<new password>
DB_POSTGRESDB_SCHEMA=n8n
N8N_ENCRYPTION_KEY=<newly generated random string>
This uses the Session pooler (n8n is a long-running process, and it works on IPv4 networks). DB_POSTGRESDB_SCHEMA is set to a separate n8n schema so n8n's tables don't land in public, which Supabase exposes. Remember to run create schema n8n; first.
N8N_ENCRYPTION_KEY is the key part
n8n uses this key to encrypt the third-party credentials stored in its database. In a normal move, the new n8n has to keep the same key, or it can't decrypt the old credentials.
Zeabur is a different case. The incident report says that of 607 n8n instances sampled, 602 kept the encryption key in environment variables, which means the third-party credentials stored in them have leaked. Zeabur's instruction is to change the n8n encryption key and recreate all third-party credentials. So the recommended approach is:
-
Export the workflows inside the n8n container on Zeabur:
n8n export:workflow --backup --output=backups/latest/ -
Start the new n8n with a new
N8N_ENCRYPTION_KEYand import the workflows:n8n import:workflow --separate --input=backups/latest/ -
At each third-party service, revoke the old key and generate a new one, recreate the credentials in the new n8n, and reconnect them to each workflow.
n8n has export:credentials --decrypted to export credentials in plain text, but those credentials have already leaked and would need replacing anyway, so it's better to recreate them directly. Imported workflows are deactivated by default; activate them one by one after checking them.
Other one-click templates
If a template deploys open-source software, the approach is similar to n8n: find the software's official Docker image and config docs, pin the version, put it on a VPS or Containers, connect the database to Supabase (if it supports Postgres) and files to R2. Zeabur's October incident report also tells new-api / one-api users to revoke their upstream AI service keys and regenerate access tokens. Services like these need everything replaced during the move as well.
Verification and cutover checklist
Before the cutover:
- Every service has a counterpart in the new environment, and
grepfinds nozeabur.app/zeabur.internal - All keys are newly generated, and the old keys are on a revocation list
- Row counts match table by table
- Tables in Supabase
publichave RLS handled oranonprivileges revoked - Volume files are in Storage / R2, and a spot check of a few files downloads fine
- On the test subdomain: login, sign-up, payments, uploads, outgoing email and webhooks all tested by hand
- The new URLs are added to OAuth (Google, GitHub, etc.) callback URLs and to notification URLs in payment provider dashboards
- Cron schedules are converted to UTC correctly
- Supabase backups are running, and you have actually restored one into another project (Zeabur's guide points out that having a dump file doesn't mean you've tested a restore)
After the cutover:
- The site opens from an outside network with working HTTPS
- Workers logs show no errors
- After a few days without problems, revoke all old keys
- Once things are stable, delete the services on Zeabur and request your refund
Rollback plan
Until your Zeabur services expire, you roll back by changing DNS back to the records you wrote down before the cutover and restarting the suspended services with zeabur service restart. Data written to the new environment after the cutover does not flow back to Zeabur on its own, so keep the rollback window as short as possible. From Dec 1 the shared clusters are suspended and dedicated servers shut down when they expire, so the time you can roll back is limited. That is another reason to finish moving by the end of November.
What it costs (checked Nov 3, 2026)
Only numbers listed on the official pricing pages, in US dollars:
| Service | Free plan | Paid plan |
|---|---|---|
| Supabase | $0; 500 MB database, 1 GB Storage, 5 GB egress, up to 2 projects; paused after a week of inactivity; no backups | Pro from $25/month; 8 GB database disk, 100 GB Storage, 250 GB egress, daily backups kept 7 days, $10 compute credit included |
| Cloudflare Workers | 100,000 requests per day, 10 ms CPU per request | $5/month minimum; includes 10 million requests and 30 million CPU milliseconds |
| Hyperdrive | 100,000 queries per day | Unlimited queries on Paid |
| R2 | 10 GB storage, 1 million Class A operations, 10 million Class B operations per month | $0.015 per GB-month; free egress |
| Queues | 10,000 operations per day | 1 million operations per month included, then $0.40 per million |
| Containers | None | Requires Workers Paid; includes 25 GiB-hours of memory, 375 vCPU-minutes and 200 GB-hours of disk per month; egress in the Taiwan region $0.05 per GB (500 GB included) |
For a typical small website plus API, Supabase Pro plus Workers Paid comes to about $30 a month. Personal projects can run on both free plans, as long as you accept that Supabase pauses after a week of inactivity and has no automatic backups.
Estimated cost of running Containers 24 hours a day without sleeping (my own calculation from the unit prices above, memory and disk only; CPU is billed separately by actual use): basic (1/4 vCPU, 1 GiB memory, 4 GB disk) is about $7 a month, and standard-1 (1/2 vCPU, 4 GiB, 8 GB) about $27, each plus the $5 Workers Paid fee. For services that must be online all day, compare these numbers with a VPS's monthly price before deciding.
FAQ
Can I get my data back after the Zeabur shutdown?
According to the official announcements, all services stop and their data is deleted on Dec 31; dedicated servers delete their data on their expiry date. The shared clusters are suspended from Dec 1, but you can still resume services by hand to back them up. Export now, and keep the backups somewhere other than your new host, with restricted access.
What are the alternatives to Zeabur?
The options Zeabur's official guide asks the agent to compare are Railway, Coolify, k3s and Docker Compose. The Supabase + Cloudflare setup in this article suits people who want managed services to run both the database and the servers. For projects with a lot of long-running processes, Docker Compose on a VPS may be more direct.
I only have a frontend. What's the fastest way to migrate from Zeabur?
Build the frontend as static files, write a wrangler.jsonc with only assets, run npx wrangler deploy, and add your domain as a Custom Domain. If the frontend code calls an API on *.zeabur.app, change that too.
My service uses Zeabur's PostgreSQL. Can I just restore Zeabur's daily backup?
You can, but I don't recommend it. That data.sql is produced by pg_dumpall and restores the roles along with their old passwords. Export each database with pg_dump instead. If data.sql is all you have, delete the CREATE ROLE / ALTER ROLE lines first.
Do I have to switch my login system to Supabase Auth?
No. Move your existing users table along with everything else and keep using your current login logic. Do the migration and the auth switch as two separate changes; problems are easier to track down that way.
What are the deadlines for refunds and security claims after the Zeabur shutdown?
Both are Nov 30, 2026, filed as separate Zeabur support tickets. Refunds are capped at the balance you actually paid in cash; promotional credit is not refunded. The refund is settled only after all billable items have stopped, so the sooner you shut your services down, the sooner you get it.
Sources
- Zeabur: "Zeabur is shutting down" (in Chinese) https://zeabur.com/zh-TW/blogs/zeabur-is-shutting-down
- Zeabur: "Migrating off Zeabur: hand this guide to your coding agent" https://zeabur.com/blogs/migrate-from-zeabur (agent version: https://zeabur.com/blogs/migrate-from-zeabur.md )
- Zeabur: "Service migration, refunds and security incident claims" (in Chinese) https://zeabur.com/zh-TW/blogs/migration-refunds-and-security-claims
- Zeabur: "October 2026 security incident (preliminary report)" https://zeabur.com/blogs/october-2026-security-incident
- Zeabur: "Leaving Zeabur checklist" https://zeabur.com/blogs/leaving-zeabur-checklist
- Zeabur docs: PostgreSQL https://zeabur.com/docs/en-US/marketplace/postgresql
- Zeabur CLI source code https://github.com/zeabur/cli
- Zeabur Open Source https://oss.zeabur.com/ , https://github.com/zeabur-oss/zeabur
- Velox Data: Zeabur keys and backup restores https://velox-data.com/blog/zeabur-security-keys/
- Supabase: Migrate from Postgres https://supabase.com/docs/guides/platform/migrating-to-supabase/postgres
- Supabase: Migrate from MySQL https://supabase.com/docs/guides/platform/migrating-to-supabase/mysql
- Supabase: Connecting to Postgres https://supabase.com/docs/guides/database/connecting-to-postgres
- Supabase: Row Level Security https://supabase.com/docs/guides/database/postgres/row-level-security
- Supabase: Storage S3 authentication https://supabase.com/docs/guides/storage/s3/authentication
- Supabase: Pricing https://supabase.com/pricing
- Cloudflare: Migrate from Pages to Workers https://developers.cloudflare.com/workers/static-assets/migration-guides/migrate-from-pages/
- Cloudflare: Single Page Application https://developers.cloudflare.com/workers/static-assets/routing/single-page-application/
- Cloudflare: Hyperdrive get started https://developers.cloudflare.com/hyperdrive/get-started/
- Cloudflare: Hyperdrive + Supabase https://developers.cloudflare.com/hyperdrive/examples/connect-to-postgres/postgres-database-providers/supabase/
- Cloudflare: Workers limits https://developers.cloudflare.com/workers/platform/limits/
- Cloudflare: Workers pricing https://developers.cloudflare.com/workers/platform/pricing/
- Cloudflare: Secrets https://developers.cloudflare.com/workers/configuration/secrets/
- Cloudflare: Cron Triggers https://developers.cloudflare.com/workers/configuration/cron-triggers/
- Cloudflare: Queues get started https://developers.cloudflare.com/queues/get-started/
- Cloudflare: Service bindings https://developers.cloudflare.com/workers/runtime-apis/bindings/service-bindings/
- Cloudflare: Custom Domains https://developers.cloudflare.com/workers/configuration/routing/custom-domains/
- Cloudflare: Durable Objects WebSockets https://developers.cloudflare.com/durable-objects/best-practices/websockets/
- Cloudflare: How Workers KV works https://developers.cloudflare.com/kv/concepts/how-kv-works/
- Cloudflare: R2 pricing https://developers.cloudflare.com/r2/pricing/
- Cloudflare: R2 + rclone https://developers.cloudflare.com/r2/examples/rclone/
- Cloudflare: Containers get started https://developers.cloudflare.com/containers/get-started/
- Cloudflare: Containers lifecycle https://developers.cloudflare.com/containers/platform-details/architecture/
- Cloudflare: Containers limits https://developers.cloudflare.com/containers/platform/limits/
- Cloudflare: Containers pricing https://developers.cloudflare.com/containers/pricing/
- Cloudflare: Containers changelog https://developers.cloudflare.com/changelog/product/containers/
- Cloudflare: Next.js on Workers https://developers.cloudflare.com/workers/framework-guides/web-apps/nextjs/
- Cloudflare: Python Workers https://developers.cloudflare.com/workers/languages/python/packages/
- Astro: Cloudflare adapter https://docs.astro.build/en/guides/integrations-guide/cloudflare/
- Hono: Cloudflare Workers https://hono.dev/docs/getting-started/cloudflare-workers
- Upstash: Cloudflare Workers https://upstash.com/docs/redis/quickstarts/cloudflareworkers
- pgloader: MySQL https://pgloader.readthedocs.io/en/latest/ref/mysql.html
- n8n: Database settings https://docs.n8n.io/hosting/configuration/supported-databases-settings/
- n8n: Encryption key https://docs.n8n.io/hosting/configuration/configuration-examples/encryption-key/
- n8n: CLI commands https://docs.n8n.io/hosting/cli-commands/