Ir para o conteúdo
Nextjs

Supabase como Backend Completo para Next.js

Marcos Soares
Atualizado em 
13 minutos de leitura
Ilustração 3D de cubo translúcido com quatro camadas luminosas representando Supabase como backend completo para Next.js
Ouça este artigo
0:00Supabase como Backend Completo para Next.js--:--

Conteúdo técnico toda semana

Receba artigos sobre arquitetura, padrões de projeto e engenharia de software. Direto no seu e-mail, sem enrolação.

Sem spam. Cancele a qualquer momento com 1 clique.

Neste artigo

O problema que o Supabase resolve (e o que ele não resolve)

Montar um backend para uma aplicação Next.js exige decisões sobre autenticação, banco de dados, storage de arquivos e, dependendo do caso, comunicação em tempo real. A abordagem clássica envolve configurar um PostgreSQL, um serviço de auth (Auth0, Clerk, NextAuth), um S3 para arquivos e um servidor WebSocket separado. São quatro serviços, quatro faturas, quatro pontos de falha.

O Supabase empacota tudo isso em cima de um PostgreSQL real: auth com JWT, storage com políticas de acesso, realtime via WebSockets e uma API REST gerada automaticamente pelo PostgREST. A proposta é eliminar boilerplate de infraestrutura sem abrir mão do controle que um banco relacional oferece.

Onde o Supabase não encaixa: se você precisa de lógica de negócio complexa no backend (filas, workers, orquestração de serviços), ele não substitui um servidor dedicado. Nesses casos, o Supabase funciona como camada de dados e auth, e a lógica fica em API Routes do Next.js ou em um serviço separado.

Instalação e configuração inicial

Crie um projeto no dashboard do Supabase e copie a SUPABASE_URL e a SUPABASE_ANON_KEY. A anon key é pública por design: a segurança vem do Row Level Security, não do sigilo dessa chave.

Bash
npm install @supabase/supabase-js @supabase/ssr

Crie o arquivo de variáveis de ambiente:

Bash
# .env.local
NEXT_PUBLIC_SUPABASE_URL=https://seuprojetoid.supabase.co
NEXT_PUBLIC_SUPABASE_ANON_KEY=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...

O prefixo NEXT_PUBLIC_ expõe essas variáveis no client. Isso é intencional: a anon key só permite operações que as políticas RLS autorizam.

Criando os clientes: server e browser

O @supabase/ssr oferece helpers para criar clientes que gerenciam cookies corretamente no App Router. A separação entre client de servidor e client de browser é obrigatória porque o Next.js executa código nos dois ambientes.

TYPESCRIPT
// src/lib/supabase/server.ts
import { createServerClient } from "@supabase/ssr";
import { cookies } from "next/headers";
 
export async function createSupabaseServer() {
  const cookieStore = await cookies();
 
  return createServerClient(
    process.env.NEXT_PUBLIC_SUPABASE_URL!,
    process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!,
    {
      cookies: {
        // getAll/setAll é a API recomendada pelo @supabase/ssr v0.5+
        // evita problemas de sincronização com cookies individuais
        getAll() {
          return cookieStore.getAll();
        },
        setAll(cookiesToSet) {
          try {
            cookiesToSet.forEach(({ name, value, options }) =>
              cookieStore.set(name, value, options)
            );
          } catch {
            // setAll falha em Server Components (read-only)
            // funciona em Server Actions e Route Handlers
          }
        },
      },
    }
  );
}
TYPESCRIPT
// src/lib/supabase/browser.ts
import { createBrowserClient } from "@supabase/ssr";
 
export function createSupabaseBrowser() {
  return createBrowserClient(
    process.env.NEXT_PUBLIC_SUPABASE_URL!,
    process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!
  );
}

O middleware renova tokens expirados antes que a request chegue ao Server Component:

TYPESCRIPT
// src/middleware.ts
import { createServerClient } from "@supabase/ssr";
import { NextResponse, type NextRequest } from "next/server";
 
export async function middleware(request: NextRequest) {
  let supabaseResponse = NextResponse.next({ request });
 
  const supabase = createServerClient(
    process.env.NEXT_PUBLIC_SUPABASE_URL!,
    process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!,
    {
      cookies: {
        getAll() {
          return request.cookies.getAll();
        },
        setAll(cookiesToSet) {
          // Propaga cookies tanto na request quanto na response
          // sem isso, Server Components recebem sessão stale
          cookiesToSet.forEach(({ name, value }) =>
            request.cookies.set(name, value)
          );
          supabaseResponse = NextResponse.next({ request });
          cookiesToSet.forEach(({ name, value, options }) =>
            supabaseResponse.cookies.set(name, value, options)
          );
        },
      },
    }
  );
 
  // getUser() valida o JWT no servidor do Supabase
  // getSession() confia no token local e pode ser spoofado
  const { data: { user } } = await supabase.auth.getUser();
 
  if (!user && request.nextUrl.pathname.startsWith("/dashboard")) {
    const url = request.nextUrl.clone();
    url.pathname = "/login";
    return NextResponse.redirect(url);
  }
 
  return supabaseResponse;
}
 
