Pular para o conteúdo
📖 COOKBOOK

Login sem senha — magic link em 10 linhas.

Auth passwordless via email. Usuário digita email, recebe link, clica, está logado. Mostra como customizar o template do email, configurar SMTP e proteger contra abuso.

O que vamos fazer

  • Tela de login com 1 input de email + botão "Enviar link"
  • Email automático com link único de 1 hora de validade
  • Clique no link → callback troca por sessão → usuário entra direto no app
  • Template do email customizado com sua marca (HTML editável no Dashboard)
  • Rate limit configurado pra evitar abuso (3 emails/hora por destinatário)

Pré-requisitos

  • Conta SuperDB (Free serve, com limite de 30 emails/dia via SMTP padrão)
  • Pacote @superdb/supabase-compat instalado
  • SMTP próprio configurado se quiser passar dos 30 emails/dia (recomendado: Resend, SendGrid ou SES)
💡

Dica: em produção, configure SMTP próprio mesmo no plano Pro. O SMTP compartilhado do SuperDB tem reputação variável — se outro cliente fizer spam, seus emails podem cair na lixeira junto. Resend custa $0 até 3k emails/mês.

Passo 1 — Frontend com input de email

Client component simples com estado de "enviando" → "enviado":

app/login/magic-link-form.tsx
'use client'
import { useState } from 'react'
import { createBrowserClient } from '@superdb/supabase-compat'

export function MagicLinkForm() {
  const [email, setEmail] = useState('')
  const [enviado, setEnviado] = useState(false)
  const [erro, setErro] = useState<string | null>(null)

  const db = createBrowserClient(
    process.env.NEXT_PUBLIC_SUPERDB_URL!,
    process.env.NEXT_PUBLIC_SUPERDB_ANON_KEY!
  )

  async function enviar(e: React.FormEvent) {
    e.preventDefault()
    setErro(null)
    const { error } = await db.auth.signInWithOtp({
      email,
      options: { emailRedirectTo: `${location.origin}/auth/callback` },
    })
    if (error) setErro(error.message)
    else setEnviado(true)
  }

  if (enviado) return <p>✓ Cheque seu email — link enviado pra {email}</p>

  return (
    <form onSubmit={enviar}>
      <input type="email" value={email} onChange={(e) => setEmail(e.target.value)} required />
      <button type="submit">Enviar link de acesso</button>
      {erro && <p style={{color: 'red'}}>{erro}</p>}
    </form>
  )
}

Passo 2 — Como funciona o envio

Quando o frontend chama signInWithOtp, o SuperDB:

  1. Cria um token_hash aleatório no banco (TTL 1h)
  2. Renderiza o template Magic Link com a URL {{ .ConfirmationURL }}
  3. Envia via SMTP configurado
  4. Se for primeiro login → cria user em auth.users com email_confirmed_at = null. Confirma só quando ele clicar.

Passo 3 — Callback handler

O link no email aponta pra https://api.superdb.com.br/auth/v1/verify?token_hash=...&type=email&redirect_to=.... O SuperDB valida o hash e redireciona pro seu emailRedirectTo com ?token_hash=...&type=email&next=... ainda na query.

app/auth/callback/route.ts
import { NextResponse } from 'next/server'
import { createServerClient } from '@superdb/supabase-compat/ssr'
import { cookies } from 'next/headers'

export async function GET(request: Request) {
  const { searchParams, origin } = new URL(request.url)
  const token_hash = searchParams.get('token_hash')
  const type = searchParams.get('type') as 'email' | null
  const next = searchParams.get('next') ?? '/'

  if (token_hash && type) {
    const cookieStore = await cookies()
    const db = createServerClient(
      process.env.NEXT_PUBLIC_SUPERDB_URL!,
      process.env.NEXT_PUBLIC_SUPERDB_ANON_KEY!,
      {
        cookies: {
          getAll: () => cookieStore.getAll(),
          setAll: (all) => all.forEach(({ name, value, options }) =>
            cookieStore.set(name, value, options)),
        },
      }
    )
    const { error } = await db.auth.verifyOtp({ token_hash, type })
    if (!error) return NextResponse.redirect(`${origin}${next}`)
  }

  return NextResponse.redirect(`${origin}/login?erro=link_invalido`)
}

