← Zurück zum Blog

Supabase hat sich 2026 als die führende Open-Source-Alternative zu Firebase etabliert. Was einst nur ein Postgres-Wrapper war, ist heute ein vollständiges Backend-Ökosystem: Authentication, Echtzeit-Datenbank, Object Storage, Serverless Edge Functions, Row Level Security und — besonders spannend für KI-Anwendungen — native pgvector-Unterstützung für Embedding-Suche.

Mit Claude Code lässt sich ein Supabase-Backend in Minuten aufsetzen und vollständig konfigurieren. Dieser Guide zeigt dir die wichtigsten Patterns von der Initialisierung bis zur Hybrid-Suche mit AI.

🔐
Authentication OAuth, Magic Link, OTP, SSO
🐘
PostgreSQL Vollständiges SQL, Extensions
Realtime WebSockets, Broadcast, Presence
📦
Storage S3-kompatibel, CDN-ready
🌐
Edge Functions Deno, global deployed
🤖
pgvector AI Embeddings, Hybrid Search

1. Supabase Setup: Client, SSR & TypeScript Types

Der erste Schritt ist die Installation der Supabase-Pakete. Für Server-Side-Rendering (Next.js, SvelteKit, Remix) nutzt du @supabase/ssr, für reine Client-Apps reicht @supabase/supabase-js.

bash — Installation
# Basis-Paket npm install @supabase/supabase-js # Mit SSR-Unterstützung (Next.js / SvelteKit) npm install @supabase/supabase-js @supabase/ssr # Supabase CLI für lokale Entwicklung & Migrations npm install -D supabase

Environment Variables

Lege die Supabase-Credentials in deine .env.local. Die Werte findest du im Supabase Dashboard unter Project Settings → API.

.env.local
NEXT_PUBLIC_SUPABASE_URL=https://xyzabcdef.supabase.co NEXT_PUBLIC_SUPABASE_ANON_KEY=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9... # Service Role Key: NUR server-seitig, niemals im Client! SUPABASE_SERVICE_ROLE_KEY=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...
⚠️ Sicherheit: Der SUPABASE_SERVICE_ROLE_KEY umgeht Row Level Security vollständig. Verwende ihn ausschließlich server-seitig (API Routes, Server Actions, Edge Functions) — niemals im Browser!

Client-Side Supabase Client

lib/supabase/client.ts
import { createBrowserClient } from '@supabase/ssr' import type { Database } from '@/types/supabase' export function createClient() { return createBrowserClient<Database>( process.env.NEXT_PUBLIC_SUPABASE_URL!, process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY! ) }

Server-Side Client (Next.js App Router)

lib/supabase/server.ts
import { createServerClient } from '@supabase/ssr' import { cookies } from 'next/headers' import type { Database } from '@/types/supabase' export async function createClient() { const cookieStore = await cookies() return createServerClient<Database>( process.env.NEXT_PUBLIC_SUPABASE_URL!, process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!, { cookies: { getAll() { return cookieStore.getAll() }, setAll(cookiesToSet) { cookiesToSet.forEach(({ name, value, options }) => cookieStore.set(name, value, options) ) }, }, } ) }

TypeScript Database Types generieren

Claude Code kann automatisch typisierte Interfaces aus deinem Supabase-Schema generieren. Das gibt dir vollständige Autovervollständigung für alle Tabellen, Views und RPC-Funktionen:

bash — Types generieren
# Einmalig einrichten npx supabase login npx supabase link --project-ref xyzabcdef # Types generieren (in types/supabase.ts) npx supabase gen types typescript --linked > types/supabase.ts # Oder direkt via API ohne CLI npx supabase gen types typescript \ --project-id xyzabcdef \ --schema public > types/supabase.ts
Tipp

Füge den Codegen-Befehl als npm run types in deine package.json ein und führe ihn nach jeder Migration aus. Claude Code kann diesen Schritt automatisch in deine CI/CD-Pipeline integrieren.

2. Authentication: OAuth, Magic Links & SSR Sessions

Auth