export const config = {
  matcher: ["/((?!_next/static|_next/image|favicon.ico|api/webhooks).*)"],
};

Autenticação com Server Actions

Em vez de criar Route Handlers para login e signup, Server Actions simplificam o fluxo porque executam no servidor e podem setar cookies diretamente.

TYPESCRIPT
// src/app/login/actions.ts
"use server";
 
import { redirect } from "next/navigation";
import { createSupabaseServer } from "@/lib/supabase/server";
 
export async function loginWithEmail(formData: FormData) {
  const supabase = await createSupabaseServer();
 
  const { error } = await supabase.auth.signInWithPassword({
    email: formData.get("email") as string,
    password: formData.get("password") as string,
  });
 
  if (error) {
    // Retornar o erro para o client tratar na UI
    return { error: error.message };
  }
 
  redirect("/dashboard");
}
 
export async function signUpWithEmail(formData: FormData) {
  const supabase = await createSupabaseServer();
 
  const { error } = await supabase.auth.signUp({
    email: formData.get("email") as string,
    password: formData.get("password") as string,
    options: {
      // emailRedirectTo precisa bater com o Site URL configurado no Supabase
      emailRedirectTo: `${process.env.NEXT_PUBLIC_SUPABASE_URL}/auth/v1/callback`,
    },
  });
 
  if (error) {
    return { error: error.message };
  }
 
  return { success: "Verifique seu e-mail para confirmar o cadastro." };
}

Schema e Row Level Security

O Supabase sem RLS é um banco aberto para qualquer pessoa com a anon key. Habilitar RLS e escrever políticas é o passo que transforma a anon key de vulnerabilidade em design intencional.

SQL
-- Tabela de projetos vinculados ao usuário autenticado
CREATE TABLE projects (
  id UUID DEFAULT gen_random_uuid() PRIMARY KEY,
  user_id UUID REFERENCES auth.users(id) ON DELETE CASCADE NOT NULL,
  name TEXT NOT NULL CHECK (char_length(name) BETWEEN 1 AND 200),
  description TEXT,
  created_at TIMESTAMPTZ DEFAULT now() NOT NULL
);
 
-- RLS precisa estar habilitado, senão as policies não fazem nada
ALTER TABLE projects ENABLE ROW LEVEL SECURITY;
 
-- Cada usuário só lê seus próprios projetos
-- auth.uid() extrai o user_id do JWT que o PostgREST recebe
CREATE POLICY "users_read_own_projects" ON projects
  FOR SELECT USING (auth.uid() = user_id);
 
-- Inserção força o user_id a ser o do token, impedindo impersonação
CREATE POLICY "users_insert_own_projects" ON projects
  FOR INSERT WITH CHECK (auth.uid() = user_id);
 
-- Update e delete seguem a mesma lógica
CREATE POLICY "users_update_own_projects" ON projects
  FOR UPDATE USING (auth.uid() = user_id);
 
CREATE POLICY "users_delete_own_projects" ON projects
  FOR DELETE USING (auth.uid() = user_id);

Se você precisa de migrations versionadas e seguras, o Supabase CLI oferece supabase db diff e supabase migration new para gerar arquivos SQL versionados. Para cenários mais complexos de evolução de schema, vale conferir as estratégias de zero-downtime migrations.

Queries tipadas com geração automática

O Supabase CLI gera tipos TypeScript a partir do schema do banco. Sem isso, toda query retorna any e você perde a principal vantagem de usar TypeScript.

Bash
npx supabase gen types typescript --project-id seuprojetoid > src/lib/supabase/database.types.ts

Use os tipos gerados ao criar os clientes:

TYPESCRIPT
// src/lib/supabase/server.ts (versão tipada)
import { createServerClient } from "@supabase/ssr";
import { cookies } from "next/headers";
import type { Database } from "./database.types";
 
export async function createSupabaseServer() {
  const cookieStore = await cookies();
 
  return createServerClient<Database>(
    process.env.NEXT_PUBLIC_SUPABASE_URL!,
    process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!,
    {
      cookies: {
        getAll() {
          return cookieStore.getAll();
        },
        setAll(cookiesToSet) {
          try {
            cookiesToSet.forEach(({ name, value, options }) =>
              cookieStore.set(name, value, options)
            );
          } catch {}
        },
      },
    }
  );
}

Com os tipos no lugar, o autocomplete funciona em toda a cadeia: supabase.from("projects").select("id, name") retorna o tipo correto sem cast manual.

CRUD em Server Components e Server Actions

