Pular para o conteúdo
🔄 GUIAS

Migrar do Firebase pro SuperDB.

Firestore é NoSQL; SuperDB usa Postgres. A migração troca o paradigma mas ganha consultas SQL, joins, RLS e custo previsível em BRL. Este guia mostra como mapear coleções → tabelas, regras → RLS, e migrar com zero downtime.

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.

AspectoFirebaseSuperDB
Modelo de dadosNoSQL (documentos)Postgres (relacional)
ConsultasLimitadas (sem JOIN, sem aggregations complexas)SQL completo + RLS + views
CobrançaPor reads / writes / GB-mês em USDPlano fixo em BRL (Pro a R$ 99/mês)
NF-eNão emiteSim, CNPJ brasileiro
Self-hostImpossívelAGPL-3.0 (Sprint 9+)
Suporte em PT-BRDocumentação traduzida; suporte em inglêsEquipe 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:

FirebaseSuperDBObservação
CollectionTable (Postgres)users collection → public.users table
DocumentRowDocument ID vira id uuid primary key
SubcollectionTabela separada com FKusers/{uid}/orders vira orders.user_id
Security RulesRLS PoliciesSintaxe diferente, mesma intenção
Cloud FunctionsSQL Triggers ou serviço externoEdge Functions chegam na Sprint 9+
Firebase AuthSuperDB AuthFeatures equivalentes; senha re-hash lazy
Cloud StorageSuperDB StorageAPI S3-compatible + policies
Realtime Database / onSnapshotRealtime postgres_changesWebSocket; 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 projetoTempo típicoO que dominou
Pequeno (< 5 collections, < 10k users)1 diaModelar tabelas + reescrever 5-10 queries
Médio (10-30 collections, 10k-100k users)3-5 diasSubcollections viram FKs; Security Rules viram RLS
Grande (50+ collections, Cloud Functions extensas)2-4 semanasReescrita parcial de backend; planejar zero-downtime

Passo 1: exporte o Firestore

Use o gcloud CLI pra exportar pro Cloud Storage, depois baixe localmente.

Terminal — export Firestore
# 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:

scripts/export-firestore.ts
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}.

schema.sql
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:

scripts/import-to-superdb.ts
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:

firestore.rules — antes
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;
    }
  }
}
SQL — depois (SuperDB)
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.userIduser_id = auth.uid()
  • request.auth.token.admin == true(auth.jwt() ->> 'role') = 'admin'
  • resource.data.public == trueis_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).

Terminal
firebase auth:export users.json --project [PROJECT_ID]

O users.json vai vir com algo assim:

users.json (trecho)
{
  "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" }
      ]
    }
  ]
}
scripts/import-auth.ts
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.

scripts/migrate-storage.ts
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:

  1. 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.
  2. 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.
  3. Serviço externo (Vercel Functions, Railway, Cloud Run) — pra lógica complexa: pagamentos, integrações pesadas, processamento de imagem.
  4. Edge Functions do SuperDB — em roadmap pra Sprint 9+. Vai rodar Deno/JS no edge brasileiro.
SQL — trigger exemplo (substitui uma Cloud Function de audit)
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.

Firestore (antes) → SuperDB (depois)
// ────────── 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:

onSnapshot (antes) → postgres_changes (depois)
// ────────── 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.

ItemFirebase (Blaze)SuperDB Pro
Banco / Firestore reads-writes~US$ 180incluso
Storage 100GB~US$ 26incluso até 100GB
Cloud Functions invocations~US$ 80triggers SQL: grátis
Bandwidth saída~US$ 60incluso 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ê.

Essa página ajudou?