Supabase is really two things bolted together: a PostgreSQL database with an auto-generated REST API, and a suite of hosted services around it, Auth, Storage, Realtime and Edge Functions. WattleDB is the first half, run entirely on Australian-owned infrastructure. So a migration splits cleanly into "the database, which just moves" and "the services, which you handle one at a time". Let's be upfront about the shape before touching a command.
What maps, and what doesn't
- Database, RLS, functions, extensions → move verbatim in a
pg_dump. Clean. - PostgREST REST API → WattleDB has the same thing; re-point the URL and token. Clean.
- Storage → WattleDB gives an S3-compatible bucket. Files copy over; the client API changes.
- Auth (GoTrue) → no hosted equivalent. You keep the users, bring your own auth, and mint JWTs WattleDB verifies.
- Realtime → on the WattleDB roadmap, not live today. Plan around it.
- Edge Functions → no equivalent. Host that code elsewhere.
If your project is mostly database + REST + RLS, this is a short migration. If it leans hard on Auth, Realtime or Edge Functions, read those sections carefully before you commit, because that is where the real work is.
1. Inventory what you actually use
Open your Supabase project and note which of the six pieces you depend on. Most apps use the database and RLS heavily, Auth moderately, and only some use Storage, Realtime or Edge Functions. This list is your migration scope. In the SQL editor, capture the essentials:
-- Postgres version and installed extensions
select version();
select extname from pg_extension order by 1;
-- Your tables and their RLS status
select relname, relrowsecurity
from pg_class c join pg_namespace n on n.oid = c.relnamespace
where n.nspname = 'public' and c.relkind = 'r' order by 1;
-- How many users are in Auth
select count(*) from auth.users;
2. Provision WattleDB and get the direct URL
Create a database in the WattleDB console. From Database → Connect, copy the DIRECT_URL (session mode) for the migration, and the CA certificate if you want verify-full TLS. Use the direct URL, not the pooled one, for the restore: pg_restore holds long transactions and sets session state that a transaction pooler will break.
export DIRECT_URL="postgresql://user:pass@<db-id>.db.wattledb.com.au:<port>/db?sslmode=require"
3. Dump the Supabase database
Get your Supabase connection string from Project Settings → Database (use the direct connection, or the session pooler on port 5432). Dump the schema you care about, normally public. The custom compressed format lets pg_restore load in parallel and skip pieces if needed.
export SUPABASE_URL="postgresql://postgres:[password]@db.[ref].supabase.co:5432/postgres"
# Your app schema: tables, data, functions, RLS policies
pg_dump "$SUPABASE_URL" -Fc --no-owner --no-privileges \
--schema=public -f supabase.dump
On the auth and storage schemas: these belong to Supabase's own services. You do not need Supabase's copies running on WattleDB, but you may want the data, your users, out of auth.users. Dump those rows separately so you can load them into a users table you control:
# Just your users' rows, to import into your own auth later
pg_dump "$SUPABASE_URL" --data-only --no-owner \
--table=auth.users -f supabase_users.dump
4. Restore into WattleDB
Recreate any extensions first (the dump references them but a managed target may not pre-install every one), then restore. --no-owner --no-privileges drops Supabase's role grants, which won't exist on WattleDB, and lets your database's own roles apply.
# Extensions your source used (from step 1)
psql "$DIRECT_URL" -c 'create extension if not exists "uuid-ossp";'
psql "$DIRECT_URL" -c 'create extension if not exists pgcrypto;'
# Restore schema + data + RLS, in parallel
pg_restore --no-owner --no-privileges --clean --if-exists \
-j 4 -d "$DIRECT_URL" supabase.dump
5. Confirm your RLS came across
Row-Level Security policies are part of the schema, so they restore with everything else, this is the part people expect to be hard and it simply isn't. Verify it landed:
psql "$DIRECT_URL" -c "select tablename, policyname, cmd
from pg_policies where schemaname='public' order by 1,2;"
Policies that call auth.uid() or auth.jwt() depend on the token your app sends, not on Supabase. They keep working as long as the JWT WattleDB receives carries the same sub (user id) and role claims. That's the bridge the next section builds.
6. Re-point the REST API
WattleDB generates the same PostgREST API Supabase does. In the console, open Database → REST API, enable it, and generate an authenticated bearer token. Every table becomes an endpoint, exactly as on Supabase.
In code, the query builder inside supabase-js is postgrest-js, and it works against WattleDB directly:
import { PostgrestClient } from '@supabase/postgrest-js'
const db = new PostgrestClient('https://<db-id>.api.wattledb.com.au', {
headers: { Authorization: `Bearer ${process.env.WATTLEDB_TOKEN}` },
})
// identical query surface to supabase.from(...)
const { data } = await db.from('projects').select('*').eq('owner', userId)
If you use the full supabase-js client, its .from() calls move over cleanly; its .auth, .storage and .realtime calls are the ones that change, covered below.
7. Auth: keep your users, bring your own issuer
This is the honest gap. Supabase Auth is GoTrue, a hosted service that signs up users, handles passwords and OAuth, and issues JWTs. WattleDB does not run GoTrue. What WattleDB does is verify JWTs and enforce RLS, which is the half your policies actually depend on. So the plan is:
- Keep the users. Load the rows you dumped in step 3 into a users table you own.
- Pick an issuer. Self-host GoTrue, use an auth provider, or run your own sign-in. Whatever you choose issues the JWT.
- Sign with WattleDB's secret. Set your issuer's signing secret to the database's JWT secret (console → REST API), so PostgREST trusts the token. Put the user id in the
subclaim and setroletoauthenticated.
// your issuer mints a token WattleDB's PostgREST will accept
import jwt from 'jsonwebtoken'
const token = jwt.sign(
{ sub: user.id, role: 'authenticated', email: user.email },
process.env.WATTLEDB_JWT_SECRET, { expiresIn: '1h' })
Your existing auth.uid() policies keep working unchanged, because auth.uid() reads the token's sub. You're replacing the issuer, not the authorization model.
8. Storage: copy the files, change the client
Supabase Storage and WattleDB Storage are both S3-compatible underneath, but the client APIs differ, Supabase's storage-js versus a standard S3 SDK. Two steps: move the bytes, then change the call sites. WattleDB gives you the endpoint, bucket and keys under Database → Storage.
# copy files across with rclone (configure both as S3 remotes)
rclone sync supabase:your-bucket wattledb:your-bucket --progress
// upload, WattleDB side, with the AWS S3 SDK
import { S3Client, PutObjectCommand } from '@aws-sdk/client-s3'
const s3 = new S3Client({
endpoint: process.env.WATTLEDB_S3_ENDPOINT, region: 'ap-southeast-2',
forcePathStyle: true,
credentials: { accessKeyId: process.env.WATTLEDB_S3_KEY,
secretAccessKey: process.env.WATTLEDB_S3_SECRET },
})
await s3.send(new PutObjectCommand({ Bucket: 'your-bucket', Key: key, Body: body }))
9. Realtime and Edge Functions: plan around them
No dressing this up. Realtime (Postgres change subscriptions over websockets) is on the WattleDB roadmap, not live today, if your app depends on it, keep it running on your current stack during the transition, or fall back to short polling against the REST API. Edge Functions have no WattleDB equivalent; that code is just server code, so host it wherever you like. To keep the sovereignty you're migrating for, put it on Australian infrastructure rather than a US edge platform.
10. Verify, then cut over
Before you trust the copy: check row counts and sequences match, run a real authenticated query through the REST API with a minted token, and confirm an RLS policy actually blocks a row it should.
# counts match between source and target for each table
psql "$SUPABASE_URL" -c "select count(*) from projects;"
psql "$DIRECT_URL" -c "select count(*) from projects;"
# sequences are ahead of max(id), so new inserts don't collide
psql "$DIRECT_URL" -c "select last_value from projects_id_seq;"
For a small database, take a short maintenance window: pause writes, run a final dump/restore of anything that changed, flip your environment variables, done, minutes of downtime. For a large one, use PostgreSQL logical replication to stream changes until the two are in sync, then switch. Either way the cut-over is just changing DATABASE_URL, the REST URL and token, the storage credentials, and your auth issuer.
Migrating to WattleDB specifically
The reason to do this at all is jurisdiction. Supabase's region setting controls geography, not law: the company is American and runs on US cloud, so the data sits under US legal reach regardless of which region you picked. WattleDB is Australian owned, operated and hosted, Sydney primary, cross-state backups in Melbourne, with no foreign entity in the chain. That doesn't make Australian data untouchable, the Australia-US CLOUD Act agreement, in force since 31 January 2024, still gives US authorities a treaty path for serious crime, but it removes the direct exposure of your data living inside a US company's account. For anything governed by the Privacy Act or an Australian data-residency requirement, that is the whole point.
Migrating from Supabase
The database itself is easy: Supabase is standard PostgreSQL, so pg_dump from Supabase and pg_restore into WattleDB carries your tables, data, functions and RLS policies unchanged. The work is the parts that are not the database, Auth, Storage, Realtime and Edge Functions, and this guide covers each honestly.
Yes. RLS lives in the database and is included in the dump, so it restores verbatim. Just check any policy that calls auth.uid() or auth.jwt(): those depend on the JWT your app sends, and keep working as long as the token carries the same sub and role claims.
Supabase Auth is GoTrue, a hosted service WattleDB does not run. You keep your users (dump auth.users), then run your own auth, self-host GoTrue, or use a provider, and mint JWTs signed with your database's JWT secret so PostgREST and your RLS verify them. auth.uid() policies keep working as long as the token's sub is the user id.
The database/REST part does. WattleDB exposes the same PostgREST API, so postgrest-js (the query builder inside supabase-js) works pointed at your WattleDB REST URL with a bearer token. The auth, storage and realtime modules talk to Supabase-specific services and won't point at WattleDB, so those call sites change.
Sovereignty. Supabase runs on US cloud under a US parent, so your data sits under US jurisdiction wherever the region is. WattleDB is Australian owned, operated and hosted with no foreign entity in the chain. That narrows US legal reach rather than removing it, the Australia-US CLOUD Act agreement (in force since 31 January 2024) still gives a treaty path for serious crime, but it removes the direct US-company exposure. If the Privacy Act and Australian residency matter, that is the difference.