Migrate from MySQL
Move a MySQL or MariaDB database to Postgres on SupaHost with pgloader: type differences to expect, loading over the SSH tunnel or via a local Postgres, and checking the result.
The usual tool for MySQL to Postgres is pgloader: it reads the MySQL schema, converts types, creates tables and copies the data in one run. pgloader needs a direct Postgres connection as its target. On Business and Enterprise that's your project through the SSH tunnel. On other plans you run pgloader into a local Postgres 17 first, then move the result to SupaHost.
Plan
- Size. The converted database must fit your plan's shared quota (8 GB Builder, 25 GB Pro, 50 GB Business).
- Read the type differences below and grep your app for MySQL-only SQL (backticks,
LIMIT x, y,ON DUPLICATE KEY UPDATE,IFNULL,GROUP_CONCAT). - Downtime. Stop writes to MySQL before the final run.
Type and behaviour differences
| MySQL | Postgres on SupaHost |
|---|---|
tinyint(1) |
boolean (pgloader converts by default) |
int unsigned |
bigint (Postgres has no unsigned types) |
AUTO_INCREMENT |
generated ... as identity or serial |
datetime |
timestamp; consider timestamptz |
0000-00-00 dates |
not valid; pgloader turns them into null |
enum('a','b') |
a Postgres enum type or text with a check constraint |
case-insensitive = on strings |
case-sensitive; use citext or lower() indexes |
ON DUPLICATE KEY UPDATE |
insert ... on conflict (...) do update |
Export and convert
Write a pgloader command file, bookings.load:
LOAD DATABASE
FROM mysql://app:[email protected]:3306/bookings
INTO postgresql://postgres:<password>@127.0.0.1:6543/p_app_one
WITH include drop, create tables, create indexes, reset sequences, foreign keys
ALTER SCHEMA 'bookings' RENAME TO 'public';
The ALTER SCHEMA line matters: pgloader otherwise creates a schema named after the MySQL database, and the Data API only exposes public, storage, graphql_public and pgmq_public.
Load
Business and Enterprise. Open the tunnel, then run pgloader against it:
ssh -N -L 6543:127.0.0.1:5432 [email protected] &
pgloader bookings.load
Get the password from Dashboard → your project → Connect → Database → Show connection string.
Builder and Pro. Point INTO at a local Postgres 17 (for example docker run -p 6543:5432 -e POSTGRES_PASSWORD=dev postgres:17), run pgloader, check the result, then:
- dump it (
pg_dump -Fc --no-owner --no-privileges -n public), restore it into a temporary Supabase Cloud project and use the one-click import, or - for small databases, run the schema from
pg_dump --schema-onlyin Studio's SQL editor and import each table as CSV in the table editor.
After loading
-- make the new tables safe to expose, then refresh planner stats
alter table public.reservations enable row level security;
alter table public.rooms enable row level security;
analyze;
Add policies for each table. If you didn't use reset sequences, see duplicate key after importing data.
Switch the app
Replace the MySQL driver with supabase-js:
const { data, error } = await supabase
.from('reservations')
.upsert({ room_id: 12, night: '2026-12-24', guest_id: userId }, { onConflict: 'room_id,night' })
.select()
Logic that used stored procedures becomes Postgres functions in PL/pgSQL, called with supabase.rpc().
Verify
Compare SELECT COUNT(*) per table on both sides, compare a few sums or max IDs, and check that text with accents and emoji came through intact (Postgres here is UTF-8; MySQL tables in latin1 sometimes hide double-encoded text).