Voltar ao blog
Tutorial

Supabase Auth: autenticação segura do zero

Por Flávio Emanuel · · 9 min de leitura

Autenticação é uma das partes mais importantes do seu app. E é também uma das mais frágeis se fizer errado.

Eu usava Firebase há anos. Mudei pro Supabase. A razão? RLS (Row Level Security). Você define regras de acesso diretamente no banco de dados. É seguro por padrão.

Com Firebase, qualquer cliente conseguia ler qualquer dado. Você precisava adicionar regras de segurança no Realtime Database. Era fácil esquecer. Com Supabase, o banco de dados é o guardião.

Setup básico: criando um usuário

Supabase Auth é baseado em JWT. Quando você faz login, recebe um token. Manda aquele token em todo request e Supabase valida.

Primeira coisa: criar uma tabela de perfil do usuário. Não é obrigatório, mas é bom ter informações extras além do email.

create table public.profiles (
  id uuid references auth.users on delete cascade,
  name text,
  avatar_url text,
  created_at timestamp default now(),
  primary key (id)
);

Depois, você quer um trigger que cria o perfil automaticamente quando o usuário se registra:

create function public.handle_new_user()
returns trigger as $$
begin
  insert into public.profiles (id, name)
  values (new.id, new.email);
  return new;
end;
$$ language plpgsql security definer;

create trigger on_auth_user_created
  after insert on auth.users
  for each row execute procedure public.handle_new_user();

Simples. Usuário se registra, perfil é criado automaticamente.

Login: email/password

Eu uso React pros meus projetos. Aqui está o padrão que funciona:

import { createClient } from '@supabase/supabase-js'

const supabase = createClient(
  import.meta.env.VITE_SUPABASE_URL,
  import.meta.env.VITE_SUPABASE_ANON_KEY
)

async function handleLogin(email, password) {
  const { data, error } = await supabase.auth.signInWithPassword({
    email,
    password,
  })

  if (error) {
    console.error('Login failed:', error.message)
    return
  }

  // data.session contém o token
  // data.user contém informações do usuário
  localStorage.setItem('session', JSON.stringify(data.session))
}

Você pode guardar a sessão no localStorage ou no memory. Supabase automaticamente renova o refresh token.

Pra sign up:

async function handleSignUp(email, password, name) {
  const { data, error } = await supabase.auth.signUp({
    email,
    password,
    options: {
      data: { name }
    }
  })

  if (error) throw error
}

A data retornada tem o usuário criado. Supabase manda um email de confirmação. Você pode desabilitar isso no painel de admin se quiser.

OAuth: Google login

Supabase suporta Google, GitHub, Discord, e mais. Você configura no painel e depois é uma linha de código:

async function signInWithGoogle() {
  const { data, error } = await supabase.auth.signInWithOAuth({
    provider: 'google',
    options: {
      redirectTo: `${import.meta.env.VITE_APP_URL}/auth/callback`
    }
  })
}

Isso abre a popup do Google. Usuário faz login. Volta pra sua callback URL com o token. Supabase cuida do resto.

A callback URL precisa fazer isso:

// pages/auth/callback.jsx (Astro)
import { supabase } from '@lib/supabase'

export async function getServerSideProps({ url }) {
  const code = url.searchParams.get('code')

  if (code) {
    await supabase.auth.exchangeCodeForSession(code)
  }

  return {
    redirect: {
      destination: '/',
      permanent: false,
    },
  }
}

Pronto. Usuário tá logado.

Usando sessão no app

Você vai querer saber quem está logado. Isso aqui funciona em qualquer lugar:

async function getUser() {
  const { data, error } = await supabase.auth.getUser()

  if (error) {
    console.log('User not logged in')
    return null
  }

  return data.user
}

Se você quer reativar a sessão no carregamento do app:

const { data } = await supabase.auth.getSession()

if (data.session) {
  console.log('User is logged in')
} else {
  console.log('User is logged out')
}

