Use Cases (Atualizado: 01/06/2026)

Supabase com Claude Code: Auth, RLS, Storage e Edge Functions na prática

Implemente Supabase com Claude Code com segurança: Auth, RLS, Storage, Edge Functions, migrações, testes e revisão.

Supabase com Claude Code: Auth, RLS, Storage e Edge Functions na prática

Supabase acelera muito um protótipo, mas também facilita erros quando Claude Code recebe uma tarefa vaga. Uma tela de login pode funcionar mesmo sem Row Level Security, policies de Storage, migrações bem controladas ou sessão correta no servidor.

Supabase é um BaaS, Backend as a Service. Ele reúne Postgres, Auth, Storage, Edge Functions e APIs geradas. Para iniciantes, isso reduz infraestrutura. Para produção, o ponto mais importante é aplicar autorização no Postgres com RLS, em vez de confiar apenas em checks na interface.

Este guia usa Next.js App Router e TypeScript. Ele mostra um fluxo prático para Claude Code: arquivo de requisitos, revisão de schema/RLS, comandos de migration e geração de tipos, blocos de código copiáveis, testes, armadilhas e checklist final. Veja também implementação de autenticação, design de banco de dados e migração de banco de dados.

Referências oficiais

Use a documentação oficial como base: Supabase Docs, Auth, Row Level Security, Edge Functions e Storage.

Em projetos Next.js novos, prefira @supabase/ssr para Auth com cookies e NEXT_PUBLIC_SUPABASE_PUBLISHABLE_KEY para o acesso seguro no navegador. Projetos antigos podem usar anon keys, mas novos exemplos devem apontar para publishable keys e policies explícitas.

Arquitetura

O exemplo é uma funcionalidade de notas: o usuário autenticado cria notas, faz upload de anexo privado e chama uma Edge Function para uma tarefa de notificação.

flowchart LR
  User["Browser"] --> Next["Next.js App Router"]
  Next --> SSR["@supabase/ssr client"]
  SSR --> Auth["Supabase Auth"]
  SSR --> DB["Postgres tables"]
  SSR --> Storage["Storage bucket"]
  Next --> Fn["Edge Function"]
  Fn --> DB
  DB --> RLS["RLS policies"]
  Storage --> StorageRLS["storage.objects policies"]

A fronteira de segurança fica no Postgres e nas Storage policies. A UI ajuda a experiência, mas não deve ser a única camada de autorização.

Arquivo de requisitos para Claude Code

# docs/supabase-notes-requirements.md

## Goal
Build a Supabase-backed project notes feature in Next.js App Router.

## Stack
- Next.js App Router
- TypeScript
- @supabase/supabase-js
- @supabase/ssr
- Supabase Auth, Postgres, Storage, Edge Functions

## Data model
- project_notes table
- Each note belongs to auth.users.id through owner_id
- Public notes are readable by anyone
- Private notes are readable only by the owner
- Owners can insert, update, and delete only their own notes

## Security rules
- Never expose a secret key or service role key in browser code
- Use NEXT_PUBLIC_SUPABASE_PUBLISHABLE_KEY for browser and SSR clients
- Enable RLS on every public table
- Use explicit TO anon or TO authenticated in every policy
- Storage uploads must be restricted to a user-owned folder

## Claude Code workflow
1. Create SQL migration first.
2. Review RLS policies before writing UI.
3. Generate TypeScript database types.
4. Implement Supabase clients.
5. Implement server actions and upload helper.
6. Add test or manual verification commands.
7. Return a review checklist with file paths.

Peça primeiro apenas a SQL migration. Nas anotações de Masa, começar pela UI aumentou a chance de aceitar owner_id vindo do formulário.

Instalação e ambiente

npm install @supabase/supabase-js @supabase/ssr zod
npm install --save-dev supabase vitest
npx supabase init
npx supabase start
NEXT_PUBLIC_SUPABASE_URL=https://your-project-ref.supabase.co
NEXT_PUBLIC_SUPABASE_PUBLISHABLE_KEY=sb_publishable_xxxxxxxxxxxxxxxxxxxx

Não cole valores reais em prompts. Secret keys e service role keys ficam no servidor e precisam de justificativa quando contornam RLS.

