Supabase: Backend as a Service para Full-Stack Developers
O Supabase revolucionou a forma como desenvolvedores full-stack constroem aplicações. Com autenticação, banco de dados PostgreSQL, storage e realtime integrados, você pode focar no que importa: construir features que seus usuários amam. Neste guia completo, vamos explorar como usar o Supabase para criar aplicações robustas e escaláveis.
Por que Supabase?
O Supabase se posiciona como alternativa open-source ao Firebase, mas com PostgreSQL no core:
Comparação com Alternativas
| Feature | Supabase | Firebase | PlanetScale |
|---|---|---|---|
| Banco de Dados | PostgreSQL | Firestore/RTDB | MySQL |
| Open Source | ✅ Sim | ❌ Não | ⚠️ Parcial |
| Self-hosted | ✅ Sim | ❌ Não | ❌ Não |
| SQL | ✅ Completo | ❌ NoSQL | ✅ Completo |
| Realtime | ✅ Nativo | ✅ Nativo | ❌ Não |
| Auth | ✅ Incluído | ✅ Incluído | ❌ Não |
| Storage | ✅ Incluído | ✅ Incluído | ❌ Não |
| Edge Functions | ✅ Deno | ✅ Node.js | ❌ Não |
"Supabase é o que o Firebase deveria ter sido - SQL de verdade com todas as conveniências de um BaaS." - Desenvolvedor anônimo
Configuração Inicial
Criando o Projeto
# Instalar CLI do Supabase
npm install -g supabase
# Criar novo projeto (ou usar dashboard web)
supabase init
# Linkar com projeto remoto
supabase link --project-ref seu-project-ref
# Instalar dependências no projeto Next.js
npm install @supabase/supabase-js @supabase/ssr
Configuração do Cliente
// lib/supabase/client.ts
import { createBrowserClient } from '@supabase/ssr';
export function createClient() {
return createBrowserClient(
process.env.NEXT_PUBLIC_SUPABASE_URL!,
process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!
);
}
// lib/supabase/server.ts
import { createServerClient, type CookieOptions } from '@supabase/ssr';
import { cookies } from 'next/headers';
export async function createClient() {
const cookieStore = await cookies();
return createServerClient(
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 {
// Ignore em Server Components
}
},
},
}
);
}
Autenticação Completa
Configurando Providers
// app/auth/actions.ts
'use server';
import { createClient } from '@/lib/supabase/server';
import { redirect } from 'next/navigation';
export async function signInWithEmail(formData: FormData) {
const supabase = await createClient();
const email = formData.get('email') as string;
const password = formData.get('password') as string;
const { error } = await supabase.auth.signInWithPassword({
email,
password,
});
if (error) {
return { error: error.message };
}
redirect('/dashboard');
}
export async function signInWithGoogle() {
const supabase = await createClient();
const { data, error } = await supabase.auth.signInWithOAuth({
provider: 'google',
options: {
redirectTo: `${process.env.NEXT_PUBLIC_SITE_URL}/auth/callback`,
},
});
if (error) {
return { error: error.message };
}
redirect(data.url);
}
export async function signOut() {
const supabase = await createClient();
await supabase.auth.signOut();
redirect('/');
}
Callback Route
// app/auth/callback/route.ts
import { createClient } from '@/lib/supabase/server';
import { NextResponse } from 'next/server';
export async function GET(request: Request) {
const { searchParams, origin } = new URL(request.url);
const code = searchParams.get('code');
const next = searchParams.get('next') ?? '/dashboard';
if (code) {
const supabase = await createClient();
const { error } = await supabase.auth.exchangeCodeForSession(code);
if (!error) {
return NextResponse.redirect(`${origin}${next}`);
}
}
return NextResponse.redirect(`${origin}/auth/error`);
}
Middleware de Proteção
// middleware.ts
import { createServerClient, type CookieOptions } from '@supabase/ssr';
import { NextResponse, type NextRequest } from 'next/server';
export async function middleware(request: NextRequest) {
let response = NextResponse.next({
request: {
headers: request.headers,
},
});
const supabase = createServerClient(
process.env.NEXT_PUBLIC_SUPABASE_URL!,
process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!,
{
cookies: {
getAll() {
return request.cookies.getAll();
},
setAll(cookiesToSet) {
cookiesToSet.forEach(({ name, value, options }) =>
response.cookies.set(name, value, options)
);
},
},
}
);
const { data: { user } } = await supabase.auth.getUser();
// Proteger rotas do dashboard
if (request.nextUrl.pathname.startsWith('/dashboard') && !user) {
return NextResponse.redirect(new URL('/login', request.url));
}
// Proteger rotas admin
if (request.nextUrl.pathname.startsWith('/admin')) {
if (!user) {
return NextResponse.redirect(new URL('/login', request.url));
}
// Verificar se é admin
const { data: profile } = await supabase
.from('profiles')
.select('role')
.eq('id', user.id)
.single();
if (profile?.role !== 'admin') {
return NextResponse.redirect(new URL('/dashboard', request.url));
}
}
return response;
}
export const config = {
matcher: ['/dashboard/:path*', '/admin/:path*'],
};
Row Level Security (RLS)
RLS é o coração da segurança no Supabase:
Policies Básicas
-- Habilitar RLS na tabela
ALTER TABLE posts ENABLE ROW LEVEL SECURITY;
-- Policy: usuários podem ver posts publicados
CREATE POLICY "Posts públicos são visíveis para todos"
ON posts FOR SELECT
USING (status = 'published');
-- Policy: usuários podem ver seus próprios posts (qualquer status)
CREATE POLICY "Usuários podem ver seus próprios posts"
ON posts FOR SELECT
USING (auth.uid() = author_id);
-- Policy: usuários podem criar posts
CREATE POLICY "Usuários autenticados podem criar posts"
ON posts FOR INSERT
WITH CHECK (auth.uid() = author_id);
-- Policy: usuários podem editar seus posts
CREATE POLICY "Usuários podem editar seus posts"
ON posts FOR UPDATE
USING (auth.uid() = author_id)
WITH CHECK (auth.uid() = author_id);
-- Policy: usuários podem deletar seus posts
CREATE POLICY "Usuários podem deletar seus posts"
ON posts FOR DELETE
USING (auth.uid() = author_id);
Policies com Roles
-- Função helper para verificar role
CREATE OR REPLACE FUNCTION public.is_admin()
RETURNS boolean AS $$
BEGIN
RETURN EXISTS (
SELECT 1 FROM profiles
WHERE id = auth.uid()
AND role = 'admin'
);
END;
$$ LANGUAGE plpgsql SECURITY DEFINER;
-- Admins podem fazer tudo
CREATE POLICY "Admins têm acesso total"
ON posts FOR ALL
USING (is_admin());
Queries e Mutations
CRUD Básico
// lib/queries/posts.ts
import { createClient } from '@/lib/supabase/server';
export async function getPosts() {
const supabase = await createClient();
const { data, error } = await supabase
.from('posts')
.select(`
id,
title,
slug,
excerpt,
cover_image,
created_at,
author:profiles(name, avatar_url)
`)
.eq('status', 'published')
.order('created_at', { ascending: false });
if (error) throw error;
return data;
}
export async function getPostBySlug(slug: string) {
const supabase = await createClient();
const { data, error } = await supabase
.from('posts')
.select(`
*,
author:profiles(name, avatar_url, bio)
`)
.eq('slug', slug)
.single();
if (error) throw error;
return data;
}
export async function createPost(post: NewPost) {
const supabase = await createClient();
const { data, error } = await supabase
.from('posts')
.insert(post)
.select()
.single();
if (error) throw error;
return data;
}
export async function updatePost(id: string, updates: Partial<Post>) {
const supabase = await createClient();
const { data, error } = await supabase
.from('posts')
.update(updates)
.eq('id', id)
.select()
.single();
if (error) throw error;
return data;
}
Tipagem Automática
# Gerar tipos do banco de dados
npx supabase gen types typescript --local > types/supabase.ts
// types/supabase.ts gerado automaticamente
export type Database = {
public: {
Tables: {
posts: {
Row: {
id: string;
title: string;
slug: string;
content: string;
status: 'draft' | 'published';
author_id: string;
created_at: string;
};
Insert: {
id?: string;
title: string;
slug: string;
content: string;
status?: 'draft' | 'published';
author_id: string;
created_at?: string;
};
Update: {
id?: string;
title?: string;
slug?: string;
content?: string;
status?: 'draft' | 'published';
author_id?: string;
created_at?: string;
};
};
};
};
};
Realtime Subscriptions
// hooks/useRealtimePosts.ts
'use client';
import { useEffect, useState } from 'react';
import { createClient } from '@/lib/supabase/client';
import type { Post } from '@/types';
export function useRealtimePosts(initialPosts: Post[]) {
const [posts, setPosts] = useState(initialPosts);
const supabase = createClient();
useEffect(() => {
const channel = supabase
.channel('posts-changes')
.on(
'postgres_changes',
{
event: '*',
schema: 'public',
table: 'posts',
filter: 'status=eq.published',
},
(payload) => {
if (payload.eventType === 'INSERT') {
setPosts((current) => [payload.new as Post, ...current]);
} else if (payload.eventType === 'UPDATE') {
setPosts((current) =>
current.map((post) =>
post.id === payload.new.id ? (payload.new as Post) : post
)
);
} else if (payload.eventType === 'DELETE') {
setPosts((current) =>
current.filter((post) => post.id !== payload.old.id)
);
}
}
)
.subscribe();
return () => {
supabase.removeChannel(channel);
};
}, [supabase]);
return posts;
}
Storage
Upload de Arquivos
// lib/storage/upload.ts
import { createClient } from '@/lib/supabase/client';
export async function uploadImage(file: File, bucket: string, path: string) {
const supabase = createClient();
const fileExt = file.name.split('.').pop();
const fileName = `${path}/${Date.now()}.${fileExt}`;
const { data, error } = await supabase.storage
.from(bucket)
.upload(fileName, file, {
cacheControl: '3600',
upsert: false,
});
if (error) throw error;
// Retornar URL pública
const { data: { publicUrl } } = supabase.storage
.from(bucket)
.getPublicUrl(data.path);
return publicUrl;
}
// Componente de upload
'use client';
export function ImageUpload({ onUpload }: { onUpload: (url: string) => void }) {
const [uploading, setUploading] = useState(false);
const handleUpload = async (e: React.ChangeEvent<HTMLInputElement>) => {
const file = e.target.files?.[0];
if (!file) return;
setUploading(true);
try {
const url = await uploadImage(file, 'covers', 'posts');
onUpload(url);
} catch (error) {
console.error('Upload failed:', error);
} finally {
setUploading(false);
}
};
return (
<label className="cursor-pointer">
<input
type="file"
accept="image/*"
onChange={handleUpload}
disabled={uploading}
className="hidden"
/>
<div className="border-2 border-dashed rounded-lg p-8 text-center">
{uploading ? 'Enviando...' : 'Clique para fazer upload'}
</div>
</label>
);
}
Edge Functions
// supabase/functions/send-email/index.ts
import { serve } from 'https://deno.land/std@0.168.0/http/server.ts';
import { createClient } from 'https://esm.sh/@supabase/supabase-js@2';
const corsHeaders = {
'Access-Control-Allow-Origin': '*',
'Access-Control-Allow-Headers': 'authorization, x-client-info, apikey, content-type',
};
serve(async (req) => {
if (req.method === 'OPTIONS') {
return new Response('ok', { headers: corsHeaders });
}
try {
const { to, subject, html } = await req.json();
// Enviar email via Resend, SendGrid, etc.
const response = await fetch('https://api.resend.com/emails', {
method: 'POST',
headers: {
'Authorization': `Bearer ${Deno.env.get('RESEND_API_KEY')}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
from: 'noreply@seudominio.com',
to,
subject,
html,
}),
});
const data = await response.json();
return new Response(JSON.stringify(data), {
headers: { ...corsHeaders, 'Content-Type': 'application/json' },
});
} catch (error) {
return new Response(JSON.stringify({ error: error.message }), {
status: 500,
headers: { ...corsHeaders, 'Content-Type': 'application/json' },
});
}
});
Checklist de Projeto
Setup Inicial
- Criar projeto no Supabase
- Configurar variáveis de ambiente
- Instalar @supabase/ssr
- Configurar clientes (browser e server)
Autenticação
- Configurar providers (Email, Google, GitHub)
- Criar páginas de login/registro
- Implementar middleware de proteção
- Configurar MFA (opcional)
Banco de Dados
- Criar tabelas necessárias
- Habilitar RLS em todas as tabelas
- Criar policies adequadas
- Gerar tipos TypeScript
Features
- Implementar CRUD completo
- Configurar Realtime onde necessário
- Setup de Storage para uploads
- Edge Functions para lógica serverless
Conclusão
O Supabase oferece uma experiência de desenvolvimento excepcional, combinando a potência do PostgreSQL com a conveniência de um BaaS moderno. Com RLS, Realtime e Edge Functions, você tem tudo que precisa para construir aplicações seguras e escaláveis.
Recursos Adicionais
Documentação:
Comunidade:
Este guia é atualizado conforme novas features do Supabase são lançadas.

