Supabase mit Claude Code: Auth, RLS, Storage und Edge Functions sicher umsetzen
Ein praxisnaher Workflow für Supabase mit Claude Code: Auth, RLS, Storage, Edge Functions, Migrationen, Tests und Review.
Supabase ist schnell eingerichtet, aber gerade deshalb riskant, wenn Claude Code ohne klare Grenzen loslegt. Ein Login-Screen kann funktionieren, obwohl Row Level Security, Storage Policies, Migrationen oder serverseitige Sessions fehlen.
Supabase ist ein BaaS, also Backend as a Service: Postgres, Auth, Storage, Edge Functions und generierte APIs kommen aus einer Plattform. Für Anfänger bedeutet das weniger Backend-Infrastruktur. Für produktive Teams ist wichtiger, dass Berechtigungen mit Postgres und RLS in der Datenbank durchgesetzt werden können.
Dieser Leitfaden nutzt Next.js App Router und TypeScript. Er zeigt einen Claude-Code-Workflow mit Anforderungsdatei, Schema- und RLS-Review, Migrationsbefehlen, Type-Generierung, lauffähigen Codeblöcken, Testbefehlen, Fallstricken und Review-Checkliste. Ergänzend passen Authentication Implementation, Database Design und Database Migration.
Offizielle Quellen
Nutzen Sie als Grundlage die aktuellen Supabase-Dokumente: Supabase Docs, Auth, Row Level Security, Edge Functions und Storage.
Für neue Next.js-Projekte ist @supabase/ssr für cookie-based Auth sinnvoll. Browser-sichere Zugriffe verwenden NEXT_PUBLIC_SUPABASE_PUBLISHABLE_KEY. Ältere anon keys können noch in Projekten vorkommen, sollten aber nicht mehr die Empfehlung für neue Implementierungen sein.
Zielarchitektur
Das Beispiel ist eine Notizfunktion: angemeldete Nutzer erstellen eigene Notizen, laden private Anhänge hoch und rufen eine Edge Function für eine Benachrichtigung auf.
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"]
Die Sicherheitsgrenze liegt in RLS und Storage Policies, nicht in React-Komponenten. UI-Prüfungen verbessern die Bedienung, ersetzen aber keine Datenbankregeln.
Requirements für 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.
Bitten Sie Claude Code zuerst nur um die SQL-Migration. UI und API kommen erst nach dem RLS-Review. In Masas Tests war genau diese Reihenfolge der Unterschied zwischen kleinen Review-Diffs und späteren Sicherheitskorrekturen.
Setup
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
Teilen Sie echte Secrets nicht mit Claude Code. Secret keys oder alte service role keys gehören nicht in NEXT_PUBLIC_ und nicht in Browser-Code.
Migration und 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);
Für private Anhänge wird der erste Ordner im Storage-Pfad an die User-ID gebunden.
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 für Browser und Server
// 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 und 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
Drei Use Cases
| Use Case | Supabase-Funktionen | Claude-Code-Aufgabe | Menschliches Review |
|---|---|---|---|
| SaaS-Teamnotizen | Auth, Postgres, RLS | Tabellen, Server Actions, Listen-UI | Teamgrenze vs. Ownergrenze |
| Mitgliederdateien | Auth, Storage, signed URLs | Upload-UI, Download-Route | Private Buckets und Pfadregeln |
| Eventbuchung | Postgres, Edge Functions | Reservierungsschema, Benachrichtigung | Doppelbuchung, Retry, Storno |
Typische Fehler
RLS nur zu aktivieren reicht nicht. Ohne Policies ist alles blockiert, mit zu breitem using (true) ist zu viel sichtbar.
Secret keys im Client sind ein Produktionsrisiko. Der Browser bekommt publishable keys, erhöhte Rechte bleiben serverseitig.
Storage-Pfade dürfen nicht beliebig aus der UI kommen. Code und Policy müssen dieselbe Form wie userId/random-file erzwingen.
Nach Migrationen müssen die TypeScript-Typen neu generiert werden, sonst arbeitet Claude Code gegen ein altes Schema.
Edge Functions sind öffentliche HTTP-Einstiege. Authorization prüfen und möglichst einen RLS-aware Client verwenden.
Review-Prompt
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.
Vor dem Release sollten anonyme Public Reads, private Reads nach Login, fehlgeschlagene Updates fremder Notizen, fehlgeschlagene Uploads in fremde Ordner und unauthentifizierte Edge-Function-Aufrufe geprüft werden.
ClaudeCodeLab kann Supabase-Implementierungen mit Claude Code reviewen, RLS-Regeln strukturieren, Migrationen absichern und Team-Workflows schulen. Für Teams ist die Claude Code training and consultation page der passende Einstieg.
Im Praxistest war der größte Gewinn, Claude Code zuerst die RLS-Migration prüfen zu lassen. Masa stellte fest, dass UI-first-Ansätze häufiger owner_id aus Formularen übernahmen, während migration-first kleinere Diffs und klarere Release-Checks erzeugte.
Kostenloses PDF: Claude-Code-Cheatsheet
E-Mail eintragen und eine Seite mit Befehlen, Review-Gewohnheiten und sicheren Workflows herunterladen.
Wir schützen Ihre Daten und senden keinen Spam.
Über den Autor
Masa
Engineer für praktische Claude-Code-Workflows und Team-Einführung.
Ähnliche Artikel
Claude Code Workflow von Obsidian zu CLAUDE.md
Obsidian-Arbeitsnotizen in CLAUDE.md-Betriebsnotizen verwandeln und Kontext nicht ständig neu erklären.
Claude Code Revenue CTA Routing: Artikel zu PDF, Gumroad und Beratung führen
Ein Claude-Code-Ablauf, der Leser nach Absicht zu Gratis-PDF, Gumroad oder Beratung führt.
Claude-Code-Team-Handoff-Regeln: Belege, Berechtigungen, Rollback und Umsatzpfade
Ein praktisches Claude-Code-Handoff für Review-Belege, Berechtigungen, Rollback, Gratis-PDF, Gumroad und Beratung.