O que vamos fazer
- Uma tela com um campo de e-mail e, depois, um campo para o código.
db.auth.signInWithOtp({ email })manda um código de 6 dígitos, que vale 10 minutos.db.auth.verifyOtp({ email, code })confere o código e devolve a sessão.- No primeiro acesso, a conta é criada na hora, já com o e-mail verificado: o código é a prova de que a pessoa lê aquela caixa de entrada.
- O e-mail sai com a sua marca, pelo editor de templates do painel.
Código ou link mágico
O link mágico também funciona: POST /auth/v1/signin/magic-link com email e redirect_to. O link do e-mail volta para o redirect_to se ele estiver cadastrado em Autenticação → URLs de redirecionamento, ou se for o endereço do app. Página não cadastrada não recebe a pessoa: a chamada é recusada com redirect_to_nao_cadastrado, e nenhum e-mail é enviado. A sessão chega no fragmento da URL (#access_token=…&refresh_token=…), que não vai ao seu servidor, e a página a entrega ao SDK:
const p = new URLSearchParams(location.hash.slice(1))
if (p.get('access_token')) {
await db.auth.setSession({
access_token: p.get('access_token'),
refresh_token: p.get('refresh_token'),
})
// O link não traz o data_plane_token, que o db.from() usa. A renovação traz.
await db.auth.refreshSession()
history.replaceState(null, '', location.pathname + location.search)
}
Link vencido ou já usado volta com ?error=… na URL.
O código tem vantagens próprias, e muita equipe o prefere ao link:
- Não depende de deep link. O mesmo fluxo serve para site, app de celular e desktop.
- O filtro de segurança do e-mail corporativo não queima o acesso. Muitos filtros abrem cada link da mensagem para inspecionar; um código de uso único não é consumido por eles.
- A pessoa pode ler no celular e digitar no computador. Com link, ela precisa abrir o e-mail no mesmo aparelho em que quer entrar.
Pré-requisitos
- Um projeto no SuperDB, em qualquer plano. O envio de e-mail já vem pronto: não há SMTP para configurar.
@superdb/client0.2.2 ou mais novo, com o cliente criado com oproject.
import { createClient } from '@superdb/client'
export const db = createClient(
import.meta.env.VITE_SUPERDB_URL, // https://auth.superdb.com.br
import.meta.env.VITE_SUPERDB_ANON_KEY, // a chave anon, pública
{ project: import.meta.env.VITE_SUPERDB_PROJECT },
)
Passo 1: a tela de e-mail e código
O exemplo é em React, mas as duas chamadas são as mesmas em qualquer framework. A tela tem duas etapas: primeiro o e-mail, depois o código.
import { useState, type FormEvent } from 'react'
import { db } from './lib/superdb'
const MENSAGENS: Record<string, string> = {
invalid_otp: 'Código errado ou vencido. Confira os 6 dígitos ou peça outro.',
rate_limited: 'Muitas tentativas. Espere alguns minutos e tente de novo.',
invalid_request: 'Confira o e-mail digitado.',
}
export function EntrarSemSenha({ aoEntrar }: { aoEntrar: () => void }) {
const [email, setEmail] = useState('')
const [codigo, setCodigo] = useState('')
const [etapa, setEtapa] = useState<'email' | 'codigo'>('email')
const [erro, setErro] = useState<string | null>(null)
const [enviando, setEnviando] = useState(false)
const alvo = email.trim().toLowerCase()
async function pedirCodigo(e: FormEvent) {
e.preventDefault()
setErro(null)
setEnviando(true)
const { error } = await db.auth.signInWithOtp({ email: alvo })
setEnviando(false)
if (error) return setErro(MENSAGENS[error.code] ?? 'Não deu para enviar agora. Tente de novo.')
setEtapa('codigo')
}
async function confirmar(e: FormEvent) {
e.preventDefault()
setErro(null)
setEnviando(true)
const { error } = await db.auth.verifyOtp({ email: alvo, code: codigo.trim() })
setEnviando(false)
if (error) return setErro(MENSAGENS[error.code] ?? 'Não deu para entrar agora. Tente de novo.')
aoEntrar()
}
if (etapa === 'email') {
return (
<form onSubmit={pedirCodigo}>
<label htmlFor="email">Seu e-mail</label>
<input id="email" type="email" autoComplete="email" required
value={email} onChange={(e) => setEmail(e.target.value)} />
<button disabled={enviando}>{enviando ? 'Enviando…' : 'Receber código'}</button>
{erro && <p role="alert">{erro}</p>}
</form>
)
}
return (
<form onSubmit={confirmar}>
<p>Mandamos um código para <strong>{alvo}</strong>. Ele vale 10 minutos.</p>
<label htmlFor="codigo">Código</label>
<input id="codigo" inputMode="numeric" autoComplete="one-time-code"
pattern="[0-9]{6}" maxLength={6} required
value={codigo} onChange={(e) => setCodigo(e.target.value)} />
<button disabled={enviando}>{enviando ? 'Entrando…' : 'Entrar'}</button>
<button type="button" onClick={() => { setCodigo(''); setEtapa('email') }}>
Usar outro e-mail ou pedir outro código
</button>
{erro && <p role="alert">{erro}</p>}
</form>
)
}
autoComplete="one-time-code" faz o celular oferecer o código em cima do teclado quando ele chega por SMS e, no iPhone com iOS 17 ou mais novo, também quando chega pelo app Mail. Não custa nada e poupa digitação.
Passo 2: o que acontece em cada chamada
signInWithOtp({ email }) gera um código de 6 dígitos, grava só o hash dele e manda o e-mail. A resposta é sempre { ok: true }, exista ou não uma conta com aquele endereço: assim ninguém usa a sua tela para descobrir quem é cliente. O canal padrão é o e-mail; options: { channel: 'whatsapp' } manda pelo WhatsApp, como na receita de WhatsApp.
verifyOtp({ email, code }) confere o código e, se estiver certo:
- cria a conta, se é o primeiro acesso daquele e-mail, e marca o e-mail como verificado;
- queima o código (ele não serve uma segunda vez);
- devolve a sessão, que o SDK guarda sozinho. A partir daí,
db.from()já consulta com o usuário logado, e as policies de RLS comauth.uid()funcionam.
A sessão vale 1 hora. Desde a versão 0.3, o SDK renova sozinho um minuto antes de vencer, inclusive com várias abas abertas. Numa versão anterior, chame db.auth.refreshSession() antes de vencer para o usuário não cair do app; os templates oficiais trazem um sessao.ts pronto que faz isso. Detalhes em SDK JS → Auth.
Passo 3: o e-mail com a sua marca
No painel, abra o projeto e vá em Emails. O código de login usa o template Código MFA, o mesmo do código de verificação. Dá para editar o assunto e o corpo, no modo visual ou em HTML. As variáveis são {{code}} e {{expires_in_minutes}}:
<div style="font-family: system-ui, sans-serif; max-width: 480px; margin: 0 auto; padding: 32px;">
<h1 style="font-size: 20px; margin: 0 0 12px;">Seu código para entrar no Meu App</h1>
<p>Digite este código na tela de login:</p>
<p style="font-size: 32px; font-weight: 700; letter-spacing: 6px; margin: 16px 0;">{{code}}</p>
<p style="color: #666; font-size: 13px;">
Ele vale {{expires_in_minutes}} minutos. Se não foi você, ignore este e-mail.
</p>
</div>
Na mesma tela, em Remetente dos emails, você verifica o seu próprio domínio para o código sair de um endereço seu, e não do SuperDB.
Limites e segurança
| Regra | Valor | Por quê |
|---|---|---|
| Tamanho do código | 6 dígitos | Cabe no campo numérico do celular |
| Validade | 10 minutos | Tempo de chegar e ser digitado |
| Uso | Uma vez | Código já usado não entra de novo |
| Tentativas por código | 5 | Na sexta, o código morre e é preciso pedir outro |
| Pedidos de código | 10 por hora, por IP, no projeto | Ninguém usa a sua tela para mandar spam |
| Confirmações | 10 a cada 5 minutos, por IP | Chutar 1 milhão de combinações fica inviável |
Erros comuns
invalid_otp (401) ao confirmar
O código está errado, venceu (mais de 10 minutos), já foi usado ou passou das 5 tentativas. A resposta é a mesma nos quatro casos, de propósito. Mostre a mensagem e ofereça pedir outro código.
rate_limited (429)
Muitos pedidos do mesmo IP. Em escritório ou escola, várias pessoas saem pelo mesmo IP; se isso for comum no seu público, mostre um aviso claro para esperar alguns minutos em vez de um erro genérico.
O e-mail não chegou
Peça para olhar o spam e a aba Promoções. Se o projeto ainda envia pelo remetente padrão, verificar o seu domínio em Emails → Remetente dos emails costuma resolver a entrega. Lembre que a resposta do envio é sempre ok: um e-mail digitado errado também "dá certo" na tela, por isso a etapa do código mostra o endereço, para a pessoa conferir.
A pessoa já tinha conta com senha
Tudo bem: o código entra na mesma conta, porque o login casa pelo e-mail. Senha e código convivem, e cada um continua funcionando.