create database carreira; -- comece aqui

Supabase do zero ao nível expert

Uma apostila completa e orientada ao mercado de trabalho: você aprende o backend inteiro — banco de dados, autenticação, arquivos, tempo real e funções serverless — construindo sobre PostgreSQL, a habilidade mais pedida em vagas de backend e full-stack.

12módulos
60+exemplos de código
36exercícios práticos
3projetos de portfólio
-- módulo 01 · fundamentos

O que é o Supabase (e por que empresas o usam)

select conceito, contexto_de_mercado from modulo_01;
Ao final deste módulo você saberá
  • Explicar o que é um BaaS e onde o Supabase se encaixa numa arquitetura;
  • Comparar Supabase com Firebase e com um backend tradicional;
  • Nomear os 6 produtos da plataforma e o papel de cada um.

1.1 O problema que o Supabase resolve

Toda aplicação séria precisa das mesmas peças: um banco de dados, um sistema de login, um lugar para guardar arquivos, uma forma de reagir a eventos em tempo real e um lugar para rodar código no servidor. Montar isso do zero exige semanas de configuração — servidor, ORM, JWT, refresh tokens, buckets, websockets, deploy…

O Supabase é um BaaS (Backend as a Service): ele entrega todas essas peças prontas, integradas e gerenciadas, em cima de um banco PostgreSQL real e completo. Você cria um projeto e, em minutos, tem uma API REST gerada automaticamente a partir das suas tabelas, autenticação pronta e SDKs para JavaScript, Flutter, Python e outras linguagens.

1.2 Os 6 produtos da plataforma

ProdutoO que fazTecnologia por trás
DatabaseBanco relacional completo, com SQL, extensões, triggers e funçõesPostgreSQL
AuthLogin com e-mail/senha, OAuth (Google, GitHub…), magic link, OTP, MFAGoTrue + JWT
StorageUpload e entrega de arquivos e imagens, com permissõesCompatível com S3
RealtimeEscutar mudanças no banco, canais de broadcast e presençaElixir/Phoenix + WebSockets
Edge FunctionsCódigo serverless em TypeScript rodando perto do usuárioDeno
Vector / IABusca semântica e armazenamento de embeddings para apps de IApgvector

1.3 Supabase vs. Firebase vs. backend próprio

Essa comparação cai direto em entrevistas. O ponto central: o Firebase usa um banco de documentos NoSQL proprietário; o Supabase usa PostgreSQL — aberto, relacional e padrão de mercado.

SupabaseFirebaseBackend próprio
BancoPostgreSQL (relacional, SQL)Firestore (NoSQL, proprietário)Você escolhe
Consultas complexas (joins, agregações)Nativas em SQLLimitadas, exigem modelagem criativaDepende de você
Lock-inBaixo: é Postgres, dá para migrar ou auto-hospedar (open source)AltoNenhum
Velocidade para MVPAltíssimaAltíssimaBaixa
Controle finoAlto (SQL, RLS, extensões)MédioTotal
💼 No mercado

PostgreSQL aparece há anos entre os bancos mais usados e mais amados nas pesquisas anuais da Stack Overflow. Ao aprender Supabase você está, na prática, aprendendo Postgres + SQL + modelagem + segurança de dados — habilidades transferíveis para qualquer vaga de backend, mesmo em empresas que não usam Supabase. É esse o argumento que você deve usar no currículo e na entrevista: não "sei uma ferramenta", e sim "domino Postgres e sei entregar produto rápido com ele".

Quem contrata Supabase hoje: startups em estágio inicial, agências que entregam MVPs, produtos indie/SaaS e times pequenos de produto dentro de empresas maiores.

1.4 Quando NÃO usar Supabase

  • Workloads que não são bem servidos por um banco relacional central (ex.: grafos gigantes, séries temporais massivas) — embora extensões ajudem em muitos casos;
  • Empresas com exigência de infraestrutura 100% interna sem equipe para auto-hospedar;
  • Lógica de negócio extremamente pesada que pediria um backend dedicado — nesse caso, o Supabase ainda pode ser "só o banco + auth", com sua API própria ao lado.

Saber dizer quando não usar uma ferramenta é um dos sinais mais fortes de senioridade em entrevistas.

Exercícios do módulo 1
  1. Escreva, com suas palavras, um parágrafo explicando Supabase para um gerente não técnico.
  2. Liste 3 tipos de aplicação em que o modelo relacional do Postgres vence o Firestore, e justifique.
  3. Pesquise 3 vagas reais (LinkedIn/Gupy) que citem Supabase ou PostgreSQL e anote os requisitos que se repetem — essa lista vai guiar seu estudo.
-- módulo 02 · fundamentos

Primeiros passos: projeto, dashboard e primeiro código

insert into conhecimento (setup, chaves, client) values ('pronto', 'entendidas', 'conectado');
Ao final deste módulo você saberá
  • Criar um projeto e navegar pelo dashboard com segurança;
  • Entender as chaves de API (anon/publishable vs. service_role/secret) e o risco de cada uma;
  • Conectar um app JavaScript ao seu projeto.

2.1 Criando o projeto

  1. Acesse supabase.com e crie uma conta (login com GitHub é o caminho natural para devs).
  2. Crie uma Organization e depois um Project: escolha nome, uma senha forte para o banco (guarde-a!) e a região mais próxima dos seus usuários (ex.: São Paulo / sa-east-1 para o Brasil).
  3. Aguarde ~2 minutos: o Supabase provisiona um Postgres dedicado para você.

2.2 Tour pelo dashboard

  • Table Editor — criar e editar tabelas visualmente, como uma planilha;
  • SQL Editor — rodar SQL direto no banco (será sua casa a partir do módulo 3);
  • Authentication — usuários, provedores de login, templates de e-mail;
  • Storage — buckets de arquivos;
  • Edge Functions — funções serverless;
  • Database — roles, extensões, replicação, backups;
  • Logs & Reports — o que está acontecendo no projeto;
  • Settings → API — URL do projeto e chaves.

2.3 As chaves de API — leia com atenção

Em Settings → API você encontra:

ChaveOnde usarPoder
anon / publishableFrontend (navegador, app mobile). Pode ficar exposta.Só consegue o que as políticas de RLS permitirem
service_role / secretSomente servidor (Edge Functions, backend próprio, scripts)Ignora RLS: acesso total ao banco
⚠ Erro que reprova em entrevista (e vaza dados em produção)

Nunca coloque a chave service_role no frontend, em repositório público ou em variável NEXT_PUBLIC_* / VITE_*. Quem tem essa chave lê e apaga qualquer dado do seu banco. A chave anon pode ser pública somente porque o RLS (módulo 6) limita o que ela pode fazer — sem RLS ativado, a anon também é uma porta aberta.

2.4 Conectando via JavaScript

bash# em um projeto Node/Vite/Next.js
npm install @supabase/supabase-js
javascript// src/lib/supabase.js — crie o client UMA vez e reutilize
import { createClient } from '@supabase/supabase-js'

