Por que migrar?
Firebase é excelente pra protótipo e MVP rápido. Em escala, três coisas costumam apertar: custo (Firestore cobra por leitura/escrita individual e fica caro com listagens grandes), consultas (NoSQL não faz joins; queries compostas exigem indexes manuais e às vezes denormalização), e lock-in (Firestore só existe no Google Cloud). SuperDB resolve esses três: Postgres com SQL real, joins nativos, preço previsível em BRL e self-host opcional.
| Aspecto | Firebase | SuperDB |
|---|---|---|
| Modelo de dados | NoSQL (documentos) | Postgres (relacional) |
| Consultas | Limitadas (sem JOIN, sem aggregations complexas) | SQL completo + RLS + views |
| Cobrança | Por reads / writes / GB-mês em USD | Plano fixo em BRL (Pro a R$ 99/mês) |
| NF-e | Não emite | Sim, CNPJ brasileiro |
| Self-host | Impossível | AGPL-3.0 (Sprint 9+) |
| Suporte em PT-BR | Documentação traduzida; suporte em inglês | Equipe brasileira, suporte por email/WhatsApp |
Mapeamento Firebase → SuperDB
A migração mental mais importante: parar de pensar em "documentos aninhados" e voltar pra "tabelas com chaves estrangeiras". Algumas vezes você ganha (queries mais simples); outras vezes custa (mais boilerplate pra modelar relações N:N). O mapeamento típico:
| Firebase | SuperDB | Observação |
|---|---|---|
| Collection | Table (Postgres) | users collection → public.users table |
| Document | Row | Document ID vira id uuid primary key |
| Subcollection | Tabela separada com FK | users/{uid}/orders vira orders.user_id |
| Security Rules | RLS Policies | Sintaxe diferente, mesma intenção |
| Cloud Functions | SQL Triggers ou serviço externo | Edge Functions chegam na Sprint 9+ |
| Firebase Auth | SuperDB Auth | Features equivalentes; senha re-hash lazy |
| Cloud Storage | SuperDB Storage | API S3-compatible + policies |
| Realtime Database / onSnapshot | Realtime postgres_changes | WebSocket; latência ~30ms BR |
| FCM (push) | — | Use OneSignal ou serviço externo |
Não é drop-in: diferente da migração do Supabase (mesma API), aqui você reescreve queries. Firestore .where('user', '==', uid) vira .from('orders').select().eq('user_id', uid). Reserve tempo pra refatorar o data layer.
Tempo estimado
Não tente fazer numa tarde. Refatorar o data layer leva tempo proporcional ao tamanho do app.
| Tamanho do projeto | Tempo típico | O que dominou |
|---|---|---|
| Pequeno (< 5 collections, < 10k users) | 1 dia | Modelar tabelas + reescrever 5-10 queries |
| Médio (10-30 collections, 10k-100k users) | 3-5 dias | Subcollections viram FKs; Security Rules viram RLS |
| Grande (50+ collections, Cloud Functions extensas) | 2-4 semanas | Reescrita parcial de backend; planejar zero-downtime |
Passo 1: exporte o Firestore
Use o gcloud CLI pra exportar pro Cloud Storage, depois baixe localmente.
# 1. Exporte tudo pro GCS (use --collection-ids pra exportar só algumas)
gcloud firestore export gs://[BUCKET]/firestore-backup \
--project=[PROJECT_ID]
# 2. Baixe localmente
gsutil -m cp -r gs://[BUCKET]/firestore-backup ./firestore-dump
# 3. (Alternativa) Export legível com firebase-tools
npx -p firebase-tools firestore-export \
--accountCredentials service-account.json \
--backupFile firestore.json
O export do Firestore vem em formato LevelDB binário. Pra trabalhar com JSON, use ferramentas como firestore-export-import ou um script Node simples:
import admin from 'firebase-admin'
import { writeFileSync } from 'fs'
admin.initializeApp({ credential: admin.credential.cert('./service-account.json') })
const db = admin.firestore()
async function dump(collection: string) {
const snap = await db.collection(collection).get()
const docs = snap.docs.map((d) => ({ id: d.id, ...d.data() }))
writeFileSync(`${collection}.json`, JSON.stringify(docs, null, 2))
console.log(`[ok] ${collection}: ${docs.length} docs`)
}
await Promise.all([dump('users'), dump('orders'), dump('products')])
Passo 2: modele tabelas Postgres
Pegue cada collection e desenhe a tabela equivalente. Exemplo concreto: um app de e-commerce com users/{uid}/orders/{orderId}/items/{itemId}.
create table public.users (
id uuid primary key default gen_random_uuid(),
email text unique not null,
display_name text,
created_at timestamptz default now()
);
create table public.orders (
id uuid primary key default gen_random_uuid(),
user_id uuid not null references public.users(id) on delete cascade,
status text not null default 'pending',
total_cents int not null,
created_at timestamptz default now()
);
create index on public.orders (user_id, created_at desc);
create table public.order_items (
id uuid primary key default gen_random_uuid(),
order_id uuid not null references public.orders(id) on delete cascade,
product_id uuid not null,
qty int not null,
price_cents int not null
);
Use UUIDs nos IDs: os document IDs do Firestore costumam ser strings de 20 chars. Em vez de carregar isso pra sempre, gere UUIDs novos e mantenha o ID antigo num campo firebase_id text unique durante a transição. Depois de validar tudo, dropa a coluna.
Pro import dos dados, gere INSERTs a partir do JSON:
import { createClient } from '@superdb/supabase-compat'
import users from './users.json' assert { type: 'json' }
import orders from './orders.json' assert { type: 'json' }
const db = createClient(
process.env.SUPERDB_URL!,
process.env.SUPERDB_SERVICE_ROLE!
)
const idMap = new Map<string, string>() // firebase id → uuid
for (const batch of chunk(users, 500)) {
const { data, error } = await db.from('users').insert(
batch.map((u) => ({
email: u.email,
display_name: u.displayName ?? null,
firebase_id: u.id,
}))
).select('id, firebase_id')
if (error) throw error
for (const row of data!) idMap.set(row.firebase_id, row.id)
}
for (const batch of chunk(orders, 500)) {
await db.from('orders').insert(
batch.map((o) => ({
user_id: idMap.get(o.userId)!,
status: o.status,
total_cents: Math.round(o.total * 100),
firebase_id: o.id,
}))
)
}
function chunk<T>(arr: T[], n: number): T[][] {
const out: T[][] = []
for (let i = 0; i < arr.length; i += n) out.push(arr.slice(i, i + n))
return out
}
Passo 3: regras → RLS
Security Rules do Firestore viram políticas RLS do Postgres. A intenção é a mesma; a sintaxe muda. Exemplo lado a lado:
rules_version = '2';
service cloud.firestore {
match /databases/{database}/documents {
match /orders/{orderId} {
allow read: if request.auth.uid == resource.data.userId;
allow create: if request.auth.uid == request.resource.data.userId;
allow update, delete: if request.auth.uid == resource.data.userId;
}
}
}
alter table public.orders enable row level security;
create policy "users see their own orders"
on public.orders for select
using (user_id = auth.uid());
create policy "users create their own orders"
on public.orders for insert
with check (user_id = auth.uid());
create policy "users update their own orders"
on public.orders for update
using (user_id = auth.uid())
with check (user_id = auth.uid());
create policy "users delete their own orders"
on public.orders for delete
using (user_id = auth.uid());
Casos comuns:
request.auth.uid == resource.data.userId→user_id = auth.uid()request.auth.token.admin == true→(auth.jwt() ->> 'role') = 'admin'resource.data.public == true→is_public = true- Regras com
get(/databases/.../doc).data.x(lookup em outra collection) → joins ou subqueries dentro da policy
Passo 4: migre usuários do Firebase Auth
Exporte os usuários com o admin SDK. Senhas do Firebase usam SHA-256 + salt (configurável pra scrypt). O SuperDB suporta importação com hash original e migração lazy (re-hash pra Argon2id no próximo login).
firebase auth:export users.json --project [PROJECT_ID]
O users.json vai vir com algo assim:
{
"users": [
{
"localId": "xYz123...",
"email": "alice@example.com",
"emailVerified": true,
"passwordHash": "BASE64==",
"salt": "BASE64==",
"createdAt": "1670000000000",
"providerUserInfo": [
{ "providerId": "google.com", "rawId": "1100...", "email": "alice@gmail.com" }
]
}
]
}
import { createClient } from '@superdb/supabase-compat'
import users from './users.json' assert { type: 'json' }
const db = createClient(
process.env.SUPERDB_URL!,
process.env.SUPERDB_SERVICE_ROLE!
)
for (const u of users.users) {
await db.auth.admin.createUser({
email: u.email,
email_confirm: u.emailVerified,
// hash externo: SuperDB aceita firebase_scrypt e re-hash no próximo login
password_hash: u.passwordHash
? `firebase_scrypt$${u.salt}$${u.passwordHash}`
: undefined,
user_metadata: {
firebase_uid: u.localId,
providers: u.providerUserInfo?.map((p) => p.providerId) ?? [],
},
})
}
OAuth-only users: usuários que entraram só via Google/Apple não têm passwordHash. Importe sem senha; eles continuam entrando pelo provider. Depois da migração, reconfigure o OAuth no Dashboard SuperDB → Auth → Providers (Passo 7).
Passo 5: Cloud Storage → SuperDB Storage
Cópia simples: lista do Firebase, download, upload no SuperDB, atualiza referências no banco se necessário.
import admin from 'firebase-admin'
import { createClient } from '@superdb/supabase-compat'
admin.initializeApp({
credential: admin.credential.cert('./service-account.json'),
storageBucket: '[PROJECT].appspot.com',
})
const sup = createClient(
process.env.SUPERDB_URL!,
process.env.SUPERDB_SERVICE_ROLE!
)
const bucket = admin.storage().bucket()
const [files] = await bucket.getFiles({ prefix: 'uploads/' })
for (const file of files) {
const [buf] = await file.download()
const { error } = await sup.storage
.from('uploads')
.upload(file.name.replace(/^uploads\//, ''), buf, {
contentType: file.metadata.contentType,
upsert: true,
})
if (error) console.error(`[fail] ${file.name}:`, error.message)
else console.log(`[ok] ${file.name}`)
}
Se as referências no banco usam URLs absolutas do Firebase (https://firebasestorage.googleapis.com/...), faça um UPDATE em SQL pra reescrever pro domínio do SuperDB. Se usam paths relativos, nada a fazer.
Passo 6: Cloud Functions?
Suas opções, da mais barata pra mais flexível:
- SQL triggers no Postgres — pra lógica de banco (audit log, cascata, validação). Roda dentro do banco, latência zero, sem custo extra. Cobre uns 50-60% dos casos típicos de Cloud Functions.
- Database webhooks — o SuperDB pode chamar uma URL HTTP em INSERT/UPDATE/DELETE. Aponta pro seu app Next.js ou pra um serviço externo. Bom pra notificações, integrações.
- Serviço externo (Vercel Functions, Railway, Cloud Run) — pra lógica complexa: pagamentos, integrações pesadas, processamento de imagem.
- Edge Functions do SuperDB — em roadmap pra Sprint 9+. Vai rodar Deno/JS no edge brasileiro.
create function public.log_order_change()
returns trigger language plpgsql as $$
begin
insert into public.audit_log (table_name, action, row_id, actor_id)
values ('orders', tg_op, new.id, auth.uid());
return new;
end $$;
create trigger orders_audit
after insert or update or delete on public.orders
for each row execute function public.log_order_change();
Passo 7: troque o SDK no frontend
Aqui é refactor de verdade — não tem compatibilidade de API entre firebase e @superdb/supabase-compat. Mas o padrão é repetitivo.
// ────────── ANTES: Firestore ──────────
import { getFirestore, collection, query, where, getDocs } from 'firebase/firestore'
import { getAuth, signInWithEmailAndPassword } from 'firebase/auth'
const auth = getAuth(app)
const db = getFirestore(app)
await signInWithEmailAndPassword(auth, email, password)
const q = query(
collection(db, 'orders'),
where('userId', '==', auth.currentUser!.uid)
)
const snap = await getDocs(q)
const orders = snap.docs.map((d) => ({ id: d.id, ...d.data() }))
// ────────── DEPOIS: SuperDB ──────────
import { createClient } from '@superdb/supabase-compat'
const db = createClient(
process.env.NEXT_PUBLIC_SUPERDB_URL!,
process.env.NEXT_PUBLIC_SUPERDB_ANON_KEY!
)
await db.auth.signInWithPassword({ email, password })
const { data: orders } = await db
.from('orders')
.select('*')
.order('created_at', { ascending: false })
// RLS já filtra por user_id = auth.uid()
Note como o filtro where('userId', '==', uid) some no SuperDB: a RLS já garante que o usuário só vê os próprios pedidos. Menos código no client, mais segurança.
Passo 8: real-time listeners
Firestore tem onSnapshot; SuperDB tem .channel().on('postgres_changes'). Casos típicos:
// ────────── ANTES: Firestore ──────────
const unsub = onSnapshot(
query(collection(db, 'messages'), where('room', '==', 'room-1')),
(snap) => {
setMessages(snap.docs.map((d) => ({ id: d.id, ...d.data() })))
}
)
// ────────── DEPOIS: SuperDB ──────────
const channel = sup
.channel('room-1')
.on('postgres_changes',
{ event: 'INSERT', schema: 'public', table: 'messages', filter: 'room=eq.room-1' },
(payload) => setMessages((prev) => [...prev, payload.new])
)
.subscribe()
// cleanup
return () => { sup.removeChannel(channel) }
Diferença prática: o onSnapshot manda o estado completo a cada mudança. O postgres_changes manda só o delta (a row inserida/atualizada/removida). Você precisa manter o estado local consistente — geralmente append em INSERT, update by id em UPDATE, filter out em DELETE.
Checklist final
- ☐ Schema Postgres desenhado e validado (FKs, indexes, constraints)
- ☐ Dados exportados do Firestore (todas as collections críticas)
- ☐ Mapa de IDs (Firebase → UUID) gerado e armazenado durante a transição
- ☐ Dados importados; spot-check de N rows aleatórias
- ☐ Security Rules traduzidas pra RLS policies (todas as tabelas RLS-on)
- ☐ Usuários do Firebase Auth importados (login com senha antiga funciona)
- ☐ OAuth providers reconfigurados (Google, Apple) com novas callback URLs
- ☐ Cloud Storage copiado pra SuperDB Storage
- ☐ Referências de URL no banco atualizadas (se aplicável)
- ☐ Frontend refatorado (queries, auth, realtime listeners)
- ☐ Cloud Functions mapeadas (triggers SQL, webhooks ou serviço externo)
- ☐ Smoke test passou em prod (login, CRUD, realtime, storage)
Quanto você vai economizar?
Exemplo concreto, baseado em um cliente real: app de e-commerce com ~10k MAU, 50GB Firestore, 100GB Cloud Storage, 5 Cloud Functions chamando ~2M vezes/mês.
| Item | Firebase (Blaze) | SuperDB Pro |
|---|---|---|
| Banco / Firestore reads-writes | ~US$ 180 | incluso |
| Storage 100GB | ~US$ 26 | incluso até 100GB |
| Cloud Functions invocations | ~US$ 80 | triggers SQL: grátis |
| Bandwidth saída | ~US$ 60 | incluso até 250GB |
| Total mensal | ~US$ 350 (≈ R$ 1.750) | R$ 99 |
Economia mensal: ~R$ 1.650, ou cerca de 94%. Em 1 ano: R$ 19.800 — geralmente paga sozinho o tempo de migração com folga.
Esses números são pra um caso específico. Apps muito read-heavy podem economizar mais; apps que dependem fortemente de Cloud Functions complexas vão precisar do serviço externo até a Sprint 9. Mande os números do seu app pra contato@superdb.com.br e a gente roda uma estimativa de bolso pra você.