Quanto tempo leva?
Depende do tamanho do projeto, mas dá pra estimar bem. A maioria das migrações pequenas (até ~10k usuários, < 5GB) cabe num turno de trabalho. Schema e SDK são as partes rápidas; usuários e storage variam com o volume.
| Etapa | Tempo típico | Depende de |
|---|---|---|
| Troca de SDK (1 linha de import) | ~5 min | Tamanho do codebase pra rebuild + smoke test |
| Schema (export + import) | ~30 min | Quantidade de tabelas, extensions e funções |
| Migração de usuários | ~1h pra < 10k | Volume; senhas em hash continuam funcionando |
| Storage (cópia de arquivos) | Varia | Volume total em GB e largura de banda |
| Total típico (projeto pequeno) | 2-4h | Inclui smoke test e ajustes finos |
Dica: rode os 2 sistemas em paralelo durante a migração. Como o SDK é compatível, dá pra apontar o staging pro SuperDB e o prod pro Supabase, validar tudo, e só então virar o DNS / env vars.
Pré-requisitos
- Node 18 ou superior
- Acesso ao projeto Supabase com permissão pra export (owner ou admin)
psqlinstalado localmente (vem com o Postgres client)- Conta SuperDB criada em app.superdb.com.br
- Projeto SuperDB provisionado (anote URL, anon key e service-role key)
Passo 1: troque o SDK
O @superdb/supabase-compat implementa a mesma API pública do supabase-js v2. Não muda nenhuma chamada no seu código — só o pacote de origem.
npm uninstall @supabase/supabase-js
npm install @superdb/supabase-compat
Depois mude o import. Esse é o diff que normalmente é o único do projeto:
- import { createClient } from '@supabase/supabase-js'
+ import { createClient } from '@superdb/supabase-compat'
export const db = createClient(
- process.env.NEXT_PUBLIC_SUPABASE_URL!,
- process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!
+ process.env.NEXT_PUBLIC_SUPERDB_URL!,
+ process.env.NEXT_PUBLIC_SUPERDB_ANON_KEY!
)
Pronto. Todos os métodos (.from().select(), .auth.signUp(), .storage.from(), .channel()) continuam funcionando exatamente igual.
Nota: se você usa supabase-js em projetos serverless (Edge Functions, Vercel Functions, Cloudflare Workers), o @superdb/supabase-compat também roda nesses runtimes. Mesmo bundle, mesma dependência de fetch nativo.
Passo 2: exporte o schema
O Supabase é Postgres, então um pg_dump resolve. Você consegue a connection string em Project Settings → Database → Connection string (use a do tipo "Session pooler" pra evitar limite de conexões).
# 1. Dump completo (schema + dados) excluindo schemas internos do Supabase
pg_dump \
"postgresql://postgres.[REF]:[PASS]@aws-0-[REGION].pooler.supabase.com:5432/postgres" \
--no-owner \
--no-privileges \
--schema=public \
--schema=storage \
--exclude-table=storage.migrations \
--exclude-table=storage.objects \
-f supabase-dump.sql
# 2. Só o schema (sem dados) — útil pra inspecionar antes de importar
pg_dump \
"postgresql://postgres.[REF]:[PASS]@aws-0-[REGION].pooler.supabase.com:5432/postgres" \
--schema-only \
--schema=public \
--no-owner \
-f schema.sql
Agora importe no SuperDB. A connection string do seu projeto está em Dashboard → Conexão direta.
psql "postgresql://postgres.[PROJ_ID]:[PASS]@db.superdb.com.br:5432/postgres" \
-f supabase-dump.sql
Ou, se preferir interface visual, abra o Studio do SuperDB e cole o SQL no editor.
Cuidado com extensions e schemas custom: se seu projeto usa pgvector, pg_cron, pgsodium ou similares, confirme que estão habilitadas no SuperDB antes de rodar o import (Dashboard → Banco → Extensions). Schemas custom (fora de public) também precisam ser criados manualmente antes do psql -f.
Passo 3: migre os usuários
Os 2 sistemas guardam usuários numa tabela auth.users com schema muito parecido. A diferença prática: no SuperDB, cada projeto tem seu próprio schema proj_X com a tabela users. As senhas em hash (bcrypt ou Argon2id) continuam funcionando sem reset.
Você tem 2 opções: (a) via SQL direto (CSV intermediário) ou (b) via Foreign Data Wrapper se ainda tem acesso ativo ao Supabase.
Opção A — export pra CSV e import
psql "postgresql://postgres.[REF]:[PASS]@aws-0-[REGION].pooler.supabase.com:5432/postgres" \
-c "\COPY (SELECT id, email, encrypted_password, email_confirmed_at, created_at, raw_user_meta_data FROM auth.users) TO 'users.csv' WITH CSV HEADER"
-- substitua proj_X pelo schema do seu projeto SuperDB
CREATE TEMP TABLE _import_users (
id uuid,
email text,
encrypted_password text,
email_confirmed_at timestamptz,
created_at timestamptz,
raw_user_meta_data jsonb
);
\COPY _import_users FROM 'users.csv' WITH CSV HEADER;
INSERT INTO proj_X.users (id, email, encrypted_password, email_confirmed_at, created_at, raw_user_meta_data)
SELECT id, email, encrypted_password, email_confirmed_at, created_at, raw_user_meta_data
FROM _import_users
ON CONFLICT (email) DO NOTHING;
Senhas: o Supabase usa bcrypt; o SuperDB aceita bcrypt e re-hash lazy pra Argon2id no próximo login bem-sucedido. Ou seja: nenhum usuário precisa resetar senha. O hash original migra junto.
Passo 4: migre o storage
Storage é cópia de arquivos + metadata. O script abaixo lista todos os objects do bucket Supabase, baixa um por um e re-upload no SuperDB mantendo o path. Roda em background, sem bloquear o resto.
import { createClient as createSupa } from '@supabase/supabase-js'
import { createClient as createSuper } from '@superdb/supabase-compat'
const supa = createSupa(
process.env.SUPABASE_URL!,
process.env.SUPABASE_SERVICE_ROLE!
)
const sup = createSuper(
process.env.SUPERDB_URL!,
process.env.SUPERDB_SERVICE_ROLE!
)
const BUCKET = 'avatars'
async function migrate() {
const { data: files, error } = await supa.storage
.from(BUCKET)
.list('', { limit: 10_000, sortBy: { column: 'name', order: 'asc' } })
if (error) throw error
for (const f of files ?? []) {
const { data: blob } = await supa.storage.from(BUCKET).download(f.name)
if (!blob) continue
const { error: upErr } = await sup.storage
.from(BUCKET)
.upload(f.name, blob, { upsert: true, contentType: f.metadata?.mimetype })
if (upErr) console.error(`[fail] ${f.name}:`, upErr.message)
else console.log(`[ok] ${f.name} (${f.metadata?.size} bytes)`)
}
}
migrate().catch(console.error)
Rode com npx tsx scripts/migrate-storage.ts. Pra buckets grandes, paginate com offset e considere rodar em paralelo (Promise.all com batches de 10).
Passo 5: realtime
Aqui é onde a compatibilidade brilha: nenhuma mudança de código. A API .channel().on('postgres_changes', ...) funciona idêntica. Os triggers e a publicação supabase_realtime vêm no dump SQL e ficam ativos no SuperDB sem ajustes.
const channel = db
.channel('room-1')
.on('postgres_changes',
{ event: 'INSERT', schema: 'public', table: 'messages' },
(payload) => setMessages((m) => [...m, payload.new])
)
.subscribe()
Performance: o realtime do SuperDB usa o mesmo protocolo do Supabase Realtime (WebSocket + Phoenix Channels). Latência típica em São Paulo: ~30ms. Confirme que a tabela está na publicação rodando SELECT * FROM pg_publication_tables WHERE pubname = 'supabase_realtime';
Passo 6: revise RLS
As políticas RLS do Supabase são SQL puro — copiam direto. A sintaxe é a mesma e as funções auth.uid(), auth.jwt(), auth.role() continuam disponíveis com o mesmo comportamento.
create policy "users can read their own posts"
on public.posts
for select
using (auth.uid() = user_id);
create policy "users can insert their own posts"
on public.posts
for insert
with check (auth.uid() = user_id);
Atenção: as políticas vêm no pg_dump, mas sempre rode um \d+ public.tabela no SuperDB depois pra confirmar. Se o seu app usa auth.uid() dentro de funções (não só em políticas), valide que essas funções foram criadas com o search_path correto.
Passo 7: troque as env vars
Mude o .env e os secrets em Vercel / Netlify / Railway. As chaves do SuperDB ficam em Dashboard → API Keys.
- NEXT_PUBLIC_SUPABASE_URL=https://abcdefgh.supabase.co
- NEXT_PUBLIC_SUPABASE_ANON_KEY=eyJhbGciOi...
- SUPABASE_SERVICE_ROLE_KEY=eyJhbGciOi...
+ NEXT_PUBLIC_SUPERDB_URL=https://api.superdb.com.br
+ NEXT_PUBLIC_SUPERDB_ANON_KEY=sdp_anon_...
+ SUPERDB_SERVICE_ROLE_KEY=sdp_service_...
Se você prefere não renomear as variáveis, o @superdb/supabase-compat também lê NEXT_PUBLIC_SUPABASE_URL e NEXT_PUBLIC_SUPABASE_ANON_KEY se eles apontarem pro SuperDB. Mas renomear é mais limpo e evita confusão futura.
Passo 8: deploy
- Trigger um rebuild no Vercel / Netlify (qualquer alteração de env var força rebuild).
- Smoke test num preview deployment: login, signup, leitura, escrita, upload, realtime.
- Confirme que a página de login não está com cache do CDN servindo o bundle antigo.
- Monitore o Dashboard SuperDB nas primeiras horas: queries lentas, taxa de erro, throughput.
- Mantenha o projeto Supabase em standby por ~48h em caso de rollback.
Checklist final
Antes de considerar a migração completa:
- ☐ SDK trocado (
@superdb/supabase-compatinstalado e import atualizado) - ☐ Schema importado e validado (todas as tabelas, views, functions, triggers)
- ☐ Usuários migrados (login com senha antiga funciona)
- ☐ Storage copiado (cheque um arquivo aleatório baixando pela URL)
- ☐ RLS testado (policy bloqueia user A de ler dado do user B)
- ☐ Env vars trocadas em prod, staging e local
- ☐ OAuth providers (Google / GitHub / Apple) reconfigurados com novas callback URLs
- ☐ MFA TOTP preservada (usuários com MFA ativa conseguem entrar)
- ☐ Magic links testados de ponta a ponta
- ☐ Smoke test passou em todos os fluxos críticos
E se eu quiser voltar?
Como nada foi destruído no Supabase original, rollback é só:
- Reverter as env vars pro Supabase (renomeia ou troca os valores).
- Reverter o import do SDK (
npm install @supabase/supabase-js). - Redeploy.
Os 2 sistemas convivem em paralelo durante a migração. Dados criados no SuperDB depois do switchover precisariam ser re-sincronizados de volta — por isso recomendamos manter o paralelismo curto (24-48h) e validar bem antes.
Precisa de ajuda? Migrações de projetos médios e grandes podem ter pontas soltas (custom triggers, RLS complexas, integrações com webhooks). Mande um email pra contato@superdb.com.br — a gente acompanha a migração junto, sem custo.