Pular para o conteúdo
📖 COOKBOOK

Login sem senha: um código de 6 dígitos no e-mail.

A pessoa digita o e-mail, recebe um código e entra. São duas chamadas do SDK, sem senha para esquecer e sem backend seu no meio. A receita também mostra o link mágico, e quando preferir cada um.

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.

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:

a página que recebe o link
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/client 0.2.2 ou mais novo, com o cliente criado com o project.
src/lib/superdb.ts
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.

src/EntrarSemSenha.tsx
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:

  1. cria a conta, se é o primeiro acesso daquele e-mail, e marca o e-mail como verificado;
  2. queima o código (ele não serve uma segunda vez);
  3. 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 com auth.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}}:

Emails → Código MFA (corpo em HTML)
<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

RegraValorPor quê
Tamanho do código6 dígitosCabe no campo numérico do celular
Validade10 minutosTempo de chegar e ser digitado
UsoUma vezCódigo já usado não entra de novo
Tentativas por código5Na sexta, o código morre e é preciso pedir outro
Pedidos de código10 por hora, por IP, no projetoNinguém usa a sua tela para mandar spam
Confirmações10 a cada 5 minutos, por IPChutar 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.

Essa página ajudou?