const supabaseUrl = import.meta.env.VITE_SUPABASE_URL
const supabaseKey = import.meta.env.VITE_SUPABASE_ANON_KEY

export const supabase = createClient(supabaseUrl, supabaseKey)
bash# .env (nunca commite este arquivo — adicione ao .gitignore)
VITE_SUPABASE_URL=https://xxxx.supabase.co
VITE_SUPABASE_ANON_KEY=eyJhbGciOi...

2.5 Ambiente local com a CLI (opcional agora, essencial depois)

Profissionalmente, você não desenvolve direto no projeto de produção. A CLI do Supabase sobe toda a stack localmente via Docker:

bash# instalar a CLI (macOS/Linux via brew; Windows via scoop ou npx)
npm install supabase --save-dev

npx supabase init      # cria a pasta supabase/ no repositório
npx supabase start     # sobe Postgres + Auth + Storage + Studio local
npx supabase status    # mostra URLs e chaves locais

Voltaremos à CLI no módulo 10 (migrations) e no módulo 11 (CI/CD).

💼 No mercado

Fluxo profissional que você deve citar em entrevista: banco local via CLI → migrations versionadas no Git → deploy para staging → produção. Times que usam Supabase valorizam quem já chega sabendo esse fluxo, porque a maioria dos iniciantes edita produção direto pelo dashboard — e isso quebra ambientes.

Exercícios do módulo 2
  1. Crie um projeto chamado apostila-lab na região de São Paulo.
  2. Crie um app Vite (npm create vite@latest), instale o supabase-js e monte o arquivo supabase.js com variáveis de ambiente.
  3. Explique em 3 linhas por que a chave anon pode ir ao navegador e a service_role não.
-- módulo 03 · fundamentos

PostgreSQL essencial: modelando seus dados

create table conhecimento_sql (id bigint, nivel text default 'solido');
Ao final deste módulo você saberá
  • Criar tabelas com tipos, constraints e valores padrão adequados;
  • Modelar relações 1-N e N-N com chaves estrangeiras;
  • Escrever SELECTs com filtros, joins, agregações e ordenação.

3.1 O projeto-guia da apostila

Dos módulos 3 a 9 construiremos o backend de um app real de portfólio: o DevBoard — um gerenciador de tarefas colaborativo (estilo Trello simplificado) com usuários, quadros, tarefas, comentários e anexos. Ele exige exatamente as habilidades que o mercado pede: modelagem relacional, permissões por usuário, arquivos e tempo real.

3.2 Criando tabelas com SQL

Você pode usar o Table Editor visual, mas profissionais escrevem SQL — é versionável, repetível e cai em entrevista. No SQL Editor:

sql-- Quadros (boards) pertencem a um usuário do Auth
create table boards (
  id          uuid primary key default gen_random_uuid(),
  owner_id    uuid not null references auth.users(id) on delete cascade,
  title       text not null check (char_length(title) between 1 and 80),
  created_at  timestamptz not null default now()
);

-- Tarefas pertencem a um quadro (relação 1-N)
create table tasks (
  id          bigint generated always as identity primary key,
  board_id    uuid not null references boards(id) on delete cascade,
  title       text not null,
  status      text not null default 'todo'
              check (status in ('todo', 'doing', 'done')),
  due_date    date,
  created_at  timestamptz not null default now()
);

Tipos que você usará o tempo todo

TipoUso
uuidIDs públicos e não sequenciais (padrão para chaves ligadas a usuários)
bigint generated always as identityIDs numéricos autoincrementais internos
textQualquer texto (no Postgres, prefira a varchar(n) + use check para limites)
timestamptzData e hora com fuso — sempre prefira ao timestamp puro
numeric(10,2)Dinheiro (nunca use float para valores monetários!)
boolean, date, jsonbFlags, datas puras e documentos JSON indexáveis

3.3 Relação N-N: tabela de junção

Um quadro pode ter vários membros; um usuário participa de vários quadros. Isso pede uma tabela intermediária:

sqlcreate table board_members (
  board_id  uuid references boards(id) on delete cascade,
  user_id   uuid references auth.users(id) on delete cascade,
  role      text not null default 'member' check (role in ('member','admin')),
  primary key (board_id, user_id)  -- chave composta impede duplicatas
);

3.4 Consultando: o SELECT que cai em toda entrevista

sql-- Filtro, ordenação e limite
select id, title, status
from tasks
where status = 'doing' and due_date <= current_date + 7
order by due_date asc
limit 20;

-- JOIN: tarefas com o título do quadro
select t.title as tarefa, b.title as quadro
from tasks t
join boards b on b.id = t.board_id;

-- Agregação: quantas tarefas por status em cada quadro
select b.title, t.status, count(*) as total
from tasks t
join boards b on b.id = t.board_id
group by b.title, t.status
order by b.title;

3.5 Alterando e apagando com segurança

sqlupdate tasks set status = 'done' where id = 42;

-- Regra de ouro: TODO update/delete tem WHERE.
-- Dica: rode antes um SELECT com o mesmo WHERE para conferir o alvo.
delete from tasks where status = 'done' and created_at < now() - interval '90 days';
💼 No mercado

Entrevistas de backend quase sempre têm uma etapa de SQL ao vivo: joins, group by e uma pergunta sobre chaves estrangeiras ou normalização. Pratique reescrevendo as consultas deste módulo sem olhar. Saber justificar por que usou uuid vs identity, ou on delete cascade vs restrict, diferencia você de quem só decorou sintaxe.

Exercícios do módulo 3
  1. Crie a tabela comments (id, task_id → tasks, author_id → auth.users, content, created_at) com as constraints adequadas.
  2. Escreva um SELECT que traga os 5 quadros com mais tarefas pendentes (status != 'done').
  3. Explique a diferença entre on delete cascade, set null e restrict — e escolha o mais adequado para comments.author_id.
-- módulo 04 · fundamentos

CRUD com supabase-js: a API automática

select * from api where gerada_automaticamente = true;
Ao final deste módulo você saberá
  • Fazer select, insert, update, delete e upsert pelo client JS;
  • Usar filtros, ordenação, paginação e joins aninhados;
  • Chamar funções do banco via RPC e tratar erros corretamente.

4.1 Como a API nasce das suas tabelas

O Supabase roda o PostgREST, um servidor que transforma o schema do Postgres numa API REST automaticamente. Criou a tabela tasks? Existe um endpoint /rest/v1/tasks. O supabase-js é um client elegante em cima dessa API — cada método vira parâmetros de URL por baixo dos panos.

4.2 Leitura (select)

javascript// Todas as colunas
const { data, error } = await supabase.from('tasks').select('*')

// Colunas específicas + filtro + ordenação + paginação
const { data, error } = await supabase
  .from('tasks')
  .select('id, title, status, due_date')
  .eq('status', 'doing')
  .order('due_date', { ascending: true })
  .range(0, 19)   // paginação: itens 0–19