Passo 4 — Customizar o template do email

No Dashboard: Auth → Email Templates → Magic Link. Você edita HTML com variáveis. Exemplo brand-friendly:

Dashboard → Email Templates → Magic Link
<!doctype html>
<html lang="pt-BR">
<body style="font-family: -apple-system, sans-serif; padding: 40px; background: #fafafa;">
  <div style="max-width: 480px; margin: 0 auto; background: white; padding: 32px; border-radius: 12px;">
    <h1 style="margin: 0 0 16px; font-size: 22px;">Entrar no Meu App</h1>
    <p>Olá, <strong>{{ .Email }}</strong></p>
    <p>Clique no botão abaixo pra entrar. O link expira em 1 hora.</p>
    <a href="{{ .ConfirmationURL }}"
       style="display: inline-block; background: #3ecf8e; color: white; padding: 12px 24px; border-radius: 8px; text-decoration: none; font-weight: 600;">
      Entrar agora
    </a>
    <p style="font-size: 12px; color: #71717a; margin-top: 24px;">
      Se não foi você, ignore este email.
    </p>
  </div>
</body>
</html>

Variáveis disponíveis: {{ .ConfirmationURL }}, {{ .Email }}, {{ .Token }} (o OTP de 6 dígitos, caso queira mostrar como alternativa), {{ .SiteURL }}.

⚠️

Cuidado com o pré-load de links: alguns clients de email (Outlook, Gmail "Smart features") visitam URLs do email pra preview de segurança — isso pode consumir o token antes do usuário clicar. Solução: use OTP de 6 dígitos em vez de link, OU adicione uma página intermediária que confirme com clique manual.

Resultado

Usuário entra no app sem nunca digitar senha. Para apps B2B e produtos com churn baixo, isso elimina 80% dos tickets de "esqueci a senha".

mail.google.com — Inbox
v0.4.2
SuperDB · noreply@meuapp.com
Entrar no Meu App
Clique no botão abaixo pra entrar. O link expira em 1 hora.
Entrar agora →

Variações

OTP de 6 dígitos em vez de link

Útil em mobile (paste rápido) ou se você se preocupa com pré-load de links. Mesma chamada, fluxo diferente:

app/login/otp-form.tsx
// 1. Pedir o código
await db.auth.signInWithOtp({ email, options: { shouldCreateUser: true } })

// 2. Usuário digita o código no input
await db.auth.verifyOtp({
  email,
  token: '123456',
  type: 'email',
})

Magic link com data payload

Capture referrer, plano de signup, código de promoção — o que quiser. O data entra em user_metadata:

signup-form.tsx
await db.auth.signInWithOtp({
  email,
  options: {
    emailRedirectTo: `${location.origin}/auth/callback`,
    data: {
      referrer: document.referrer,
      plano_inicial: 'pro_trial',
      utm_source: searchParams.get('utm_source'),
    },
  },
})

TTL customizado do link

Padrão é 1h. Configure no Dashboard em Auth → Settings → Email OTP Expiration. Mínimo 60s, máximo 24h. Para apps de alto risco use 15min; pra apps casuais 12h é ok.

Erros comuns

SMTP rate limit ("email rate limit exceeded")

Você atingiu o limite do SMTP compartilhado (30/dia no Free, 100/dia no Pro). Solução: configure SMTP próprio em Auth → SMTP Settings. Resend, SendGrid, AWS SES — escolha qualquer um.

Redirect URL não está na allowlist

Por segurança o SuperDB só aceita redirects pra URLs cadastradas. Erro típico: redirect_to not allowed. Solução: adicione a URL em Auth → URL Configuration → Redirect URLs. Suporta wildcards (https://meuapp.com/**).

TTL padrão é 1h. Se o usuário clica depois disso, o verifyOtp retorna 401. Solução: na página de callback, detecte o erro e mostre um botão "reenviar link" — não jogue o usuário pra uma 404.

ℹ️

Observação: magic link consome o token na primeira validação. Se o usuário clicar duas vezes (ou se o cliente de email pré-carregou), a segunda falha. Isso é por design — é o que impede replay.

Essa página ajudou?