Supabase cuida de renovar o refresh token automaticamente. Você não precisa fazer nada.

Row Level Security (RLS): a parte que importa

RLS é onde a segurança realmente mora. Você escreve políticas SQL que dizem: “esse usuário só pode ver seus próprios dados”.

Primeiro, você ativa RLS na tabela:

alter table public.profiles enable row level security;

Depois, cria uma política. Exemplo: usuário só vê seu próprio perfil:

create policy "Users can view own profile"
  on public.profiles
  for select
  using (auth.uid() = id);

Agora ninguém consegue fazer select * from profiles. Supabase automaticamente filtra pra mostrar só o perfil do usuário logado.

Outro exemplo: clínica dentária. Paciente só vê seu próprio agendamento:

create table public.appointments (
  id uuid primary key default gen_random_uuid(),
  patient_id uuid references auth.users not null,
  clinic_id uuid not null,
  scheduled_at timestamp not null
);

alter table public.appointments enable row level security;

create policy "Patients see only their appointments"
  on public.appointments
  for select
  using (auth.uid() = patient_id);

create policy "Patients can book appointments"
  on public.appointments
  for insert
  with check (auth.uid() = patient_id);

Agora é impossível um usuário ver agendamentos de outro. Está no banco de dados. Nem seu código pode pular isso.

Testando autenticação

Pra testar, você precisa realmente logar. Supabase fornece um cliente pra Node:

const { createClient } = require('@supabase/supabase-js')

const supabase = createClient(
  process.env.SUPABASE_URL,
  process.env.SUPABASE_SERVICE_ROLE_KEY // Use service role pra testes
)

// Cria um usuário
const { data, error } = await supabase.auth.admin.createUser({
  email: 'test@example.com',
  password: 'password123',
  email_confirm: true
})

// Depois testa com aquela conta

Service role key é como super admin. Nunca exponha isso no frontend.

Checklist de segurança

  • Usar HTTPS em produção (Vercel cuida disso)
  • Ativar RLS em TODAS as tabelas
  • Nunca usar service role key no frontend
  • Testar que usuário anônimo não consegue acessar dados
  • Configurar email de reset de senha
  • Habilitar verificação de email pra produção
  • Rodar testes de autenticação antes de deploy

Supabase Auth com RLS é uma combinação poderosa. Você tem segurança integrada no banco de dados, não precisa de código extra.

Refresh tokens: deixa automático

Um erro comum que vejo: dev tenta gerenciar refresh tokens manualmente.

Supabase trata disso automático. Mas você precisa configurar certo.

import { createClient } from '@supabase/supabase-js'

const supabase = createClient(
  import.meta.env.VITE_SUPABASE_URL,
  import.meta.env.VITE_SUPABASE_ANON_KEY,
  {
    auth: {
      persistSession: true,
      detectSessionInUrl: true,
    }
  }
)

Com persistSession: true, Supabase automaticamente:

  1. Guarda token no localStorage
  2. Quando token expira, usa refresh token pra gerar novo
  3. Você não mexe em nada

Se você não coloca isso, usuário tá logado 5 minutos e perde sessão. Experiência ruim.

Múltiplos provedores OAuth

Supabase suporta Google, GitHub, Discord, Microsoft, LinkedIn, Apple. Você pode colocar todos:

async function signInWithProvider(provider) {
  const { data, error } = await supabase.auth.signInWithOAuth({
    provider: provider, // 'google', 'github', 'discord', etc
    options: {
      redirectTo: `${import.meta.env.VITE_APP_URL}/auth/callback`
    }
  })
}

Usei isso em projeto de consultório onde paciente pode logar com Google (usuário comum) ou Facebook (usuário técnico). Mesma aplicação, provedores diferentes.

Segurança: nunca guardar token manualmente

Erro que mais vejo:

// NÃO FAÇA ISSO
localStorage.setItem('token', data.session.access_token)