Filtros mais usados

MétodoSQL equivalente
.eq('status','done') / .neq()= / !=
.gt() .gte() .lt() .lte()> >= < <=
.like('title','%api%') / .ilike()LIKE / ILIKE (i = ignora maiúsculas)
.in('status',['todo','doing'])IN (...)
.is('due_date', null)IS NULL
.or('status.eq.done,due_date.lt.2026-01-01')OR

4.3 Joins aninhados: o superpoder do select

Se existe chave estrangeira, o PostgREST monta o join para você — basta aninhar no select:

javascript// Quadros com suas tarefas e, dentro delas, os comentários
const { data } = await supabase
  .from('boards')
  .select(`
    id, title,
    tasks ( id, title, status,
      comments ( id, content )
    )
  `)
  .eq('tasks.status', 'doing')  // filtro na tabela aninhada

4.4 Escrita: insert, update, upsert, delete

javascript// INSERT — .select() ao final retorna a linha criada
const { data, error } = await supabase
  .from('tasks')
  .insert({ board_id, title: 'Estudar RLS' })
  .select()
  .single()   // espera exatamente 1 linha; erro se 0 ou 2+

// UPDATE — SEMPRE com filtro
await supabase.from('tasks')
  .update({ status: 'done' })
  .eq('id', taskId)

// UPSERT — insere ou atualiza se a chave já existir
await supabase.from('board_members')
  .upsert({ board_id, user_id, role: 'admin' })

// DELETE
await supabase.from('tasks').delete().eq('id', taskId)

4.5 Tratamento de erros — o jeito profissional

O supabase-js não lança exceção em erros de API: ele retorna { data, error }. Ignorar o error é o bug nº 1 de iniciantes.

javascriptasync function createTask(boardId, title) {
  const { data, error } = await supabase
    .from('tasks')
    .insert({ board_id: boardId, title })
    .select()
    .single()

  if (error) {
    // error.code traz o código Postgres: '23505' = violação de unique, etc.
    console.error('Falha ao criar tarefa:', error.message)
    throw new Error('Não foi possível criar a tarefa.')
  }
  return data
}

4.6 RPC: chamando funções do banco

Lógica que envolve várias tabelas ou precisa ser atômica deve virar uma função Postgres (módulo 10) chamada via rpc:

javascriptconst { data, error } = await supabase
  .rpc('mover_tarefa', { task_id: 42, novo_status: 'done' })
TypeScript de graça

Gere tipos do seu schema com npx supabase gen types typescript --project-id XXX > src/database.types.ts e passe-os ao client: createClient<Database>(...). Autocomplete de tabelas e colunas no editor — e um item forte para citar em entrevista.

💼 No mercado

Times avaliam se você sabe paginar (nunca carregar tabelas inteiras), tratar erros e tipar o acesso a dados. Um repositório de portfólio com essas três práticas já se destaca da maioria.

Exercícios do módulo 4
  1. Implemente listarTarefasAtrasadas(boardId): status ≠ done, due_date < hoje, ordenado por vencimento, com paginação de 10.
  2. Implemente busca por texto no título usando ilike.
  3. Escreva um select aninhado que devolva um quadro com membros (via board_members) e tarefas num único request.
-- módulo 05 · intermediário

Autenticação: usuários, sessões e OAuth

select * from auth.users where voce = 'no controle';
Ao final deste módulo você saberá
  • Implementar cadastro, login, logout e recuperação de senha;
  • Adicionar login social (Google/GitHub) e magic link;
  • Entender JWT e sessões, e reagir a mudanças de auth no frontend;
  • Criar a tabela profiles ligada ao usuário — padrão universal.

5.1 Como funciona por baixo

O Supabase Auth emite um JWT (JSON Web Token) quando o usuário loga. Esse token vai em cada request e carrega o user_id (claim sub) e o papel (role). É esse token que o RLS usará no módulo 6 para decidir o que cada usuário pode ver. O client renova a sessão automaticamente com um refresh token.

5.2 E-mail e senha

javascript// Cadastro
const { data, error } = await supabase.auth.signUp({
  email: 'ana@exemplo.com',
  password: 'senha-forte-123',
  options: { data: { display_name: 'Ana' } }  // metadados do usuário
})

// Login
const { data, error } = await supabase.auth.signInWithPassword({
  email: 'ana@exemplo.com',
  password: 'senha-forte-123'
})

// Logout
await supabase.auth.signOut()

// Usuário atual (validado no servidor — prefira a getSession p/ decisões de segurança)
const { data: { user } } = await supabase.auth.getUser()

Por padrão, o Supabase exige confirmação de e-mail (configurável em Authentication → Providers). Os templates dos e-mails são personalizáveis — traduza-os para português antes de mostrar o projeto a alguém.

5.3 Login social (OAuth) e magic link

javascript// OAuth — requer configurar o provedor no dashboard (client id/secret)
await supabase.auth.signInWithOAuth({
  provider: 'github',
  options: { redirectTo: 'https://meuapp.com/callback' }
})

// Magic link — login sem senha, por e-mail
await supabase.auth.signInWithOtp({ email: 'ana@exemplo.com' })

5.4 Reagindo ao estado de auth (React)

javascriptimport { useEffect, useState } from 'react'
import { supabase } from './lib/supabase'

export function useSession() {
  const [session, setSession] = useState(null)

  useEffect(() => {
    supabase.auth.getSession().then(({ data }) => setSession(data.session))

    const { data: { subscription } } =
      supabase.auth.onAuthStateChange((_event, session) => setSession(session))

    return () => subscription.unsubscribe()
  }, [])

  return session
}

5.5 O padrão profiles — memorize este bloco

A tabela auth.users pertence ao sistema; você não deve pendurar colunas nela. O padrão é uma tabela profiles no schema público, criada automaticamente por trigger a cada cadastro:

sqlcreate table profiles (
  id           uuid primary key references auth.users(id) on delete cascade,
  display_name text,
  avatar_url   text,
  created_at   timestamptz default now()
);

-- Trigger: cria o profile junto com o usuário
create function handle_new_user()
returns trigger language plpgsql security definer set search_path = ''
as $$
begin
  insert into public.profiles (id, display_name)
  values (new.id, new.raw_user_meta_data->>'display_name');
  return new;
end;
$$;

create trigger on_auth_user_created
  after insert on auth.users
  for each row execute function handle_new_user();
⚠ Segurança em SSR (Next.js e afins)

Em código de servidor, use supabase.auth.getUser() (que valida o token junto ao servidor de Auth) para decisões de autorização — getSession() lê o token do cookie/armazenamento sem revalidar. Para Next.js, use o pacote @supabase/ssr, que gerencia a sessão via cookies corretamente entre server e client.

💼 No mercado

