Pular para o conteúdo
🔄 GUIAS

Migrar do Auth0 para o SuperDB.

O Auth0 cobra por usuário ativo, em dólar. Este guia mostra o que tem equivalente no SuperDB e o que ainda não tem, e migra os usuários no primeiro login, com a mesma senha, enquanto o Auth0 continua no ar.

Por que migrar do Auth0?

  • Preço por usuário ativo, em dólar. A conta cresce junto com a base e com o câmbio. No SuperDB, o auth vem no plano fixo em real, com os usuários ativos incluídos (250 mil por mês no Pro).
  • Auth e dados no mesmo lugar. O usuário que entra já é o auth.uid() das policies de RLS do seu banco, sem sincronizar nada.
  • Recursos brasileiros. Código de login pelo WhatsApp e suporte em português.

O que tem equivalente

RecursoAuth0SuperDBObservação
E-mail e senha✓✓A conta migra no login; o hash não é importado (passo 2)
Google, GitHub, Apple✓✓Modo gerenciado ou credenciais próprias
Microsoft✓–Ainda não é self-serve
Sem senha, por e-mail✓✓Código de 6 dígitos ou link mágico (volta para as URLs cadastradas)
Código pelo WhatsApp–✓Planos Pro e Escala
MFA TOTP✓parcialCadastro e conferência; o login ainda não exige o segundo fator
WebAuthn / passkey✓–
Claims no token (Actions)✓–Não há hook de token: papel numa tabela, lido pelo RLS
Regras no login (Actions)✓✓Hooks before-signup, before-signin, before-mfa
SAML / SCIM✓–Add-on opcional, sob consulta (fora dos planos)

Se o seu app depende de uma linha com "–", essa parte fica no Auth0 por enquanto, ou espera.

Passo 1: prepare os dois lados

No SuperDB: as tabelas que têm dono ganham uma coluna com o id antigo, para o vínculo do passo 2. O user_id fica vazio até a pessoa migrar, e linha sem dono não aparece para ninguém:

superdb/pedidos.sql (SQL Editor)
create table if not exists pedidos (
  id uuid primary key default gen_random_uuid(),
  user_id uuid default auth.uid(),  -- dono no SuperDB; vazio até a pessoa migrar
  auth0_user_id text,               -- dono no Auth0 (ex.: auth0|65f...), para o vínculo
  total_centavos int not null,
  criado_em timestamptz not null default now()
);
create index if not exists pedidos_a_vincular on pedidos (auth0_user_id) where user_id is null;

alter table pedidos enable row level security;
drop policy if exists pedidos_do_dono on pedidos;
create policy pedidos_do_dono on pedidos
  for all to authenticated
  using (user_id = auth.uid()) with check (user_id = auth.uid());
revoke all on pedidos from anon;
grant select, insert, update, delete on pedidos to authenticated;

No Auth0: a migração confere a senha pelo Resource Owner Password Grant. No app que o seu servidor vai usar:

  1. Em Applications → (seu app) → Advanced Settings → Grant Types, ligue Password.
  2. Em Attack Protection, o Auth0 bloqueia o IP que erra muitas senhas, e aqui todas as tentativas saem do seu servidor. Ligue Trust Token Endpoint IP Header no app e mande o IP do usuário no header auth0-forwarded-for, como no código abaixo.

Passo 2: usuários, migrados no primeiro login

O export de usuários do Auth0 não traz a senha (o hash só sai com um pedido ao suporte do Auth0), e a importação de usuários do painel do SuperDB é a do formato do Supabase (senha em bcrypt). Por isso, aqui, cada pessoa migra no primeiro login, enquanto o Auth0 ainda está no ar:

  1. A tela de login tenta o SuperDB. Deu certo: a pessoa já migrou.
  2. Não deu: o app manda e-mail e senha para uma rota do seu servidor, que confere a senha no Auth0.
  3. A senha confere: o servidor cria a conta no SuperDB com a mesma senha (signUp), passa os dados daquele auth0_user_id para a conta nova e devolve a sessão.
app/api/migrar-login/route.ts (servidor)
import { createClient } from '@superdb/client'

