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
| Recurso | Auth0 | SuperDB | Observaçã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 | ✓ | parcial | Cadastro 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:
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:
- Em Applications → (seu app) → Advanced Settings → Grant Types, ligue Password.
- 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:
- A tela de login tenta o SuperDB. Deu certo: a pessoa já migrou.
- Não deu: o app manda e-mail e senha para uma rota do seu servidor, que confere a senha no Auth0.
- A senha confere: o servidor cria a conta no SuperDB com a mesma senha (
signUp), passa os dados daqueleauth0_user_idpara a conta nova e devolve a sessão.
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:
exports.onExecutePostLogin = async (event, api) => {
const role = event.user.app_metadata.role ?? 'user'
api.accessToken.setCustomClaim('https://app.com/role', role)
}
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
papeise 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