Quanto tempo leva?
Depende do tamanho do projeto e de quanto o app usa de login e sessão. O banco é a parte previsível; a camada de login do app e os usuários são as que pedem trabalho.
| Etapa | Tempo típico | Depende de |
|---|---|---|
| Troca do SDK nas consultas de dados | Rápida | Quanto o app usa de auth.admin e de troca de e-mail, que o SDK ainda não tem |
| Login e sessão do app | Varia | Com um dos templates oficiais a sessão já vem pronta; com @supabase/ssr, é trocar essa camada |
| Schema e dados (dump + restauração pela equipe) | Combinada no chamado | Tamanho do dump, extensões e referências a auth.users |
| Usuários | Rápida | Volume: você importa no painel, até 500 usuários por arquivo |
| Storage (cópia de arquivos) | Varia | Volume total em GB e largura de banda |
Dica: rode os 2 sistemas em paralelo durante a migração: aponte o staging pro SuperDB, deixe o prod no Supabase, valide tudo, e só então vire as env vars de produção.
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/client segue os nomes do supabase-js v2. As consultas de dados continuam iguais: select, insert, upsert, update, delete, os filtros (eq, in, or, not, contains, textSearch e os outros), count, order, range, single e rpc. No login, onAuthStateChange, updateUser e resetPasswordForEmail também existem, e a sessão se renova sozinha. As poucas diferenças estão logo abaixo.
npm uninstall @supabase/supabase-js
npm install @superdb/client
Depois mude o import e acrescente o project:
- import { createClient } from '@supabase/supabase-js'
+ import { createClient } from '@superdb/client'
export const db = createClient(
- process.env.NEXT_PUBLIC_SUPABASE_URL!,
- process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!
+ process.env.NEXT_PUBLIC_SUPERDB_URL!, // aponta pra https://auth.superdb.com.br
+ process.env.NEXT_PUBLIC_SUPERDB_ANON_KEY!,
+ { project: 'meu_app' } // slug do projeto → vira o Accept-Profile
)
O que pede ajuste no código: a API auth.admin não existe no SDK. updateUser({ password }) de uma conta que já tem senha pede também current_password, e updateUser({ email }) ainda não existe. O link de "esqueci a senha" volta com #token=..., e a sua tela conclui com db.auth.resetPassword({ token, password }). O @supabase/ssr não funciona como está, porque a sessão é nossa: o login devolve dois tokens, e os templates oficiais já trazem essa camada pronta para Next.js. O realtime tem o próprio passo (Passo 5).
Se o seu backend chama a API por HTTP direto, em vez do SDK, leia isto antes: no SuperDB o login devolve dois tokens, e o que vale para dados não é o access_token. O SDK resolve isso sozinho; quem monta os headers na mão precisa mandar o data_plane_token. Está detalhado em Autenticação da API REST.
Nota: se você usa supabase-js em projetos serverless (Edge Functions, Vercel Functions, Cloudflare Workers), o @superdb/client também roda nesses runtimes. Mesmo bundle, mesma dependência de fetch nativo.
Hospeda na Vercel? Em Configurações, no painel do seu projeto, conecte a Vercel: a URL, o identificador do projeto e as chaves entram sozinhas nas variáveis de ambiente de lá, e a URL de retorno do seu app fica cadastrada aqui, inclusive o endereço das publicações de teste. Aí o Passo 1 vira só a troca do import.
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 mande o dump pra gente por um chamado no painel. O banco do SuperDB não fica aberto na internet (não existe conexão psql de fora), então a restauração é feita pela equipe, junto com você: o schema public vira o schema do seu projeto (proj_<slug>), e o que aponta para auth.users é adaptado (veja o Passo 3).
Projeto pequeno também dá pra levar sozinho: o SQL roda no editor do painel ou pela API com a management key (POST https://auth.superdb.com.br/platform/v1/projects/<id>/db/exec). Gere o dump com --inserts para ele sair como SQL comum, sem COPY.
Cuidado com extensions e schemas custom: se o seu projeto usa pg_cron, pgsodium ou similares, diga no chamado antes de mandar o dump: pgvector e PostGIS vêm instalados, o pg_cron não está disponível nos projetos, nem o pg_net, e a gente confere o resto. Schemas custom (fora de public) também entram nessa conversa.
Database Webhooks não migram. No Supabase, eles são um gatilho que chama net.http_post, do pg_net, e essa chamada não sai do banco de um projeto aqui: o gatilho do webhook não é recriado, e função sua que chame net.http_* não funciona. Liste os seus webhooks no chamado e mova cada um para fora do banco: a sua aplicação chama a URL logo depois de gravar, ou um gatilho grava o evento numa tabela sua e um worker lê essa tabela e chama a URL.
Passo 3: migre os usuários
No SuperDB não existe a tabela auth.users: os usuários de cada projeto ficam no schema dele, na tabela proj_<slug>.auth_users. É para ela que apontam as chaves estrangeiras que no Supabase apontavam para auth.users(id) — o papel do seu projeto tem permissão de referenciá-la. O Supabase guarda as senhas em bcrypt e o SuperDB em Argon2id, mas o login do SuperDB confere o bcrypt de uma conta importada e, no primeiro acesso, troca a impressão da senha pela de Argon2id. Para quem usa o app, nada muda: entra com a mesma senha de sempre.
Opção A — importar no painel (mantém o UUID e a senha)
Exporte os usuários e importe você mesmo, em Autenticação → Users no painel do seu projeto. Cada um entra com o mesmo UUID, e-mail, confirmação, metadados e a mesma senha, então as chaves estrangeiras do seu schema continuam valendo e ninguém precisa redefinir nada. Quem entrava só por link mágico ou login social vem sem senha e continua entrando do mesmo jeito. Repetir o mesmo arquivo não duplica ninguém: quem já está lá fica como está.
psql "postgresql://postgres.[REF]:[PASS]@aws-0-[REGION].pooler.supabase.com:5432/postgres" -tA -c \
"select json_agg(json_build_object('id', id, 'email', email, 'encrypted_password', encrypted_password,
'email_confirmed_at', email_confirmed_at, 'created_at', created_at,
'raw_user_meta_data', raw_user_meta_data)) from auth.users" > usuarios.json
O arquivo tem o hash das senhas. Ele vai do seu computador direto para o seu projeto: você não precisa mandá-lo para ninguém. Apague a sua cópia depois da importação.
Opção B — migração no primeiro login, sem mandar arquivo
Se você prefere não mandar o arquivo com os hashes: enquanto o Supabase ainda está no ar, a tela de login tenta o SuperDB; se não der, o seu servidor confere a senha no Supabase (signInWithPassword) e, se ela conferir, cria a conta no SuperDB com a mesma senha. A conta nasce com UUID novo, então os dados daquele usuário precisam passar para o id novo. O passo a passo, com o código do servidor, está no guia do Auth0: é o mesmo desenho, trocando a conferência no Auth0 pela do Supabase.
MFA e metadados: os fatores TOTP não migram em nenhum dos dois caminhos: quem usava MFA cadastra o autenticador de novo. E o token do SuperDB não carrega app_metadata nem user_metadata: se as suas políticas leem papel ou permissão do JWT, elas passam a ler de uma tabela de papéis no seu schema.
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/client'
const supa = createSupa(
process.env.SUPABASE_URL!,
process.env.SUPABASE_SERVICE_ROLE!
)
const sup = createSuper(
process.env.SUPERDB_URL!, // https://auth.superdb.com.br
process.env.SUPERDB_SERVICE_ROLE!,
{ project: 'meu_app' }
)
const BUCKET_SUPABASE = 'avatars' // o nome no Supabase
const BUCKET = 'proj_meu_app_avatars' // no SuperDB: proj_<slug>_<nome>, criado antes no painel
async function migrate() {
const { data: files, error } = await supa.storage
.from(BUCKET_SUPABASE)
.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_SUPABASE).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
Essa é a única parte que muda de código. No SuperDB o método db.channel() ainda é stub — o realtime funciona mintando um token dedicado em /rt/v1/token e conectando o @supabase/realtime-js direto. Como no Supabase, o realtime aplica o RLS com o usuário do token: peça o token com a sessão (db.auth.getDataPlaneToken()), e cada inscrito recebe só as linhas que ele leria pela API.
A publicação do dump não vale aqui: ligue cada tabela na tela Realtime do painel. Ela precisa estar com RLS ligada, e regra que consulta outra tabela precisa usar uma função security definer para valer no realtime (o painel avisa ao ativar).
import { RealtimeClient } from '@supabase/realtime-js'
// 1. minta o token de realtime com a sessão do usuário (sem login: a anon key)
const credencial = (await db.auth.getDataPlaneToken()) ?? SUPERDB_ANON_KEY
const res = await fetch('https://auth.superdb.com.br/rt/v1/token', {
method: 'POST',
headers: { Authorization: `Bearer ${credencial}` },
})
const { token, url } = await res.json()
// 2. conecta no realtime-js usando o token mintado como apikey
const client = new RealtimeClient(url, {
params: { apikey: token },
})
const channel = client
.channel('proj_seu_projeto:room-1', { config: { private: true } })
.on('postgres_changes',
// o schema é proj_ + o slug do projeto, não public
{ event: 'INSERT', schema: 'proj_seu_projeto', table: 'messages' },
(payload) => setMessages((m) => [...m, payload.new])
)
.subscribe()
Mesmo protocolo: o realtime do SuperDB é o Supabase Realtime (WebSocket + Phoenix Channels), então .on('postgres_changes'), .on('presence') e .on('broadcast') funcionam igual. As tabelas ligadas aparecem na tela Realtime do painel.
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 e voltam na restauração, mas confira depois, no painel ou numa consulta a pg_policies, que cada tabela ficou com as dela. 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://auth.superdb.com.br
+ NEXT_PUBLIC_SUPERDB_ANON_KEY=eyJhbGciOi...
+ SUPERDB_SERVICE_ROLE_KEY=eyJhbGciOi...
O @superdb/client não lê as variáveis antigas do Supabase: o nome que vale é o que você passar no createClient.
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á saindo do cache da sua hospedagem ou da sua CDN, com 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/clientinstalado e import atualizado) - ☐ Schema importado e validado (todas as tabelas, views, functions, triggers)
- ☐ Database Webhooks e chamadas a
net.http_*passados para a sua aplicação ou para um worker - ☐ Usuários migrados (login com a senha antiga testado numa conta)
- ☐ 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
- ☐ Quem usava MFA foi avisado para cadastrar o autenticador de novo
- ☐ 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.