Perguntas frequentes de entrevista que este módulo responde: "o que é um JWT e o que ele carrega?", "diferença entre autenticação e autorização?" (autenticação = quem é você → este módulo; autorização = o que você pode → módulo 6), "como funciona refresh token?". Implementar MFA/TOTP no seu projeto de portfólio é um diferencial raro entre juniores.

Exercícios do módulo 5
  1. Monte telas de cadastro/login/logout no seu app Vite usando as funções acima.
  2. Configure login com GitHub (crie um OAuth App no GitHub e cole as credenciais no dashboard).
  3. Implemente o padrão profiles + trigger e confirme que um novo cadastro cria a linha automaticamente.
-- módulo 06 · intermediário · o mais importante

Row Level Security: o coração da segurança

alter table todas enable row level security; -- sempre.
Ao final deste módulo você saberá
  • Explicar o que é RLS e por que ele permite expor o banco ao frontend;
  • Escrever políticas de select/insert/update/delete com auth.uid();
  • Aplicar os padrões: dados públicos, dados do dono, dados compartilhados por equipe;
  • Evitar as armadilhas clássicas (recursão, performance, políticas permissivas demais).

6.1 O conceito

Row Level Security é um recurso do próprio Postgres: regras SQL que decidem, linha por linha, quem pode ler ou escrever. Como a regra vive no banco, ela vale para qualquer caminho de acesso — API REST, realtime, SQL direto. É isso que torna seguro usar a chave anon no navegador.

Modelo mental: "toda consulta ganha um WHERE invisível definido por você".

6.2 Ativando e criando a primeira política

sql-- 1. Ativar RLS na tabela (a partir daí, TUDO é negado por padrão)
alter table boards enable row level security;

-- 2. Permitir que o dono veja seus quadros
create policy "donos veem seus quadros"
on boards for select
to authenticated
using ( owner_id = (select auth.uid()) );

Anatomia de uma política:

  • for select|insert|update|delete|all — a operação coberta;
  • to authenticated|anon — a quem se aplica (logados ou anônimos);
  • using (…) — condição para linhas existentes (leitura/alteração/remoção);
  • with check (…) — condição para linhas novas ou alteradas (insert/update);
  • auth.uid() — função do Supabase que devolve o id do usuário logado, extraído do JWT.

6.3 O conjunto completo para "cada um no seu quadrado"

sqlcreate policy "criar quadros próprios"
on boards for insert to authenticated
with check ( owner_id = (select auth.uid()) );

create policy "editar quadros próprios"
on boards for update to authenticated
using ( owner_id = (select auth.uid()) )
with check ( owner_id = (select auth.uid()) );

create policy "apagar quadros próprios"
on boards for delete to authenticated
using ( owner_id = (select auth.uid()) );

6.4 Padrão: acesso por equipe (membership)

Tarefas devem ser visíveis a membros do quadro, não só ao dono. A condição consulta a tabela de junção:

sqlalter table tasks enable row level security;

create policy "membros veem tarefas do quadro"
on tasks for select to authenticated
using (
  exists (
    select 1 from board_members bm
    where bm.board_id = tasks.board_id
      and bm.user_id = (select auth.uid())
  )
);
⚠ Armadilha: recursão infinita

Se a política de board_members consultar board_members, o Postgres entra em recursão e retorna erro. A solução profissional é mover a checagem para uma função security definer (que roda ignorando RLS) e chamá-la na política:

sqlcreate function is_board_member(b_id uuid)
returns boolean language sql security definer set search_path = ''
as $$
  select exists (
    select 1 from public.board_members
    where board_id = b_id and user_id = (select auth.uid())
  );
$$;

6.5 Padrão: conteúdo público para leitura

sql-- perfis são públicos para leitura, editáveis só pelo dono
create policy "perfis são públicos"
on profiles for select to anon, authenticated
using ( true );

create policy "cada um edita o seu"
on profiles for update to authenticated
using ( id = (select auth.uid()) );

6.6 Performance de RLS

  • Envolva auth.uid() em (select auth.uid()) — permite ao Postgres calcular uma vez por consulta, não por linha;
  • Crie índice nas colunas usadas pelas políticas: create index on tasks (board_id); e create index on board_members (user_id, board_id);
  • Prefira exists a in (select …) em condições de membership;
  • Declare to authenticated/to anon sempre — evita avaliar políticas para papéis que nem deveriam tentar.
💼 No mercado

RLS é o tema que separa quem "já mexeu com Supabase" de quem sabe usá-lo em produção. Em entrevistas para vagas com Supabase, espere: "como você faria multi-tenancy?", "o que acontece se esquecer de ativar RLS numa tabela?" (resposta: com a anon key e a tabela exposta, qualquer pessoa lê tudo), "quando usar security definer?". Domine este módulo e você estará à frente da maioria dos candidatos.

Exercícios do módulo 6
  1. Escreva as políticas completas de tasks (insert/update/delete para membros; delete talvez só para admins do quadro).
  2. Teste com dois usuários diferentes: confirme que o usuário B não vê os quadros do usuário A (abra dois navegadores).
  3. Provoque de propósito a recursão em board_members e conserte com is_board_member().
-- módulo 07 · intermediário

Storage: arquivos, imagens e permissões

insert into storage.objects (bucket, arquivo) values ('avatars', 'ana.png');
Ao final deste módulo você saberá
  • Criar buckets públicos e privados e entender a diferença;
  • Fazer upload, download e listar arquivos pelo client;
  • Gerar URLs públicas e URLs assinadas (temporárias);
  • Proteger arquivos com políticas de RLS no Storage.

7.1 Buckets: público vs. privado

  • Bucket público: qualquer pessoa com a URL lê o arquivo. Ideal para avatares, imagens de produto, assets do site.
  • Bucket privado: leitura só via política de RLS ou URL assinada com validade. Ideal para documentos, anexos, comprovantes.

Crie pelo dashboard (Storage → New bucket) ou por SQL. Convenção que usaremos: avatars (público) e attachments (privado).

7.2 Upload e leitura

javascript// Upload — caminho organizado por usuário (importante p/ as políticas!)
const file = inputEl.files[0]
const path = `${user.id}/${crypto.randomUUID()}-${file.name}`

const { error } = await supabase.storage
  .from('avatars')
  .upload(path, file, { upsert: true, contentType: file.type })

// URL pública (bucket público)
const { data } = supabase.storage.from('avatars').getPublicUrl(path)
// data.publicUrl → salve em profiles.avatar_url

// URL assinada (bucket privado) — expira em 1 hora
const { data: signed } = await supabase.storage
  .from('attachments')
  .createSignedUrl(path, 3600)

// Download direto (respeita RLS)
const { data: blob } = await supabase.storage
  .from('attachments')
  .download(path)

7.3 Políticas de Storage

Arquivos são linhas na tabela storage.objects — logo, RLS funciona igual. O truque de mercado: usar a primeira pasta do caminho como id do dono.