const SUPERDB_URL = process.env.SUPERDB_URL!           // https://auth.superdb.com.br
const PROJETO = process.env.SUPERDB_PROJECT!
const admin = createClient(SUPERDB_URL, process.env.SUPERDB_SERVICE_ROLE_KEY!, { project: PROJETO })
const TABELAS_COM_DONO = ['pedidos']

// Um cliente por pedido, com a sessão só em memória: nada vaza entre usuários.
function clienteDoPedido() {
  const m = new Map<string, string>()
  return createClient(SUPERDB_URL, process.env.SUPERDB_ANON_KEY!, {
    project: PROJETO,
    storage: {
      getItem: (k) => m.get(k) ?? null,
      setItem: (k, v) => { m.set(k, v) },
      removeItem: (k) => { m.delete(k) },
    },
  })
}

// A senha confere no Auth0? É a prova de que a conta antiga é desta pessoa.
async function idNoAuth0(email: string, senha: string, ipDoUsuario: string) {
  const token = await fetch(`https://${process.env.AUTH0_DOMAIN}/oauth/token`, {
    method: 'POST',
    headers: { 'Content-Type': 'application/json', 'auth0-forwarded-for': ipDoUsuario },
    body: JSON.stringify({
      grant_type: 'http://auth0.com/oauth/grant-type/password-realm',
      realm: 'Username-Password-Authentication',
      username: email,
      password: senha,
      client_id: process.env.AUTH0_CLIENT_ID,
      client_secret: process.env.AUTH0_CLIENT_SECRET,
      scope: 'openid email',
    }),
  })
  if (!token.ok) return null
  const { access_token } = (await token.json()) as { access_token: string }
  const eu = await fetch(`https://${process.env.AUTH0_DOMAIN}/userinfo`, {
    headers: { Authorization: `Bearer ${access_token}` },
  })
  return eu.ok ? ((await eu.json()) as { sub: string }).sub : null // ex.: auth0|65f...
}

export async function POST(req: Request) {
  const { email, senha, novaSenha } = (await req.json()) as { email: string; senha: string; novaSenha?: string }
  const ip = req.headers.get('x-forwarded-for')?.split(',')[0]?.trim() ?? ''

  const auth0Id = await idNoAuth0(email, senha, ip)
  if (!auth0Id) return Response.json({ erro: 'E-mail ou senha incorretos.' }, { status: 401 })

  // O SuperDB pede no mínimo 8 caracteres. Se a senha antiga não serve, a tela pede uma nova.
  const senhaFinal = novaSenha ?? senha
  if (senhaFinal.length < 8) return Response.json({ precisaSenhaNova: true }, { status: 409 })

  const { data, error } = await clienteDoPedido().auth.signUp({ email, password: senhaFinal })
  if (error?.code === 'password_breached') return Response.json({ precisaSenhaNova: true }, { status: 409 })
  if (error) return Response.json({ erro: 'Não deu para migrar a conta agora. Fale com o suporte.' }, { status: 409 })

  // Os dados que eram desta conta no Auth0 passam a ser dela no SuperDB.
  for (const tabela of TABELAS_COM_DONO) {
    await admin.from(tabela).update({ user_id: data.user.id }).eq('auth0_user_id', auth0Id).is('user_id', null)
  }

  const { access_token, refresh_token, data_plane_token, expires_in } = data.session
  return Response.json({ access_token, refresh_token, data_plane_token, expires_in })
}

No navegador, o login tenta o SuperDB primeiro e, se a resposta for invalid_credentials, chama a rota acima e aplica a sessão com db.auth.setSession(corpo). É o mesmo entrar() do guia do Firebase.

⚠️

