Voltar para o blog
Desenvolvimento Web

Supabase: Backend as a Service para Full-Stack Developers

Como construir aplicações full-stack modernas usando Supabase, Auth e Real-time

Foto de Vinicius Mendes
Especialista em IA Aplicada | Full Stack Engineer | UX/UI Designer
22 min de leitura
Imagem de capa do artigo: Supabase: Backend as a Service para Full-Stack Developers

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.

Supabase - Backend completo para aplicações modernas

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

bash
# 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

typescript
// 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!
  );
}
typescript
// 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

typescript
// 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

typescript
// 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

typescript
// 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

sql
-- 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

sql
-- 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

typescript
// 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

bash
# Gerar tipos do banco de dados
npx supabase gen types typescript --local > types/supabase.ts
typescript
// 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

typescript
// 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

typescript
// 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

typescript
// 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.

Tags

  • Supabase
  • PostgreSQL
  • Backend
  • Next.js
  • Auth
  • Realtime
  • BaaS
  • Full Stack

Continue explorando outros conteúdos que podem te interessar