Supabase avec Claude Code : Auth, RLS, Storage et Edge Functions en production
Implémentez Supabase avec Claude Code : Auth, RLS, Storage, Edge Functions, migrations, tests et checklist de revue.
Supabase permet de construire très vite, mais cette vitesse peut masquer des failles. Si vous demandez simplement à Claude Code d’ajouter une connexion et une base de données, l’interface peut fonctionner alors que Row Level Security, les policies Storage, les migrations ou la gestion de session serveur sont incomplètes.
Supabase est un BaaS, c’est-à-dire Backend as a Service. Il réunit Postgres, Auth, Storage, Edge Functions et des API générées. Pour débuter, cela évite de créer tout le backend. Pour un produit réel, l’intérêt principal est de pouvoir placer les règles d’autorisation dans Postgres avec RLS.
Ce guide utilise Next.js App Router et TypeScript. Il décrit un workflow Claude Code complet : fichier d’exigences, revue du schema et des policies RLS, commandes de migration et de génération de types, blocs de code copiables, tests, pièges fréquents et checklist finale. Pour compléter, consultez aussi l’implémentation de l’authentification, la conception de base de données et les migrations de base de données.
Sources officielles
Gardez les documents officiels comme référence : Supabase Docs, Auth, Row Level Security, Edge Functions et Storage.
Pour un nouveau projet Next.js, utilisez @supabase/ssr pour l’auth basée sur cookies et NEXT_PUBLIC_SUPABASE_PUBLISHABLE_KEY côté navigateur. Les anciens anon keys existent encore dans certains projets, mais les nouvelles implémentations doivent orienter les lecteurs vers les publishable keys et les policies RLS explicites.
Architecture
L’exemple est une fonctionnalité de notes : un utilisateur connecté crée ses propres notes, ajoute éventuellement une pièce jointe privée et déclenche une Edge Function pour une tâche de notification.
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"]
La frontière de sécurité doit rester dans Postgres et dans les Storage policies. Le code React peut aider l’expérience utilisateur, mais il ne doit pas être la seule protection.
Fichier d’exigences pour 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.
Demandez d’abord uniquement la migration SQL. Dans les tests de Masa, les approches qui commençaient par l’UI avaient plus souvent tendance à laisser passer un owner_id provenant du formulaire.
Installation et variables
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
Ne collez jamais les vraies clés dans un prompt. Les secret keys et service role keys doivent rester côté serveur et être documentées si elles contournent RLS.
Migration et 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);
Pour Storage, utilisez un bucket privé et un dossier par utilisateur.
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
Clients 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 et 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
Trois cas d’usage
| Cas | Fonctions Supabase | Travail Claude Code | Revue humaine |
|---|---|---|---|
| Notes d’équipe SaaS | Auth, Postgres, RLS | Tables, Server Actions, UI | Limite équipe vs propriétaire |
| Ressources membres | Auth, Storage, signed URLs | Upload, téléchargement, audit | Bucket privé et chemins |
| Réservations événement | Postgres, Edge Functions | Schéma, notification, annulation | Double réservation, retries |
Pièges fréquents
Activer RLS sans policies bloque tout. Utiliser using (true) trop largement expose trop de lignes. Testez séparément anonyme, utilisateur connecté et autre utilisateur.
Une secret key dans le client est une erreur critique. Le navigateur ne doit voir qu’une publishable key.
Les chemins Storage ne doivent pas être libres. Faites correspondre le code et la policy sur une forme comme userId/random-file.
Après chaque migration, régénérez database.types.ts, sinon Claude Code continuera à écrire contre un ancien schema.
Une Edge Function reste un endpoint HTTP public. Vérifiez Authorization et utilisez un client qui respecte RLS, sauf justification documentée.
Prompt de revue
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.
Avant publication, vérifiez les lectures publiques anonymes, les lectures privées connectées, l’échec de mise à jour d’une note d’un autre utilisateur, l’échec d’upload dans le dossier d’un autre utilisateur et l’échec d’appel Edge Function sans authentification.
ClaudeCodeLab peut auditer une intégration Supabase + Claude Code, structurer RLS, migrations et règles CLAUDE.md, puis former une équipe à ce workflow. Pour un accompagnement d’équipe, consultez la page Claude Code training and consultation.
En pratique, le meilleur résultat vient du review RLS avant l’UI. Dans les notes de Masa, l’approche migration-first produit des diffs plus petits et des vérifications de release plus nettes qu’une approche centrée sur l’écran.
PDF gratuit: cheatsheet Claude Code
Saisissez votre email et téléchargez une page avec commandes, habitudes de review et workflow sûr.
Nous protégeons vos données et n'envoyons pas de spam.
À propos de l'auteur
Masa
Ingénieur spécialisé dans les workflows pratiques avec Claude Code.
Articles liés
Workflow Obsidian vers CLAUDE.md avec Claude Code
Transformer des notes Obsidian en notes CLAUDE.md concises pour reprendre les sessions sans réexpliquer.
Claude Code Revenue CTA Routing : relier articles, PDF, Gumroad et consultation
Un workflow Claude Code pour orienter les lecteurs vers PDF gratuit, Gumroad ou consultation selon l'intention.
Règles de handoff Claude Code en équipe: preuves, permissions, rollback et revenus
Un format concret pour transmettre un travail Claude Code avec preuves, permissions, rollback, PDF gratuit, Gumroad et consultation.