Vincule só depois da prova. O servidor passa os dados para a conta nova porque acabou de conferir a senha no Auth0. Não vincule pelo e-mail sozinho: o cadastro com senha não confirma o e-mail, então um e-mail na conta não prova que ela é de quem diz.

  • Limite de cadastro: 30 por minuto por IP no projeto, e aqui todos saem do IP do seu servidor. Numa virada com muita gente, alguns recebem 429 e tentam de novo em seguida.
  • Enquanto a migração dura: um hook before-signup (em Hooks, no painel) pode recusar cadastro com senha de um e-mail que ainda está no Auth0 quando não vem do seu servidor. É uma regra de negócio, não uma trava: se o seu endpoint cair, o SuperDB deixa passar.
  • Depois de algumas semanas, quem não voltou ainda está só no Auth0. Mantenha a rota no ar por mais tempo, ou avise essas pessoas para entrarem com o código por e-mail e vincule os dados delas pelo suporte.

Passo 3: login social

Configure cada provedor em Autenticação → Login social (OAuth), no modo gerenciado ou com credenciais próprias. Com credenciais próprias, a URL de callback que vai no console do provedor é https://auth.superdb.com.br/auth/v1/callback/<provider>. Dá para reaproveitar o client do Google que o Auth0 usava: basta acrescentar essa URL a ele. O passo a passo está em Signup com Google.

Para quem entrava só pelo Google no Auth0, o vínculo dos dados segue a mesma regra da prova: a pessoa entra com Google no SuperDB e, uma vez, também pelo Auth0, ainda ativo. O servidor recebe o access token do Auth0, pergunta o sub em /userinfo e vincula por ele.

Passo 4: MFA

Os segredos TOTP do Auth0 (Guardian) não saem no export. Quem usava MFA cadastra o autenticador de novo no SuperDB, com QR code novo: db.auth.mfa.enroll({ factorType: 'totp' }). Hoje o login do SuperDB não exige o segundo fator; o cookbook de MFA mostra o que ele faz e como exigir o código em ações sensíveis, conferido no seu servidor.

Passo 5: Actions, claims e papéis

No Auth0, uma Action põe o papel do usuário no token, e o app confere o papel a cada requisição. No SuperDB não existe hook que altere o token. O papel mora numa tabela, e a policy de RLS consulta essa tabela:

antes: Auth0 Action
exports.onExecutePostLogin = async (event, api) => {
  const role = event.user.app_metadata.role ?? 'user'
  api.accessToken.setCustomClaim('https://app.com/role', role)
}
depois: papel numa tabela, lido pela policy
create table if not exists papeis (
  user_id uuid primary key,
  papel text not null check (papel in ('membro', 'admin'))
);
alter table papeis enable row level security;
drop policy if exists papeis_ler_o_proprio on papeis;
create policy papeis_ler_o_proprio on papeis
  for select to authenticated using (user_id = auth.uid());
revoke all on papeis from anon;
grant select on papeis to authenticated;

-- admin também lê todos os pedidos (soma à policy do dono, que continua valendo)
drop policy if exists pedidos_admin_le_tudo on pedidos;
create policy pedidos_admin_le_tudo on pedidos
  for select to authenticated
  using (exists (select 1 from papeis p where p.user_id = auth.uid() and p.papel = 'admin'));

O papel de cada pessoa vem do app_metadata do Auth0: grave em papeis na rota de migração, logo depois do signUp, com a service key. De bônus, mudar um papel vale na hora, sem esperar o token ser renovado.

Actions que bloqueiam login ou cadastro (por domínio de e-mail, por país) viram hooks before-signup e before-signin: o SuperDB chama uma URL sua, assinada com HMAC, e recusa se a resposta for {"allow": false}. Se a sua URL não responder, o acesso passa: o hook é regra de negócio, não trava de segurança.

Checklist final

  • ☐ Tabelas com dono ganharam auth0_user_id; dados importados com a service key
  • ☐ Grant Type Password ligado no app do Auth0, com o IP do usuário em auth0-forwarded-for
  • ☐ Rota de migração testada com uma conta real, inclusive senha com menos de 8 caracteres
  • ☐ Login social configurado no SuperDB e vínculo das contas só-sociais testado
  • ☐ Papéis levados para a tabela papeis e policies de RLS revisadas
  • ☐ Quem tinha MFA foi avisado para cadastrar o autenticador de novo
  • ☐ Data para desligar a rota de migração combinada com o time
Essa página ajudou?