Supabase Auth unterstützt E-Mail/Passwort, Magic Links, OTP per SMS, OAuth (Google, GitHub, Twitter, Discord, u.v.m.) sowie SAML SSO für Enterprise-Kunden. Die gesamte Session-Verwaltung läuft über JWTs, die automatisch refresht werden.

E-Mail-Registrierung und Login

lib/auth/actions.ts (Server Action)
'use server' import { createClient } from '@/lib/supabase/server' import { redirect } from 'next/navigation' export async function signUp(formData: FormData) { const supabase = await createClient() const { error } = await supabase.auth.signUp({ email: formData.get('email') as string, password: formData.get('password') as string, options: { emailRedirectTo: `${process.env.NEXT_PUBLIC_SITE_URL}/auth/callback`, data: { full_name: formData.get('name') as string, } } }) if (error) throw error redirect('/auth/check-email') } export async function signIn(formData: FormData) { const supabase = await createClient() const { error } = await supabase.auth.signInWithPassword({ email: formData.get('email') as string, password: formData.get('password') as string, }) if (error) throw error redirect('/dashboard') }

OAuth-Login (Google, GitHub, Discord)

components/OAuthButtons.tsx
'use client' import { createClient } from '@/lib/supabase/client' export function OAuthButtons() { const supabase = createClient() const signInWithGoogle = async () => { await supabase.auth.signInWithOAuth({ provider: 'google', options: { redirectTo: `${window.location.origin}/auth/callback`, queryParams: { access_type: 'offline', prompt: 'consent', } } }) } const signInWithGitHub = async () => { await supabase.auth.signInWithOAuth({ provider: 'github', options: { redirectTo: `${window.location.origin}/auth/callback`, scopes: 'read:user user:email' } }) } return ( <div className="oauth-buttons"> <button onClick={signInWithGoogle}>Mit Google anmelden</button> <button onClick={signInWithGitHub}>Mit GitHub anmelden</button> </div> ) }

Auth State Changes abonnieren