Depois, em request:

// NÃO FAÇA ISSO
const token = localStorage.getItem('token')
fetch('/api/dados', {
  headers: { Authorization: `Bearer ${token}` }
})

Por quê é ruim? Se seu token vazar, tá vazado pra sempre.

Supabase já guarda e renova. Você só precisa usar:

const { data, error } = await supabase
  .from('dados')
  .select('*')

Supabase automático coloca token em header, renova quando expira, você nem toca.

Revogar acesso

Às vezes você precisa deslogar usuário:

async function logout() {
  const { error } = await supabase.auth.signOut()
  if (error) console.error('Logout failed:', error)
}

Supabase automaticamente invalida token. Se usuário tenta usar session antiga, é rejeitado.

Útil pra security breach, ou quando usuário muda senha. Todos os tokens antigos ficam inválidos.

RLS avançado: multi-tenant

Cenário: aplicação de agendamento pra múltiplas clínicas. Paciente de clínica A não pode ver agendamento de clínica B.

-- Tabela de clínicas
create table clinics (
  id uuid primary key,
  name text,
  owner_id uuid references auth.users
);

-- Tabela de pacientes (multi-tenant)
create table patients (
  id uuid primary key,
  clinic_id uuid references clinics not null,
  user_id uuid references auth.users,
  name text
);

-- Ativar RLS
alter table patients enable row level security;

-- Política: usuário vê pacientes só da sua clínica
create policy "Users see patients from their clinic"
  on patients
  for select
  using (
    clinic_id = (
      select id from clinics where owner_id = auth.uid()
    )
  );

Agora, se você faz select * from patients, Supabase automaticamente filtra pra mostrar só pacientes da clínica do usuário logado.

Impossível fazer query fora desse escopo.

Auditoria: rastrear quem mudou o quê

Supabase tem audit logs built-in. Vai em admin panel > Logs.

Mas para auditoria customizada, criar uma tabela:

create table audit_log (
  id uuid primary key default gen_random_uuid(),
  table_name text not null,
  user_id uuid references auth.users,
  action text not null, -- 'INSERT', 'UPDATE', 'DELETE'
  old_data jsonb,
  new_data jsonb,
  created_at timestamp default now()
);

-- Trigger em pacientes
create function log_patient_changes()
returns trigger as $$
begin
  insert into audit_log (table_name, user_id, action, old_data, new_data)
  values (
    'patients',
    auth.uid(),
    TG_OP,
    row_to_json(OLD),
    row_to_json(NEW)
  );
  return COALESCE(NEW, OLD);
end;
$$ language plpgsql;

create trigger patients_audit
  after insert or update or delete on patients
  for each row execute function log_patient_changes();

Agora toda mudança em pacientes é registrada com quem fez, quando fez, o que mudou.

Útil pra compliance, LGPD, auditoria interna.

Testando tudo local

Supabase fornece CLI pra rodar local:

supabase start

Isso sobe Postgres, Auth, Storage tudo local. Você desenvolve offline, não precisa staging.

Depois quando pronto:

supabase push

Manda migrações pra production.

Com MVP do zero ao deploy, falei sobre isso. Desenvolver local é importante.

  • Configurar Supabase Auth com persistSession
  • Implementar login com 2+ provedores OAuth
  • Criar RLS básico (usuário vê só seus dados)
  • Adicionar RLS multi-tenant (clínica A vs B)
  • Configurar audit log pra compliance
  • Testar revogação de tokens
  • Setup de desenvolvimento local com CLI
  • Rodar migrations em staging antes de prod

Supabase Auth bem feito é segurança que seu cliente paga pra ter.

Próximo passo

Precisa de um dev que entrega de verdade?

Seja pra um projeto pontual, reforço no time, ou parceria de longo prazo. Vamos conversar.

Falar no WhatsApp

Respondo em até 2h durante horário comercial.