Buscar dados em um Server Component e mutar com Server Actions é o padrão que mantém a lógica no servidor e o client leve:

TYPESCRIPT
// src/app/dashboard/page.tsx
import { createSupabaseServer } from "@/lib/supabase/server";
import { redirect } from "next/navigation";
 
export default async function DashboardPage() {
  const supabase = await createSupabaseServer();
  const { data: { user } } = await supabase.auth.getUser();
 
  if (!user) redirect("/login");
 
  const { data: projects, error } = await supabase
    .from("projects")
    .select("id, name, description, created_at")
    .order("created_at", { ascending: false });
 
  if (error) {
    // Em produção, log estruturado + fallback UI
    console.error("Falha ao buscar projetos:", error.message);
    return <p>Erro ao carregar projetos.</p>;
  }
 
  return (
    <section>
      <h1>Seus projetos</h1>
      <ul>
        {projects.map((project) => (
          <li key={project.id}>
            <strong>{project.name}</strong>
            <p>{project.description}</p>
          </li>
        ))}
      </ul>
    </section>
  );
}

A query acima não precisa de cláusula WHERE user_id = ... porque a política RLS já filtra. O PostgREST injeta o auth.uid() automaticamente.

O que NÃO fazer

Erro 1: Usar getSession() no servidor para validar autenticação

TYPESCRIPT
// ERRADO: getSession() confia no token local sem validar no servidor
const { data: { session } } = await supabase.auth.getSession();
if (session) {
  // Um atacante pode forjar o JWT no cookie
  // e getSession() vai aceitar sem reclamar
}
TYPESCRIPT
// CORRETO: getUser() faz uma request ao servidor Supabase
// e valida o JWT contra a chave secreta do projeto
const { data: { user } } = await supabase.auth.getUser();
if (user) {
  // Token validado no servidor, seguro para tomar decisões de autorização
}

A documentação oficial do Supabase marca getSession() como inseguro para server-side desde a versão 2.x do @supabase/ssr.

Erro 2: Desabilitar RLS "para testar" e esquecer de reabilitar

SQL
-- ERRADO: tabela aberta para qualquer request com a anon key
ALTER TABLE projects DISABLE ROW LEVEL SECURITY;
-- "Depois eu habilito" — famosas últimas palavras
SQL
-- CORRETO: habilite RLS antes de criar qualquer dado
ALTER TABLE projects ENABLE ROW LEVEL SECURITY;
-- Crie policies específicas por operação (SELECT, INSERT, UPDATE, DELETE)
-- Uma policy genérica FOR ALL é mais difícil de auditar

Erro 3: Criar um único client Supabase e reutilizar entre requests

TYPESCRIPT
// ERRADO: singleton compartilhado entre requests no servidor
// cada request tem cookies diferentes, então o user é diferente
const supabase = createServerClient(/* ... */);
 
export function getSharedClient() {
  return supabase; // Vai retornar o user da primeira request
}

Crie um client novo por request. O overhead de instanciação é desprezível comparado ao risco de vazar sessão entre usuários.

Supabase vs. alternativas para Next.js

CritérioSupabaseFirebasePrisma + Auth separado
Banco de dadosPostgreSQL real, SQL completo, busca full-text com tsvectorFirestore (NoSQL, sem joins)Qualquer banco relacional
Auth integradoSim, com JWT e RLSSim, Firebase AuthNão, precisa de Clerk/NextAuth/Auth0
StorageIntegrado com policiesFirebase StorageS3 ou similar, separado
RealtimeWebSocket nativo via Postgres changesFirestore listenersPrecisa de setup manual com WebSockets
Vendor lock-inBaixo (PostgreSQL padrão, self-host possível)Alto (APIs proprietárias)Nenhum
Lógica no backendEdge Functions (Deno) ou API Routes do Next.jsCloud FunctionsControle total
Custo (tier gratuito)500MB banco, 1GB storage, 50k auth users1GB Firestore, 5GB storageDepende do provider

Se o projeto precisa de queries SQL complexas, joins, constraints e migrations versionadas, Supabase ou Prisma são as opções viáveis. Firebase força modelagem NoSQL que complica relatórios e queries ad hoc.

Storage com políticas de acesso

O storage do Supabase segue a mesma lógica de RLS: buckets com policies SQL.

SQL
-- Criar bucket para avatares dos usuários
INSERT INTO storage.buckets (id, name, public)
VALUES ('avatars', 'avatars', false);
 
-- Cada usuário só acessa seus próprios arquivos
-- O path segue o padrão: user_id/filename
CREATE POLICY "users_manage_own_avatars" ON storage.objects
  FOR ALL USING (
    bucket_id = 'avatars'
    AND auth.uid()::text = (storage.foldername(name))[1]
  );

Upload a partir de um Server Action:

TYPESCRIPT
// src/app/profile/actions.ts
"use server";
 