Migration e RLS

-- supabase/migrations/202606010001_create_project_notes.sql
create table if not exists public.project_notes (
  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 120),
  body text not null default '',
  visibility text not null default 'private'
    check (visibility in ('private', 'public')),
  attachment_path text,
  created_at timestamptz not null default now(),
  updated_at timestamptz not null default now()
);

create index if not exists project_notes_owner_created_idx
  on public.project_notes (owner_id, created_at desc);

alter table public.project_notes enable row level security;

create policy "Anyone can read public notes"
on public.project_notes
for select
to anon, authenticated
using (
  visibility = 'public'
  or (select auth.uid()) = owner_id
);

create policy "Owners can insert notes"
on public.project_notes
for insert
to authenticated
with check ((select auth.uid()) = owner_id);

create policy "Owners can update notes"
on public.project_notes
for update
to authenticated
using ((select auth.uid()) = owner_id)
with check ((select auth.uid()) = owner_id);

create policy "Owners can delete notes"
on public.project_notes
for delete
to authenticated
using ((select auth.uid()) = owner_id);

Para Storage, o bucket privado usa o primeiro diretório como ID do usuário.

insert into storage.buckets (id, name, public, file_size_limit, allowed_mime_types)
values (
  'note-attachments',
  'note-attachments',
  false,
  5242880,
  array['image/png', 'image/jpeg', 'application/pdf']
)
on conflict (id) do update
set public = excluded.public,
    file_size_limit = excluded.file_size_limit,
    allowed_mime_types = excluded.allowed_mime_types;

create policy "Users can upload own note attachments"
on storage.objects
for insert
to authenticated
with check (
  bucket_id = 'note-attachments'
  and (select auth.uid())::text = (storage.foldername(name))[1]
);

create policy "Users can read own note attachments"
on storage.objects
for select
to authenticated
using (
  bucket_id = 'note-attachments'
  and (select auth.uid())::text = (storage.foldername(name))[1]
);
npx supabase db reset
npx supabase gen types typescript --local > src/lib/database.types.ts
npm run typecheck

Clientes Supabase

// src/lib/supabase/client.ts
import { createBrowserClient } from "@supabase/ssr";
import type { Database } from "@/lib/database.types";

export function createClient() {
  return createBrowserClient<Database>(
    process.env.NEXT_PUBLIC_SUPABASE_URL!,
    process.env.NEXT_PUBLIC_SUPABASE_PUBLISHABLE_KEY!,
  );
}
// src/lib/supabase/server.ts
import { createServerClient } from "@supabase/ssr";
import { cookies } from "next/headers";
import type { Database } from "@/lib/database.types";

export async function createClient() {
  const cookieStore = await cookies();

  return createServerClient<Database>(
    process.env.NEXT_PUBLIC_SUPABASE_URL!,
    process.env.NEXT_PUBLIC_SUPABASE_PUBLISHABLE_KEY!,
    {
      cookies: {
        getAll() {
          return cookieStore.getAll();
        },
        setAll(cookiesToSet) {
          try {
            cookiesToSet.forEach(({ name, value, options }) => {
              cookieStore.set(name, value, options);
            });
          } catch {
            // Server Components cannot set cookies directly.
          }
        },
      },
    },
  );
}

Auth, CRUD e upload

// app/login/actions.ts
"use server";

import { redirect } from "next/navigation";
import { createClient } from "@/lib/supabase/server";

export async function signIn(formData: FormData) {
  const email = String(formData.get("email") ?? "");
  const password = String(formData.get("password") ?? "");
  const supabase = await createClient();

  const { error } = await supabase.auth.signInWithPassword({ email, password });

  if (error) return { error: error.message };
  redirect("/dashboard");
}
// src/features/notes/actions.ts
"use server";

import { revalidatePath } from "next/cache";
import { createClient } from "@/lib/supabase/server";

type CreateNoteInput = {
  title: string;
  body: string;
  visibility?: "private" | "public";
  attachmentPath?: string | null;
};