hooks/useAuth.ts
import { useEffect, useState } from 'react' import { createClient } from '@/lib/supabase/client' import type { User } from '@supabase/supabase-js' export function useAuth() { const [user, setUser] = useState<User | null>(null) const [loading, setLoading] = useState(true) const supabase = createClient() useEffect(() => { // Aktuelle Session holen supabase.auth.getUser().then(({ data: { user } }) => { setUser(user) setLoading(false) }) // Auth-Änderungen in Echtzeit abonnieren const { data: { subscription } } = supabase.auth.onAuthStateChange( (event, session) => { setUser(session?.user ?? null) setLoading(false) } ) return () => subscription.unsubscribe() }, []) return { user, loading } }

Auth Callback Route (SSR Session)

app/auth/callback/route.ts
import { NextResponse } from 'next/server' import { createClient } from '@/lib/supabase/server' export async function GET(request: Request) { const { searchParams, origin } = new URL(request.url) const code = searchParams.get('code') const next = searchParams.get('next') ?? '/' 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 für geschützte Routen: Ergänze eine middleware.ts im Root deines Projekts, die bei jeder Request die Session refresht und nicht authentifizierte User auf die Login-Seite weiterleitet.

3. Database & Row Level Security

Das Supabase-SDK bietet eine typisierte Query-Builder-API, die direkt auf PostgreSQL aufsetzt. RLS (Row Level Security) sorgt dafür, dass User nur auf ihre eigenen Daten zugreifen können — ohne zusätzlichen Server-Code.

CRUD-Operationen

lib/data/posts.ts
import { createClient } from '@/lib/supabase/server' import type { Database } from '@/types/supabase' type Post = Database['public']['Tables']['posts']['Row'] type PostInsert = Database['public']['Tables']['posts']['Insert'] // SELECT mit Filter export async function getPostsByUser(userId: string): Promise<Post[]> { const supabase = await createClient() const { data, error } = await supabase .from('posts') .select('id, title, content, created_at, tags') .eq('user_id', userId) .order('created_at', { ascending: false }) .limit(20) if (error) throw error return data } // INSERT export async function createPost(post: PostInsert): Promise<Post> { const supabase = await createClient() const { data, error } = await supabase .from('posts') .insert(post) .select() .single() if (error) throw error return data } // UPDATE export async function updatePost(id: string, updates: Partial<PostInsert>): Promise<Post> { const supabase = await createClient() const { data, error } = await supabase .from('posts') .update({ ...updates, updated_at: new Date().toISOString() }) .eq('id', id) .select() .single() if (error) throw error return data } // DELETE export async function deletePost(id: string): Promise<void> { const supabase = await createClient() const { error } = await supabase .from('posts') .delete() .eq('id', id) if (error) throw error }

Row Level Security: Policies definieren

RLS-Policies laufen direkt in PostgreSQL und erzwingen Datenzugangsbeschränkungen auf Datenbankebene — egal ob der Request über das SDK, REST API oder SQL kommt.

supabase/migrations/001_rls_policies.sql
-- RLS aktivieren ALTER TABLE posts ENABLE ROW LEVEL SECURITY; ALTER TABLE comments ENABLE ROW LEVEL SECURITY; -- Posts: User sieht nur eigene CREATE POLICY "users_own_posts" ON posts FOR ALL USING (auth.uid() = user_id) WITH CHECK (auth.uid() = user_id); -- Posts: Öffentlich lesbar wenn published = true CREATE POLICY "public_read_published" ON posts FOR SELECT USING (published = true); -- Comments: Eigene bearbeiten, alle lesen CREATE POLICY "read_all_comments" ON comments FOR SELECT USING (true); CREATE POLICY "insert_own_comments" ON comments FOR INSERT WITH CHECK (auth.uid() = author_id); CREATE POLICY "delete_own_comments" ON comments FOR DELETE USING (auth.uid() = author_id); -- Admin-Rolle: Voller Zugriff via Service Role Key CREATE POLICY "service_role_all" ON posts FOR ALL USING (auth.role() = 'service_role') WITH CHECK (auth.role() = 'service_role');
Best Practice

Definiere alle Policies als SQL-Migrations in supabase/migrations/. So sind sie versioniert, reproduzierbar und können mit supabase db push auf alle Environments deployed werden — inkl. CI/CD-Integration.

Joins und Relations

lib/data/posts-with-author.ts
// Nested Select: Posts mit Autor-Profil und Kommentaranzahl const { data } = await supabase .from('posts') .select(` id, title, content, created_at, profiles ( id, username, avatar_url ), comments ( count ) `) .eq('published', true) .order('created_at', { ascending: false })

4. Realtime Subscriptions: Postgres Changes, Broadcast & Presence

Realtime

Supabase Realtime baut auf PostgreSQL's WAL (Write-Ahead Log) auf. Clients verbinden sich via WebSocket und erhalten Push-Notifications bei Datenbankänderungen — ohne Polling, ohne eigenen WebSocket-Server.

Postgres Changes abonnieren

hooks/useRealtimePosts.ts
import { useEffect, useState } from 'react' import { createClient } from '@/lib/supabase/client' export function useRealtimePosts() { const [posts, setPosts] = useState([]) const supabase = createClient() useEffect(() => { // Initiale Daten laden supabase.from('posts').select('*').then(({ data }) => setPosts(data ?? [])) // Realtime Channel für Postgres Changes const channel = supabase .channel('posts-changes') .on( 'postgres_changes', { event: 'INSERT', schema: 'public', table: 'posts', filter: 'published=eq.true' }, (payload) => { setPosts(prev => [payload.new, ...prev]) } ) .on( 'postgres_changes', { event: 'UPDATE', schema: 'public', table: 'posts' }, (payload) => { setPosts(prev => prev.map(p => p.id === payload.new.id ? payload.new : p)) } ) .on( 'postgres_changes', { event: 'DELETE', schema: 'public', table: 'posts' }, (payload) => { setPosts(prev => prev.filter(p => p.id !== payload.old.id)) } ) .subscribe() return () => { supabase.removeChannel(channel) } }, []) return posts }

Broadcast: Echtzeit-Nachrichten zwischen Clients

Broadcast ermöglicht das direkte Senden von Events zwischen verbundenen Clients — ideal für kollaborative Features wie Live-Cursor, Chat oder Notifications.

hooks/useLiveChat.ts
const channel = supabase.channel(`room:${roomId}`) // Nachrichten empfangen channel.on('broadcast', { event: 'message' }, (payload) => { setMessages(prev => [...prev, payload.payload]) }) // Nachricht senden const sendMessage = async (text: string) => { await channel.send({ type: 'broadcast', event: 'message', payload: { id: crypto.randomUUID(), text, user_id: user.id, timestamp: new Date().toISOString() } }) } channel.subscribe()

Presence: Online-Status und Live-Cursors

hooks/usePresence.ts
const channel = supabase.channel(`doc:${docId}`, { config: { presence: { key: user.id } } }) // Presence-Sync: Alle verbundenen User channel.on('presence', { event: 'sync' }, () => { const state = channel.presenceState() setOnlineUsers(Object.values(state).flat()) }) channel.on('presence', { event: 'join' }, ({ newPresences }) => { console.log('Beigetreten:', newPresences) }) channel.on('presence', { event: 'leave' }, ({ leftPresences }) => { console.log('Verlassen:', leftPresences) }) await channel.subscribe(async (status) => { if (status === 'SUBSCRIBED') { await channel.track({ user_id: user.id, username: user.user_metadata.username, cursor: { x: 0, y: 0 }, online_at: new Date().toISOString() }) } })
💡 Performance-Tipp: Aktiviere Realtime-Subscriptions nur für Tabellen, die es wirklich brauchen. Im Supabase Dashboard unter Database → Replication kannst du genau steuern, welche Tabellen publiziert werden.

5. Storage & Edge Functions

Storage Edge Functions

Supabase Storage ist ein S3-kompatibler Object Store mit direkter Integration in das Auth-System. Edge Functions laufen auf Deno in über 300 Regionen weltweit — ohne Cold Starts bei kleinen Funktionen.

Datei-Upload mit Progress

lib/storage/upload.ts
import { createClient } from '@/lib/supabase/client' export async function uploadAvatar( userId: string, file: File, onProgress?: (percent: number) => void ): Promise<string> { const supabase = createClient() const ext = file.name.split('.').pop() const path = `avatars/${userId}.${ext}` const { error } = await supabase.storage .from('public-assets') .upload(path, file, { upsert: true, contentType: file.type, cacheControl: '3600' }) if (error) throw error // Öffentliche URL zurückgeben const { data } = supabase.storage .from('public-assets') .getPublicUrl(path) return data.publicUrl } export async function getSignedUrl(bucket: string, path: string) { const supabase = createClient() const { data, error } = await supabase.storage .from(bucket) .createSignedUrl(path, 3600) // 1 Stunde gültig if (error) throw error return data.signedUrl } export async function downloadFile(bucket: string, path: string) { const supabase = createClient() const { data, error } = await supabase.storage .from(bucket) .download(path) if (error) throw error return data // Blob }

Storage Policies (RLS)

supabase/migrations/002_storage_policies.sql
-- Bucket für private User-Dokumente INSERT INTO storage.buckets (id, name, public) VALUES ('user-documents', 'user-documents', false); -- RLS: User darf nur eigene Dateien sehen CREATE POLICY "user_own_files" ON storage.objects FOR ALL USING ( bucket_id = 'user-documents' AND auth.uid()::text = (storage.foldername(name))[1] ) WITH CHECK ( bucket_id = 'user-documents' AND auth.uid()::text = (storage.foldername(name))[1] );

Edge Functions: Serverless mit Deno

Edge Functions sind TypeScript/Deno-Funktionen, die global verteilt ausgeführt werden. Sie haben Zugriff auf alle Supabase-Services und können externe APIs aufrufen.

supabase/functions/process-upload/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' serve(async (req) => { const { file_path, user_id } = await req.json() // Service-Role-Client für adminisrative Operationen const supabase = createClient( Deno.env.get('SUPABASE_URL')!, Deno.env.get('SUPABASE_SERVICE_ROLE_KEY')! ) // Datei verarbeiten (z.B. Thumbnail generieren) const { data: file } = await supabase.storage .from('uploads') .download(file_path) // Embedding generieren (OpenAI / Ollama) const embeddingRes = await fetch('https://api.openai.com/v1/embeddings', { method: 'POST', headers: { 'Authorization': `Bearer ${Deno.env.get('OPENAI_API_KEY')}`, 'Content-Type': 'application/json' }, body: JSON.stringify({ model: 'text-embedding-3-small', input: file_path }) }) const { data: [{ embedding }] } = await embeddingRes.json() // In Datenbank speichern await supabase.from('documents').insert({ user_id, file_path, embedding }) return new Response(JSON.stringify({ success: true }), { headers: { 'Content-Type': 'application/json' } }) })
bash — Edge Function deployen
# Lokal entwickeln npx supabase functions serve process-upload --env-file .env.local # Secrets setzen (einmalig) npx supabase secrets set OPENAI_API_KEY=sk-... # Deployen npx supabase functions deploy process-upload # Alle Functions deployen npx supabase functions deploy

6. pgvector & AI: Embedding-Suche für KI-Anwendungen

pgvector & AI

pgvector ist die mächtigste Funktion von Supabase für AI-Applikationen. Es ermöglicht die Speicherung und Suche von Vektoren (Embeddings) direkt in PostgreSQL — keine separate Vektor-Datenbank nötig. Mit Hybrid Search kombinierst du semantische Ähnlichkeit und klassische Volltextsuche.

pgvector Setup: Extension und Schema

supabase/migrations/003_pgvector.sql
-- Extension aktivieren CREATE EXTENSION IF NOT EXISTS vector; -- Dokumente-Tabelle mit Embedding-Spalte CREATE TABLE documents ( id UUID DEFAULT gen_random_uuid() PRIMARY KEY, user_id UUID REFERENCES auth.users(id) ON DELETE CASCADE, title TEXT NOT NULL, content TEXT NOT NULL, embedding vector(1536), -- OpenAI text-embedding-3-small fts tsvector GENERATED ALWAYS AS ( to_tsvector('german', title || ' ' || content) ) STORED, metadata JSONB DEFAULT '{}', created_at TIMESTAMPTZ DEFAULT now() ); -- HNSW Index für schnelle Vektor-Suche (besser als IVFFlat) CREATE INDEX documents_embedding_hnsw ON documents USING hnsw (embedding vector_cosine_ops) WITH (m = 16, ef_construction = 64); -- GIN Index für Volltext-Suche CREATE INDEX documents_fts_idx ON documents USING gin(fts); -- RLS aktivieren ALTER TABLE documents ENABLE ROW LEVEL SECURITY; CREATE POLICY "users_own_docs" ON documents FOR ALL USING (auth.uid() = user_id);

match_documents RPC-Funktion

supabase/migrations/004_rpc_match_documents.sql
-- Semantische Ähnlichkeitssuche via RPC CREATE OR REPLACE FUNCTION match_documents( query_embedding vector(1536), match_threshold float DEFAULT 0.75, match_count int DEFAULT 10 ) RETURNS TABLE ( id UUID, title TEXT, content TEXT, similarity float ) LANGUAGE sql STABLE AS $$ SELECT id, title, content, 1 - (embedding <=> query_embedding) AS similarity FROM documents WHERE auth.uid() = user_id AND 1 - (embedding <=> query_embedding) > match_threshold ORDER BY embedding <=> query_embedding LIMIT match_count; $$; -- Hybrid Search: Vektoren + Volltext kombiniert CREATE OR REPLACE FUNCTION hybrid_search( query_text TEXT, query_embedding vector(1536), match_count int DEFAULT 10 ) RETURNS TABLE (id UUID, title TEXT, content TEXT, score float) LANGUAGE sql STABLE AS $$ WITH semantic AS ( SELECT id, 1 - (embedding <=> query_embedding) AS score FROM documents WHERE auth.uid() = user_id ORDER BY embedding <=> query_embedding LIMIT match_count * 2 ), fulltext AS ( SELECT id, ts_rank_cd(fts, plainto_tsquery('german', query_text)) AS score FROM documents WHERE fts @@ plainto_tsquery('german', query_text) AND auth.uid() = user_id LIMIT match_count * 2 ) SELECT d.id, d.title, d.content, COALESCE(s.score, 0) * 0.7 + COALESCE(ft.score, 0) * 0.3 AS score FROM documents d LEFT JOIN semantic s ON d.id = s.id LEFT JOIN fulltext ft ON d.id = ft.id WHERE s.id IS NOT NULL OR ft.id IS NOT NULL ORDER BY score DESC LIMIT match_count; $$;

Hybrid Search im TypeScript-Code

lib/ai/search.ts
import { createClient } from '@/lib/supabase/server' import OpenAI from 'openai' const openai = new OpenAI() async function getEmbedding(text: string): Promise<number[]> { const response = await openai.embeddings.create({ model: 'text-embedding-3-small', input: text, }) return response.data[0].embedding } export async function semanticSearch(query: string) { const supabase = await createClient() const embedding = await getEmbedding(query) // Reine Vektorsuche via match_documents RPC const { data, error } = await supabase.rpc('match_documents', { query_embedding: embedding, match_threshold: 0.75, match_count: 10 }) if (error) throw error return data } export async function hybridSearch(query: string) { const supabase = await createClient() const embedding = await getEmbedding(query) // Kombinierte Vektor- + Volltext-Suche const { data, error } = await supabase.rpc('hybrid_search', { query_text: query, query_embedding: embedding, match_count: 10 }) if (error) throw error return data } export async function indexDocument( userId: string, title: string, content: string ) { const supabase = await createClient() const embedding = await getEmbedding(`${title}\n\n${content}`) const { data, error } = await supabase .from('documents') .insert({ user_id: userId, title, content, embedding }) .select('id') .single() if (error) throw error return data.id }

Supabase vs. Alternativen: Feature-Vergleich 2026

Feature Supabase Firebase PlanetScale Neon
PostgreSQL ✓ Vollständig ✗ NoSQL ✓ MySQL ✓ Vollständig
Auth inkl.
Realtime ✓ WAL-basiert
Storage
Edge Functions ✓ Deno ✓ Node.js
pgvector / AI ✓ Nativ
Row Level Security ✓ SQL-nativ ✓ Custom Rules
Open Source
Self-Hosting ✓ Docker Teilweise
Free Tier ✓ 2 Projekte ✓ Spark Plan

Claude Code + Supabase: Das optimale Workflow

Workflow
  1. Schema definieren: Claude Code generiert Migrations aus natürlichsprachigen Requirements
  2. Types generieren: npx supabase gen types typescript --linked → vollständige Typisierung
  3. RLS-Policies: Claude Code analysiert dein Datenmodell und schlägt passende Policies vor
  4. Edge Functions: Serverless-Logik direkt in deinem Repo, Deployment mit einem Befehl
  5. pgvector-Integration: Claude Code erstellt Embedding-Pipeline, Indexierung und Search-API
  6. Testing: Lokale Supabase-Instanz via supabase start, Tests gegen echte DB
ℹ️ Lokale Entwicklung: Mit npx supabase start startest du eine vollständige lokale Supabase-Instanz (PostgreSQL + Auth + Storage + Realtime) via Docker. Claude Code kann alle Migrations automatisch anwenden und die lokale Instanz für Tests nutzen.

Supabase-Backend in Minuten aufsetzen?

Starte deinen kostenlosen Trial und lass Claude Code dein Supabase-Backend automatisch konfigurieren — Auth, Datenbank, RLS, Realtime und pgvector in einem Rutsch.

Kostenlosen Trial starten →

Kein Kreditkarte erforderlich · Setup in unter 5 Minuten · Jederzeit kündbar