import { createSupabaseServer } from "@/lib/supabase/server";
 
export async function uploadAvatar(formData: FormData) {
  const supabase = await createSupabaseServer();
  const { data: { user } } = await supabase.auth.getUser();
 
  if (!user) return { error: "Não autenticado" };
 
  const file = formData.get("avatar") as File;
  if (!file || file.size === 0) return { error: "Nenhum arquivo enviado" };
 
  // Prefixar com user.id garante que a policy de storage autorize o upload
  const filePath = `${user.id}/${Date.now()}-${file.name}`;
 
  const { error } = await supabase.storage
    .from("avatars")
    .upload(filePath, file, {
      upsert: true, // Sobrescreve se já existir
      contentType: file.type,
    });
 
  if (error) return { error: error.message };
 
  return { path: filePath };
}

Checklist de segurança antes do deploy

  1. RLS habilitado em todas as tabelas que a anon key acessa.
  2. Policies testadas com supabase test ou queries manuais usando roles diferentes.
  3. getUser() (não getSession()) em todo código server-side que toma decisão de autorização.
  4. Middleware do Next.js renovando tokens antes dos Server Components.
  5. SUPABASE_SERVICE_ROLE_KEY nunca exposta no client (sem prefixo NEXT_PUBLIC_).
  6. Buckets de storage com policies explícitas, nunca públicos por padrão.
  7. Rate limiting configurado no API Gateway ou nas Edge Functions para endpoints sensíveis.

FAQ

O Supabase é gratuito para projetos em produção? O tier gratuito suporta até 500MB de banco, 1GB de storage e 50 mil usuários autenticados. Para a maioria dos MVPs e projetos com até alguns milhares de usuários ativos, isso basta. Acima disso, o plano Pro começa em 25 dólares/mês. Se o custo preocupa, o Supabase é self-hostável com Docker, mas você assume a operação do PostgreSQL, GoTrue e Storage.

Preciso do Prisma se já uso o Supabase? Depende de onde a lógica de dados vive. Se você faz queries exclusivamente pelo client do Supabase (via PostgREST), o Prisma é redundante. Se parte da lógica roda em API Routes do Next.js e você prefere um ORM com migrations tipadas, Prisma e Supabase coexistem: Prisma acessa o banco diretamente via connection string, e o Supabase cuida de auth e realtime.

Como funciona o realtime do Supabase? O Supabase escuta mudanças no PostgreSQL via replication slots e publica via WebSocket. Você se inscreve em INSERT, UPDATE ou DELETE de tabelas específicas. As policies de RLS se aplicam ao realtime: um usuário só recebe eventos de linhas que ele tem permissão de ler. Para cenários mais complexos de notificações, a implementação manual com WebSockets oferece mais controle.

Posso usar o Supabase com Pages Router em vez de App Router? Sim. O @supabase/ssr funciona com ambos. A diferença é que no Pages Router você usa getServerSideProps para criar o client de servidor, e no App Router usa Server Components e Server Actions. O middleware funciona igual nos dois.

O RLS impacta performance? As policies RLS são executadas como predicados SQL adicionais em cada query. Para tabelas com índice no user_id (que é o caso mais comum), o impacto é negligível: o planner do PostgreSQL usa o índice normalmente. O problema aparece quando a policy faz subquery em tabelas sem índice ou usa funções custosas. Mantenha as policies simples e indexe as colunas referenciadas.

Posição editorial

O Supabase é a escolha mais produtiva para projetos Next.js que precisam de auth, banco relacional e storage sem montar infraestrutura própria. A combinação de PostgreSQL real com RLS resolve o problema de segurança de dados de um jeito que o Firebase nunca conseguiu: com SQL, constraints e políticas auditáveis.

O risco está em tratar o Supabase como caixa preta. Se você não entende RLS, não entende onde está a segurança da sua aplicação. Se não gera tipos com o CLI, perde type safety. Se não separa os clientes de server e browser, vaza sessão entre usuários. A ferramenta é boa, mas exige que você saiba PostgreSQL. Se o time não tem essa competência, o Supabase vai parecer mágico até o primeiro incidente de dados expostos por uma tabela sem policy.

Marcos Soares

Escrito por

Marcos Soares

Fullstack Developer · CEO da Agência Poti

Fullstack Developer e CEO da Agência Poti. Mais de 20 anos construindo arquiteturas cloud-native com React, Next.js e sistemas distribuídos. Parceiro comercial do estúdio iellou design. Fundador do Vivo de Código.

Comentários

Participe da discussão

Seja o primeiro a comentar!

Continue Aprofundando

Guias de integração relacionados

Conteúdo técnico toda semana

Receba artigos sobre arquitetura, padrões de projeto e engenharia de software. Direto no seu e-mail, sem enrolação.

Sem spam. Cancele a qualquer momento com 1 clique.