sql-- Cada usuário só grava dentro da própria pasta: avatars/{seu-uuid}/...
create policy "upload na própria pasta"
on storage.objects for insert to authenticated
with check (
  bucket_id = 'avatars'
  and (storage.foldername(name))[1] = (select auth.uid()::text)
);

-- Anexos: só membros do quadro leem (reusando a função do módulo 6)
create policy "membros leem anexos do quadro"
on storage.objects for select to authenticated
using (
  bucket_id = 'attachments'
  and is_board_member( ((storage.foldername(name))[1])::uuid )
);

7.4 Transformação de imagens e uploads grandes

  • Em planos pagos, o Supabase redimensiona imagens on-the-fly: getPublicUrl(path, { transform: { width: 200, height: 200 } }) — perfeito para thumbnails;
  • Para arquivos grandes, use upload resumável (TUS) com a lib tus-js-client — retoma de onde parou se a conexão cair;
  • Limite tamanho e tipos de arquivo nas configurações do bucket (ex.: 5 MB, apenas image/*) — validação no cliente é cortesia, no bucket é segurança.
💼 No mercado

Upload de arquivos é tarefa de sprint 1 em quase todo produto. Saber responder "como você impediria um usuário de baixar o contrato de outro?" (bucket privado + política por pasta + URL assinada curta) mostra maturidade. Cite também validação de MIME type e limite de tamanho no servidor — segurança nunca só no front.

Exercícios do módulo 7
  1. Implemente upload de avatar com preview e atualização de profiles.avatar_url.
  2. Crie o bucket privado attachments com upload de anexos por tarefa, organizados por board_id/.
  3. Gere uma URL assinada de 60 segundos e comprove no navegador que ela expira.
-- módulo 08 · intermediário

Realtime: dados vivos na tela

listen mudancas; -- e a interface reage sozinha
Ao final deste módulo você saberá
  • Escutar INSERT/UPDATE/DELETE de tabelas (Postgres Changes);
  • Usar Broadcast para mensagens efêmeras entre clientes;
  • Usar Presence para saber quem está online;
  • Escolher a ferramenta certa para cada caso.

8.1 Os três modos

ModoO que éCasos de uso
Postgres ChangesEventos quando linhas mudam no bancoLista de tarefas que atualiza sozinha, notificações
BroadcastMensagens cliente↔cliente que não passam pelo bancoCursor do colega na tela, "fulano está digitando…"
PresenceEstado sincronizado de quem está no canalBolinhas de "online agora", contagem de espectadores

8.2 Postgres Changes na prática

Primeiro, habilite a publicação da tabela (Database → Publications, ou SQL):

sqlalter publication supabase_realtime add table tasks;
javascriptconst channel = supabase
  .channel('tasks-do-quadro')
  .on('postgres_changes',
    {
      event: '*',               // ou 'INSERT' | 'UPDATE' | 'DELETE'
      schema: 'public',
      table: 'tasks',
      filter: `board_id=eq.${boardId}`
    },
    (payload) => {
      console.log(payload.eventType, payload.new, payload.old)
      // atualize seu estado React/Vue aqui
    }
  )
  .subscribe()

// SEMPRE limpe ao desmontar o componente:
supabase.removeChannel(channel)

O RLS também vale aqui: o usuário só recebe eventos de linhas que suas políticas de select permitem ver.

8.3 Broadcast e Presence

javascriptconst room = supabase.channel(`board:${boardId}`, {
  config: { presence: { key: user.id } }
})

// Broadcast: enviar e receber
room.on('broadcast', { event: 'typing' }, ({ payload }) => {
  mostrarDigitando(payload.name)
})
room.send({ type: 'broadcast', event: 'typing', payload: { name: 'Ana' } })

// Presence: quem está online
room.on('presence', { event: 'sync' }, () => {
  const online = room.presenceState()   // { userId: [meta...] }
})
room.subscribe(async (status) => {
  if (status === 'SUBSCRIBED') await room.track({ name: 'Ana' })
})
Boas práticas de produção

1) Broadcast escala melhor que Postgres Changes para alto volume — para feeds intensos, um padrão comum é um trigger no banco enviando broadcast. 2) Reconcilie estado: ao reconectar, refaça um select para não perder eventos ocorridos offline. 3) Um canal por contexto (por quadro), não um canal global.

💼 No mercado

"Colaborativo em tempo real" é feature de destaque em produto — e a maioria dos devs nunca implementou. Um DevBoard onde duas janelas se atualizam simultaneamente é uma demo curta e devastadoramente eficaz em entrevista. Grave um GIF disso para o README do repositório.

Exercícios do módulo 8
  1. Faça a lista de tarefas atualizar em tempo real entre duas janelas de navegador.
  2. Adicione o indicador "X está digitando…" nos comentários usando broadcast com debounce.
  3. Mostre avatares dos membros online no topo do quadro usando presence.
-- módulo 09 · avançado

Edge Functions: seu código no servidor

deploy function logica_de_negocio to edge;
Ao final deste módulo você saberá
  • Criar, testar localmente e fazer deploy de Edge Functions;
  • Usar secrets e o client admin (service_role) com segurança;
  • Receber webhooks (ex.: Stripe) e reagir a eventos do banco;
  • Decidir o que vai para Edge Function vs. função do banco.

9.1 Quando o frontend não basta

Tudo que envolve segredos (chaves de APIs de terceiros), validação que o usuário não pode burlar ou integrações (pagamento, e-mail, IA) precisa rodar no servidor. Edge Functions são funções TypeScript no runtime Deno, distribuídas globalmente, disparadas por HTTP.

9.2 Criando e rodando localmente

bashnpx supabase functions new enviar-boas-vindas
# cria supabase/functions/enviar-boas-vindas/index.ts

npx supabase functions serve            # roda local com hot reload
npx supabase functions deploy enviar-boas-vindas
npx supabase secrets set RESEND_API_KEY=re_xxx
typescript// supabase/functions/enviar-boas-vindas/index.ts
Deno.serve(async (req) => {
  const { nome, email } = await req.json()

  const resp = 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: 'DevBoard <oi@devboard.app>',
      to: email,
      subject: `Bem-vindo(a), ${nome}!`,
      html: '<p>Sua conta está pronta 🎉</p>'
    })
  })

  return new Response(JSON.stringify({ ok: resp.ok }), {
    headers: { 'Content-Type': 'application/json' }
  })
})

9.3 Identificando o usuário e usando o client admin

typescriptimport { createClient } from 'jsr:@supabase/supabase-js@2'

Deno.serve(async (req) => {
  // 1) Client com o token do usuário → RLS é respeitado
  const userClient = createClient(
    Deno.env.get('SUPABASE_URL')!,
    Deno.env.get('SUPABASE_ANON_KEY')!,
    { global: { headers: { Authorization: req.headers.get('Authorization')! } } }
  )
  const { data: { user } } = await userClient.auth.getUser()
  if (!user) return new Response('Não autorizado', { status: 401 })

  // 2) Client admin → ignora RLS. Use SOMENTE após validar permissões você mesmo.
  const admin = createClient(
    Deno.env.get('SUPABASE_URL')!,
    Deno.env.get('SUPABASE_SERVICE_ROLE_KEY')!
  )
  // ... lógica privilegiada aqui ...
  return new Response('ok')
})

9.4 Webhooks: o exemplo Stripe

Padrão de mercado para SaaS: o Stripe chama sua função quando um pagamento acontece; você atualiza o plano do usuário no banco.

typescriptimport Stripe from 'npm:stripe'
const stripe = new Stripe(Deno.env.get('STRIPE_SECRET_KEY')!)

Deno.serve(async (req) => {
  const sig = req.headers.get('stripe-signature')!
  const body = await req.text()

  // Verificar a assinatura é OBRIGATÓRIO — sem isso, qualquer um forja eventos
  const event = await stripe.webhooks.constructEventAsync(
    body, sig, Deno.env.get('STRIPE_WEBHOOK_SECRET')!
  )

  if (event.type === 'checkout.session.completed') {
    // admin.from('subscriptions').upsert(...)
  }
  return new Response('ok')
})

Deploy com --no-verify-jwt nesse caso específico, pois o Stripe não envia JWT do Supabase — a segurança vem da assinatura do webhook.

9.5 Database Webhooks: o banco chamando funções

Em Database → Webhooks, configure "quando inserir em comments, chame a função notificar-mencao". É o cimento entre eventos do banco e efeitos externos (e-mail, push, Slack).

Edge Function vs. função Postgres: quem faz o quê?

Use função do banco (módulo 10)Use Edge Function
Lógica sobre dados: transações, agregações, validaçõesChamar APIs externas (pagamento, e-mail, IA)
Precisa ser atômica com o dadoPrecisa de segredos de terceiros
Baixíssima latência junto ao dadoProcessamento fora do banco (gerar PDF, redimensionar)
💼 No mercado

Integração com pagamento (Stripe/Mercado Pago) via webhook é possivelmente a habilidade mais monetizável deste curso — freelas de "adicionar assinatura ao meu SaaS" pagam bem e este módulo é exatamente esse serviço. Em entrevista, saiba explicar por que a verificação de assinatura do webhook é inegociável.

Exercícios do módulo 9
  1. Crie uma função resumo-do-quadro que retorna contagens de tarefas por status, autenticando o usuário e respeitando RLS.
  2. Configure um Database Webhook: novo comentário → função que registra num log (comece com console.log).
  3. (Desafio) Integre o modo de testes do Stripe com checkout e webhook de confirmação.
-- módulo 10 · avançado

Postgres avançado: o que separa júnior de pleno

explain analyze select senioridade from voce; -- Index Scan ;)
Ao final deste módulo você saberá
  • Criar índices certos e ler um EXPLAIN ANALYZE;
  • Escrever funções PL/pgSQL, triggers e views;
  • Implementar busca full-text em português e tarefas agendadas;
  • Versionar o banco com migrations pela CLI;
  • Entender connection pooling (Supavisor) e quando ele importa.

10.1 Índices e performance

Sem índice, o Postgres lê a tabela inteira (Seq Scan). Regra prática: indexe colunas de WHERE, JOIN e ORDER BY frequentes — inclusive toda chave estrangeira (o Postgres não as indexa automaticamente!).

sqlcreate index idx_tasks_board on tasks (board_id);
create index idx_tasks_status_due on tasks (status, due_date); -- composto
create index idx_tasks_pendentes on tasks (due_date)
  where status != 'done';  -- índice parcial: menor e mais rápido

-- Medindo: rode antes e depois de criar o índice
explain analyze
select * from tasks where board_id = '...' and status = 'doing';

Na saída, procure: Seq Scan (ruim em tabela grande) vs. Index Scan (bom), e o tempo real de execução. O dashboard também mostra consultas lentas em Database → Query Performance — hábito de quem cuida de produção.

10.2 Funções PL/pgSQL e transações

sql-- Regra de negócio atômica: mover tarefa registrando histórico
create table task_history (
  id bigint generated always as identity primary key,
  task_id bigint references tasks(id) on delete cascade,
  old_status text, new_status text,
  changed_by uuid, changed_at timestamptz default now()
);

create function mover_tarefa(task_id bigint, novo_status text)
returns tasks language plpgsql security invoker
as $$
declare t tasks;
begin
  update tasks set status = novo_status
  where id = task_id returning * into t;

  if not found then raise exception 'Tarefa % não encontrada', task_id;
  end if;

  insert into task_history (task_id, new_status, changed_by)
  values (task_id, novo_status, auth.uid());

  return t;  -- tudo na mesma transação: ou grava os dois, ou nenhum
end;
$$;

security invoker = roda com as permissões de quem chamou (RLS vale). security definer = roda com as permissões do criador (ignora RLS) — use com parcimônia e sempre com set search_path = ''.

10.3 Triggers para automatizar

sql-- updated_at automático — todo projeto profissional tem
alter table tasks add column updated_at timestamptz default now();

create function set_updated_at() returns trigger language plpgsql as $$
begin new.updated_at = now(); return new; end; $$;

create trigger trg_tasks_updated
before update on tasks for each row execute function set_updated_at();

10.4 Views e busca full-text em português

sql-- View: consulta complexa vira "tabela virtual" consumível pela API
create view board_stats with (security_invoker = true) as
select b.id, b.title,
       count(t.id) filter (where t.status != 'done') as pendentes,
       count(t.id) as total
from boards b left join tasks t on t.board_id = b.id
group by b.id;

-- Full-text search com dicionário português + índice GIN
alter table tasks add column fts tsvector
  generated always as (to_tsvector('portuguese', title)) stored;
create index idx_tasks_fts on tasks using gin (fts);
javascript// no client:
await supabase.from('tasks')
  .select().textSearch('fts', 'relatório & urgente', { config: 'portuguese' })

10.5 Extensões e tarefas agendadas

O Postgres é extensível — e o Supabase traz dezenas de extensões prontas (Database → Extensions):

  • pg_cron — agendar SQL: limpar dados antigos toda noite, fechar relatórios;
  • pg_net — fazer requisições HTTP de dentro do banco (chamar Edge Functions);
  • pgvector — embeddings e busca semântica (módulo 11);
  • postgis — dados geográficos ("lojas num raio de 5 km");
  • pg_stat_statements — estatísticas de consultas para otimização.
sqlcreate extension if not exists pg_cron;
select cron.schedule(
  'limpar-tarefas-antigas',
  '0 3 * * *',  -- todo dia às 3h
  $$ delete from tasks where status = 'done' and updated_at < now() - interval '180 days' $$
);

10.6 Migrations: o banco no Git

Mudança de schema sem migration é mudança perdida. O fluxo profissional com a CLI:

bashnpx supabase migration new criar_task_history
# edite o .sql gerado em supabase/migrations/ com o DDL

npx supabase db reset        # recria o banco local aplicando todas as migrations
npx supabase db diff -f ajuste_x   # ou: gere migration a partir de mudanças feitas no Studio local

npx supabase link --project-ref XXXX
npx supabase db push         # aplica migrations pendentes no projeto remoto

10.7 Connection pooling (Supavisor)

Postgres suporta um número limitado de conexões diretas. Ambientes serverless (Vercel, Lambda) abrem conexões em rajadas — por isso o Supabase fornece o Supavisor, um pooler que multiplexa milhares de clientes em poucas conexões reais:

  • Transaction mode (porta 6543) — para apps serverless; a conexão volta ao pool após cada transação (sem prepared statements persistentes);
  • Session mode (porta 5432 via pooler) — comportamento clássico, para servidores de longa duração;
  • A API REST (PostgREST) já gerencia o próprio pool — essa escolha importa quando você conecta direto ao banco (Prisma, Drizzle, scripts).
💼 No mercado

Perguntas clássicas de entrevista pleno/sênior cobertas aqui: "como você investigaria uma query lenta?" (pg_stat_statements → EXPLAIN ANALYZE → índice), "o que é um trigger e um caso de uso?", "por que serverless + Postgres precisa de pooler?". Essas respostas valem para qualquer stack com Postgres, não só Supabase.

Exercícios do módulo 10
  1. Popule tasks com 100 mil linhas (insert ... select generate_series(...)) e compare o EXPLAIN ANALYZE de uma busca por board_id antes e depois do índice.
  2. Converta todo o schema do DevBoard em migrations versionadas e suba num repositório Git.
  3. Crie um trigger que impeça mais de 100 tarefas com status 'todo' por quadro (levante exceção).
-- módulo 11 · muito avançado

Nível expert: IA, multi-tenancy, testes e produção

select * from producao where dorme_tranquilo = true;
Ao final deste módulo você saberá
  • Construir busca semântica e RAG com pgvector;
  • Arquitetar multi-tenancy segura para SaaS;
  • Testar políticas com pgTAP e montar CI/CD com GitHub Actions;
  • Aplicar o checklist de produção: segurança, backups, custos, observabilidade;
  • Avaliar self-hosting.

11.1 pgvector: busca semântica e RAG

Embeddings são vetores numéricos que representam o significado de um texto. Com a extensão pgvector, o Postgres armazena e compara vetores — a base de busca semântica e de RAG (Retrieval-Augmented Generation: buscar trechos relevantes e entregá-los a um LLM como contexto).

sqlcreate extension if not exists vector;

create table documents (
  id bigint generated always as identity primary key,
  content text not null,
  embedding vector(1536)   -- dimensão do modelo de embedding usado
);

-- Índice HNSW: busca aproximada rápida em milhões de vetores
create index on documents using hnsw (embedding vector_cosine_ops);

-- Função de busca por similaridade (quanto menor a distância, mais parecido)
create function match_documents(query_embedding vector(1536), match_count int)
returns table (id bigint, content text, similarity float)
language sql stable as $$
  select id, content, 1 - (embedding <=> query_embedding) as similarity
  from documents
  order by embedding <=> query_embedding
  limit match_count;
$$;
typescript// Edge Function: pipeline RAG resumido
// 1. gerar embedding da pergunta numa API de embeddings
// 2. buscar trechos parecidos
const { data: docs } = await admin.rpc('match_documents', {
  query_embedding: embedding, match_count: 5
})
// 3. montar o prompt com os trechos e chamar o LLM
const contexto = docs.map(d => d.content).join('\n---\n')
💼 No mercado

"Experiência com RAG/busca vetorial" explodiu nos requisitos de vagas desde 2023 e segue em alta. Um projeto "converse com seus documentos" (upload de PDF → chunks → embeddings → chat) usando só Supabase é hoje um dos itens de portfólio com melhor retorno por hora investida.

11.2 Multi-tenancy para SaaS

SaaS B2B = várias empresas (tenants/organizações) no mesmo banco, com isolamento absoluto. O padrão dominante no ecossistema Supabase:

  1. Tabelas organizations e organization_members (org_id, user_id, role);
  2. Toda tabela de dados carrega org_id;
  3. RLS em toda tabela: using (org_id in (select org_id from organization_members where user_id = (select auth.uid()))) — de preferência encapsulado numa função security definer ou lido de um custom claim no JWT (via Auth Hook) para performance;
  4. Índice composto começando por org_id em todas as tabelas grandes;
  5. Testes automatizados provando que o tenant A jamais lê o tenant B (a seguir).

11.3 Testando o banco com pgTAP

Política de RLS sem teste é promessa, não garantia. O framework pgTAP roda testes dentro do banco:

sql-- supabase/tests/rls_boards.test.sql
begin;
select plan(2);

-- simula um usuário autenticado
select tests.create_supabase_user('ana');
select tests.authenticate_as('ana');

insert into boards (owner_id, title)
values (tests.get_supabase_uid('ana'), 'Meu quadro');

select results_eq(
  'select count(*) from boards', array[1::bigint],
  'Ana vê o próprio quadro'
);

select tests.create_supabase_user('beto');
select tests.authenticate_as('beto');
select results_eq(
  'select count(*) from boards', array[0::bigint],
  'Beto NÃO vê o quadro da Ana'
);

select * from finish();
rollback;
bashnpx supabase test db   # roda os testes .sql localmente

(As funções auxiliares tests.* vêm do pacote comunitário supabase_test_helpers — instale como migration de teste.)

11.4 CI/CD com GitHub Actions

yaml# .github/workflows/deploy.yml (esqueleto)
name: Deploy Supabase
on: { push: { branches: [main] } }
jobs:
  test-and-deploy:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: supabase/setup-cli@v1
      - run: supabase start          # stack local no runner
      - run: supabase test db        # pgTAP: RLS testada a cada push
      - run: supabase link --project-ref ${{ secrets.SUPABASE_PROJECT_REF }}
      - run: supabase db push        # migrations → produção
      - run: supabase functions deploy
        env:
          SUPABASE_ACCESS_TOKEN: ${{ secrets.SUPABASE_ACCESS_TOKEN }}

Fluxo completo de gente grande: branch → PR roda testes → merge na main → migrations e functions vão sozinhas para produção. Para times maiores, o recurso de branching do Supabase cria bancos de preview por PR.

11.5 Checklist de produção

ÁreaChecklist
SegurançaRLS em todas as tabelas do schema público; nenhum service_role no client; Security Advisor do dashboard sem alertas; MFA no seu login do Supabase; search_path fixado em funções definer
BackupsBackups diários (automáticos nos planos pagos); PITR (point-in-time recovery) para produtos sérios; teste a restauração ao menos uma vez
PerformanceÍndices para FKs e políticas; Query Performance revisado; pooler correto para serverless; paginação em toda listagem
ObservabilidadeLogs de API/DB/Functions no dashboard; alertas de uso; Sentry (ou similar) no frontend e nas functions
CustosConhecer limites do plano Free (pausa por inatividade!) vs. Pro; monitorar egress (tráfego de saída) e tamanho do banco; imagem otimizada = egress menor
AuthTemplates de e-mail com seu domínio (SMTP próprio); rate limits revisados; redirect URLs restritas

11.6 Self-hosting: quando e como

O Supabase é open source — dá para rodar tudo em servidores seus com Docker Compose. Faz sentido quando há exigência regulatória de dados on-premise ou escala com equipe de infra dedicada. O trade-off: você vira responsável por atualizações, backups, segurança e disponibilidade de ~10 serviços (Postgres, GoTrue, PostgREST, Realtime, Storage, Kong…). Para a imensa maioria dos projetos, a nuvem gerenciada custa menos que o tempo de engenharia do self-host — mas saber que a opção existe é um argumento forte contra lock-in em discussões de arquitetura.

Exercícios do módulo 11
  1. Monte o pipeline "converse com seus documentos": upload de texto → divisão em chunks → embeddings → match_documents → resposta com LLM.
  2. Escreva testes pgTAP para as políticas de tasks (membro vê; não-membro não vê; só admin apaga).
  3. Configure o workflow de CI acima num repositório real e faça um deploy completo via push.
-- módulo 12 · carreira

Mercado de trabalho: transformando estudo em salário

update carreira set status = 'contratado' where portfolio is not null;
Ao final deste módulo você terá
  • Um plano de portfólio com 3 projetos que provam competência;
  • Um roteiro de preparação para entrevistas técnicas;
  • Estratégia de posicionamento: onde procurar e como se apresentar.

12.1 Que vagas este conhecimento abre

Perfil de vagaComo o Supabase entra
Full-stack (React/Next + backend)Você entrega o produto inteiro sozinho — perfil favorito de startups
Backend Postgres/NodeSQL, modelagem, RLS, funções e performance são o núcleo da vaga
Mobile (Flutter/React Native)Supabase é backend popularíssimo no ecossistema mobile
Freelance / agênciaMVPs em semanas: nicho lucrativo e em crescimento
Apps de IApgvector + Edge Functions = infraestrutura de RAG pronta

Dica de busca: além de "Supabase", pesquise vagas por "PostgreSQL", "Postgres", "Next.js", "serverless" — o conhecimento desta apostila cobre grande parte desses requisitos.

12.2 Os 3 projetos de portfólio

Recrutadores não leem 10 repositórios; leem 3 READMEs. Qualidade e narrativa valem mais que quantidade.

Projeto 1 — DevBoard (o desta apostila)

Kanban colaborativo com auth, RLS por equipe, anexos e tempo real. Prova: fundamentos completos + segurança. Diferenciais: GIF do realtime no README, testes pgTAP, CI.

Projeto 2 — SaaS com pagamento

Qualquer micro-SaaS (ex.: gerador de link-na-bio, controle de assinaturas) com planos Free/Pro via Stripe em modo teste, webhook e gating de features por plano. Prova: você sabe gerar receita — a habilidade que empresas mais valorizam.

Projeto 3 — App de IA com RAG

"Converse com seus PDFs" ou busca semântica num acervo. Prova: você está na fronteira atual do mercado.

Anatomia de um README que emprega

1) Uma frase dizendo o que o app faz; 2) GIF/screenshot em ação; 3) link do deploy funcionando (Vercel + Supabase free); 4) diagrama simples do schema; 5) seção "Decisões técnicas" explicando por que (RLS assim, índice assado); 6) como rodar localmente com supabase start. A seção 5 é a que gera conversa na entrevista.

12.3 Preparação para a entrevista técnica

Perguntas recorrentes — e o módulo onde você aprendeu a resposta:

  • "Por que Supabase e não Firebase?" → M1 (relacional, SQL, open source, lock-in);
  • "Explique RLS e um exemplo de política" → M6 (escreva uma de memória!);
  • "Diferença entre anon key e service role?" → M2;
  • "Como modelaria X?" → M3 (pense alto: entidades → relações → constraints);
  • "Query lenta em produção, o que faz?" → M10 (Query Performance → EXPLAIN → índice);
  • "Autenticação vs. autorização?" → M5/M6;
  • "Como isolaria dados de clientes num SaaS?" → M11 (multi-tenancy);
  • "O que é RAG?" → M11.

Treine falando em voz alta — explicar é uma habilidade separada de saber. Grave-se respondendo três dessas perguntas e ouça.

12.4 Posicionamento e prospecção

  • LinkedIn: título com stack ("Desenvolvedor Full-stack · React · Next.js · PostgreSQL/Supabase"), projetos em destaque com links, 1 post curto por projeto contando um problema que você resolveu (o post sobre "como implementei RLS multi-tenant" atrai exatamente quem contrata isso);
  • GitHub: os 3 projetos pinados, commits frequentes, README caprichado;
  • Comunidades: Discord oficial do Supabase, comunidades BR de dev — responder dúvidas de RLS é networking técnico puro;
  • Freelance: ofereça pacotes claros ("MVP com login, pagamentos e painel em 3 semanas") em vez de "faço sites"; a Workana/Upwork têm demanda constante de Supabase + Next.js;
  • Open source: uma PR aceita em lib do ecossistema (mesmo docs) vale linha de currículo.

12.5 Roteiro de estudo sugerido (8 semanas)

SemanasMeta
1–2Módulos 1–4 + DevBoard com CRUD funcionando
3–4Módulos 5–6 + auth completa e RLS testada com 2 usuários
5Módulos 7–8 + anexos e realtime no DevBoard (grave o GIF!)
6Módulos 9–10 + migrations, índices, 1 Edge Function; deploy público
7Módulo 11 + projeto de IA com pgvector
8Módulo 12 + SaaS com Stripe teste, READMEs, LinkedIn atualizado, simulados de entrevista
💼 A regra final

Ninguém é contratado por "ter estudado Supabase" — é contratado por demonstrar que resolve problemas com ele. Cada módulo desta apostila termina em algo demonstrável de propósito. Se você fez os exercícios, seu portfólio já existe: agora é polir, publicar e contar a história.

Exercícios finais
  1. Publique o DevBoard (Vercel + Supabase) e escreva o README no formato acima.
  2. Responda por escrito, sem consultar, as 8 perguntas de entrevista da seção 12.3 — depois confira nos módulos.
  3. Atualize LinkedIn e GitHub e envie a primeira candidatura (ou proposta de freela) esta semana.

Apostila Supabase — do básico ao expert · Material de estudo independente, não afiliado ao Supabase Inc.

Fontes recomendadas para aprofundar: documentação oficial (supabase.com/docs), blog do Supabase e a documentação do PostgreSQL (postgresql.org/docs). APIs e limites de plano evoluem — confira sempre a doc oficial antes de decisões de produção.