export async function createNote(input: CreateNoteInput) {
  const supabase = await createClient();
  const {
    data: { user },
    error: userError,
  } = await supabase.auth.getUser();

  if (userError || !user) throw new Error("Authentication required");

  const { data, error } = await supabase
    .from("project_notes")
    .insert({
      owner_id: user.id,
      title: input.title,
      body: input.body,
      visibility: input.visibility ?? "private",
      attachment_path: input.attachmentPath ?? null,
    })
    .select("id,title,visibility")
    .single();

  if (error) throw error;
  revalidatePath("/dashboard");
  return data;
}
// src/features/notes/upload-note-attachment.ts
"use client";

import { createClient } from "@/lib/supabase/client";

export async function uploadNoteAttachment(file: File, userId: string) {
  const supabase = createClient();
  const ext = file.name.split(".").pop()?.toLowerCase() ?? "bin";
  const path = `${userId}/${crypto.randomUUID()}.${ext}`;

  const { error } = await supabase.storage
    .from("note-attachments")
    .upload(path, file, {
      cacheControl: "3600",
      upsert: false,
      contentType: file.type,
    });

  if (error) throw error;
  return path;
}

Edge Function

npx supabase functions new notify-note-created
// supabase/functions/notify-note-created/index.ts
import { createClient } from "npm:@supabase/supabase-js@2";

Deno.serve(async (req) => {
  const authorization = req.headers.get("Authorization");
  if (!authorization) {
    return Response.json({ error: "Missing authorization" }, { status: 401 });
  }

  const supabase = createClient(
    Deno.env.get("SUPABASE_URL")!,
    Deno.env.get("SUPABASE_ANON_KEY")!,
    { global: { headers: { Authorization: authorization } } },
  );

  const {
    data: { user },
    error: userError,
  } = await supabase.auth.getUser();

  if (userError || !user) {
    return Response.json({ error: "Authentication required" }, { status: 401 });
  }

  return Response.json({ ok: true, userId: user.id });
});
npx supabase functions serve notify-note-created --env-file .env.local
npx supabase functions deploy notify-note-created

Três casos de uso

CasoRecursos SupabaseTarefa do Claude CodeRevisão humana
Notas de equipe SaaSAuth, Postgres, RLSTabelas, Server Actions, UILimite de time e dono
Conteúdo para membrosAuth, Storage, signed URLsUpload, download, auditoriaBucket privado e caminhos
Reservas de eventoPostgres, Edge FunctionsSchema, notificação, cancelamentoReserva dupla e retries

Armadilhas comuns

Ativar RLS sem policies bloqueia tudo. Usar using (true) de forma ampla demais expõe linhas. Teste anônimo, usuário logado e outro usuário.

Secret key no cliente é falha crítica. O navegador deve ver apenas publishable key.

Caminhos de Storage não podem ser livres. Código e policy precisam seguir o mesmo formato, como userId/random-file.

Depois de migration, gere novamente database.types.ts; senão Claude Code continuará escrevendo para schema antigo.

Edge Functions são endpoints HTTP públicos. Valide Authorization e use client que respeita RLS, salvo justificativa documentada.

Prompt de revisão

Review only the Supabase integration.
Check RLS, explicit TO roles, publishable key usage, no secret key in client code,
owner_id derived from authenticated user, Storage paths matching policies,
generated types in sync, and Edge Functions validating Authorization.
Return findings with file paths and line numbers.

Antes de publicar, confirme leitura pública anônima, leitura privada autenticada, falha ao atualizar nota de outro usuário, falha ao subir arquivo em pasta alheia e falha ao chamar Edge Function sem autenticação.

ClaudeCodeLab pode revisar integrações Supabase + Claude Code, estruturar RLS, migrations, regras CLAUDE.md e treinamento de equipe. Para adoção em equipe, use a página Claude Code training and consultation.

Ao testar este fluxo, o maior ganho foi revisar a migration RLS antes da UI. Nas anotações de Masa, a abordagem migration-first gerou diffs menores e um checklist de release mais claro.

#Claude Code #Supabase #BaaS #PostgreSQL #RLS #Auth
Grátis

PDF grátis: cheatsheet do Claude Code

Informe seu e-mail e baixe uma página com comandos, hábitos de revisão e workflows seguros.

Cuidamos dos seus dados e não enviamos spam.

Masa

Sobre o autor

Masa

Engenheiro focado em workflows práticos com Claude Code.