Initial commit: Portfolio Website
Vollständige Next.js 15 Portfolio-Website mit: - Blog-System mit 100+ Artikeln - Supabase-Integration - Responsive Design mit Tailwind CSS - TypeScript-Konfiguration - Testing-Setup mit Vitest und Playwright Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,585 @@
|
||||
# Supabase: Die Open-Source Firebase Alternative für 2026
|
||||
|
||||
**Meta-Description:** Supabase als vollständige Backend-Plattform. PostgreSQL, Realtime, Auth, Edge Functions und Vector Embeddings in einem Stack.
|
||||
|
||||
**Keywords:** Supabase, PostgreSQL, Realtime Database, Edge Functions, Authentication, Firebase Alternative, Vector Embeddings
|
||||
|
||||
---
|
||||
|
||||
## Einführung
|
||||
|
||||
Supabase ist die **Open-Source Firebase Alternative**, die auf PostgreSQL aufbaut. 2026 bietet es eine vollständige Backend-Plattform: Database, Auth, Realtime, Edge Functions, Storage und Vector Embeddings – alles integriert.
|
||||
|
||||
---
|
||||
|
||||
## Supabase Stack
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────┐
|
||||
│ SUPABASE PLATFORM │
|
||||
├─────────────────────────────────────────────────────────────┤
|
||||
│ │
|
||||
│ Database Layer: │
|
||||
│ ├── PostgreSQL (Enterprise-Grade) │
|
||||
│ ├── Row Level Security (RLS) │
|
||||
│ ├── PostgREST (Auto-Generated APIs) │
|
||||
│ └── pgvector (Vector Embeddings) │
|
||||
│ │
|
||||
│ Realtime Layer: │
|
||||
│ ├── Postgres Changes (CDC) │
|
||||
│ ├── Broadcast (User-to-User) │
|
||||
│ ├── Presence (Online Status) │
|
||||
│ └── WebSocket Connections │
|
||||
│ │
|
||||
│ Auth Layer: │
|
||||
│ ├── Email/Password │
|
||||
│ ├── OAuth Providers (Google, GitHub, etc.) │
|
||||
│ ├── Magic Links │
|
||||
│ └── Phone/SMS Auth │
|
||||
│ │
|
||||
│ Edge Functions: │
|
||||
│ ├── Deno Runtime │
|
||||
│ ├── TypeScript/JavaScript │
|
||||
│ └── Global Distribution │
|
||||
│ │
|
||||
│ Storage: │
|
||||
│ ├── S3-Compatible │
|
||||
│ ├── CDN Integration │
|
||||
│ └── Image Transformations │
|
||||
│ │
|
||||
└─────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Database Setup
|
||||
|
||||
```typescript
|
||||
// lib/supabase.ts
|
||||
import { createClient } from '@supabase/supabase-js';
|
||||
import { Database } from './database.types';
|
||||
|
||||
const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL!;
|
||||
const supabaseAnonKey = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!;
|
||||
|
||||
export const supabase = createClient<Database>(supabaseUrl, supabaseAnonKey);
|
||||
|
||||
// Server-Side mit Service Role
|
||||
import { createClient } from '@supabase/supabase-js';
|
||||
|
||||
export const supabaseAdmin = createClient<Database>(
|
||||
process.env.SUPABASE_URL!,
|
||||
process.env.SUPABASE_SERVICE_ROLE_KEY!,
|
||||
{
|
||||
auth: {
|
||||
autoRefreshToken: false,
|
||||
persistSession: false
|
||||
}
|
||||
}
|
||||
);
|
||||
```
|
||||
|
||||
### Schema Definition (SQL)
|
||||
|
||||
```sql
|
||||
-- Users Tabelle (erweitert auth.users)
|
||||
CREATE TABLE public.profiles (
|
||||
id UUID REFERENCES auth.users(id) ON DELETE CASCADE PRIMARY KEY,
|
||||
username TEXT UNIQUE,
|
||||
full_name TEXT,
|
||||
avatar_url TEXT,
|
||||
bio TEXT,
|
||||
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
|
||||
updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
|
||||
);
|
||||
|
||||
-- Posts Tabelle
|
||||
CREATE TABLE public.posts (
|
||||
id UUID DEFAULT gen_random_uuid() PRIMARY KEY,
|
||||
author_id UUID REFERENCES public.profiles(id) ON DELETE CASCADE NOT NULL,
|
||||
title TEXT NOT NULL,
|
||||
content TEXT,
|
||||
published BOOLEAN DEFAULT FALSE,
|
||||
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
|
||||
updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
|
||||
);
|
||||
|
||||
-- Trigger für updated_at
|
||||
CREATE OR REPLACE FUNCTION update_updated_at()
|
||||
RETURNS TRIGGER AS $$
|
||||
BEGIN
|
||||
NEW.updated_at = NOW();
|
||||
RETURN NEW;
|
||||
END;
|
||||
$$ LANGUAGE plpgsql;
|
||||
|
||||
CREATE TRIGGER profiles_updated_at
|
||||
BEFORE UPDATE ON public.profiles
|
||||
FOR EACH ROW EXECUTE FUNCTION update_updated_at();
|
||||
|
||||
CREATE TRIGGER posts_updated_at
|
||||
BEFORE UPDATE ON public.posts
|
||||
FOR EACH ROW EXECUTE FUNCTION update_updated_at();
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Row Level Security (RLS)
|
||||
|
||||
```sql
|
||||
-- RLS aktivieren
|
||||
ALTER TABLE public.profiles ENABLE ROW LEVEL SECURITY;
|
||||
ALTER TABLE public.posts ENABLE ROW LEVEL SECURITY;
|
||||
|
||||
-- Profiles: Jeder kann lesen, nur Owner kann schreiben
|
||||
CREATE POLICY "Profiles sind öffentlich lesbar"
|
||||
ON public.profiles FOR SELECT
|
||||
USING (true);
|
||||
|
||||
CREATE POLICY "User kann eigenes Profil bearbeiten"
|
||||
ON public.profiles FOR UPDATE
|
||||
USING (auth.uid() = id);
|
||||
|
||||
-- Posts: Veröffentlichte sind lesbar, Autor kann alles
|
||||
CREATE POLICY "Veröffentlichte Posts sind lesbar"
|
||||
ON public.posts FOR SELECT
|
||||
USING (published = true OR auth.uid() = author_id);
|
||||
|
||||
CREATE POLICY "Autor kann eigene Posts erstellen"
|
||||
ON public.posts FOR INSERT
|
||||
WITH CHECK (auth.uid() = author_id);
|
||||
|
||||
CREATE POLICY "Autor kann eigene Posts bearbeiten"
|
||||
ON public.posts FOR UPDATE
|
||||
USING (auth.uid() = author_id);
|
||||
|
||||
CREATE POLICY "Autor kann eigene Posts löschen"
|
||||
ON public.posts FOR DELETE
|
||||
USING (auth.uid() = author_id);
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## CRUD Operations
|
||||
|
||||
```typescript
|
||||
// CREATE
|
||||
async function createPost(post: { title: string; content: string }) {
|
||||
const { data, error } = await supabase
|
||||
.from('posts')
|
||||
.insert({
|
||||
title: post.title,
|
||||
content: post.content,
|
||||
author_id: (await supabase.auth.getUser()).data.user?.id
|
||||
})
|
||||
.select()
|
||||
.single();
|
||||
|
||||
if (error) throw error;
|
||||
return data;
|
||||
}
|
||||
|
||||
// READ
|
||||
async function getPosts() {
|
||||
const { data, error } = await supabase
|
||||
.from('posts')
|
||||
.select(`
|
||||
*,
|
||||
author:profiles(username, avatar_url)
|
||||
`)
|
||||
.eq('published', true)
|
||||
.order('created_at', { ascending: false })
|
||||
.limit(10);
|
||||
|
||||
if (error) throw error;
|
||||
return data;
|
||||
}
|
||||
|
||||
// READ mit Pagination
|
||||
async function getPostsPaginated(page: number, pageSize: number = 10) {
|
||||
const from = page * pageSize;
|
||||
const to = from + pageSize - 1;
|
||||
|
||||
const { data, error, count } = await supabase
|
||||
.from('posts')
|
||||
.select('*, author:profiles(username)', { count: 'exact' })
|
||||
.eq('published', true)
|
||||
.range(from, to);
|
||||
|
||||
return { data, count, hasMore: count ? to < count - 1 : false };
|
||||
}
|
||||
|
||||
// UPDATE
|
||||
async function updatePost(id: string, updates: Partial<Post>) {
|
||||
const { data, error } = await supabase
|
||||
.from('posts')
|
||||
.update(updates)
|
||||
.eq('id', id)
|
||||
.select()
|
||||
.single();
|
||||
|
||||
if (error) throw error;
|
||||
return data;
|
||||
}
|
||||
|
||||
// DELETE
|
||||
async function deletePost(id: string) {
|
||||
const { error } = await supabase
|
||||
.from('posts')
|
||||
.delete()
|
||||
.eq('id', id);
|
||||
|
||||
if (error) throw error;
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Realtime Subscriptions
|
||||
|
||||
```typescript
|
||||
import { useEffect, useState } from 'react';
|
||||
import { supabase } from '@/lib/supabase';
|
||||
import { RealtimeChannel } from '@supabase/supabase-js';
|
||||
|
||||
// 1. Database Changes (CDC)
|
||||
function useRealtimePosts() {
|
||||
const [posts, setPosts] = useState<Post[]>([]);
|
||||
|
||||
useEffect(() => {
|
||||
// Initial Load
|
||||
supabase.from('posts').select('*').then(({ data }) => {
|
||||
if (data) setPosts(data);
|
||||
});
|
||||
|
||||
// Subscribe to Changes
|
||||
const channel = supabase
|
||||
.channel('posts-changes')
|
||||
.on(
|
||||
'postgres_changes',
|
||||
{
|
||||
event: '*',
|
||||
schema: 'public',
|
||||
table: 'posts'
|
||||
},
|
||||
(payload) => {
|
||||
if (payload.eventType === 'INSERT') {
|
||||
setPosts(prev => [payload.new as Post, ...prev]);
|
||||
} else if (payload.eventType === 'UPDATE') {
|
||||
setPosts(prev =>
|
||||
prev.map(p => p.id === payload.new.id ? payload.new as Post : p)
|
||||
);
|
||||
} else if (payload.eventType === 'DELETE') {
|
||||
setPosts(prev => prev.filter(p => p.id !== payload.old.id));
|
||||
}
|
||||
}
|
||||
)
|
||||
.subscribe();
|
||||
|
||||
return () => {
|
||||
supabase.removeChannel(channel);
|
||||
};
|
||||
}, []);
|
||||
|
||||
return posts;
|
||||
}
|
||||
|
||||
// 2. Broadcast (User-to-User Messaging)
|
||||
function useBroadcast(roomId: string) {
|
||||
const [messages, setMessages] = useState<Message[]>([]);
|
||||
|
||||
useEffect(() => {
|
||||
const channel = supabase.channel(`room:${roomId}`)
|
||||
.on('broadcast', { event: 'message' }, ({ payload }) => {
|
||||
setMessages(prev => [...prev, payload]);
|
||||
})
|
||||
.subscribe();
|
||||
|
||||
return () => {
|
||||
supabase.removeChannel(channel);
|
||||
};
|
||||
}, [roomId]);
|
||||
|
||||
const sendMessage = (content: string) => {
|
||||
supabase.channel(`room:${roomId}`).send({
|
||||
type: 'broadcast',
|
||||
event: 'message',
|
||||
payload: { content, timestamp: new Date().toISOString() }
|
||||
});
|
||||
};
|
||||
|
||||
return { messages, sendMessage };
|
||||
}
|
||||
|
||||
// 3. Presence (Online Status)
|
||||
function usePresence(roomId: string, userId: string) {
|
||||
const [onlineUsers, setOnlineUsers] = useState<string[]>([]);
|
||||
|
||||
useEffect(() => {
|
||||
const channel = supabase.channel(`presence:${roomId}`)
|
||||
.on('presence', { event: 'sync' }, () => {
|
||||
const state = channel.presenceState();
|
||||
const users = Object.values(state).flat().map(p => p.user_id);
|
||||
setOnlineUsers(users);
|
||||
})
|
||||
.subscribe(async (status) => {
|
||||
if (status === 'SUBSCRIBED') {
|
||||
await channel.track({ user_id: userId });
|
||||
}
|
||||
});
|
||||
|
||||
return () => {
|
||||
supabase.removeChannel(channel);
|
||||
};
|
||||
}, [roomId, userId]);
|
||||
|
||||
return onlineUsers;
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Authentication
|
||||
|
||||
```typescript
|
||||
// Sign Up
|
||||
async function signUp(email: string, password: string) {
|
||||
const { data, error } = await supabase.auth.signUp({
|
||||
email,
|
||||
password,
|
||||
options: {
|
||||
emailRedirectTo: `${window.location.origin}/auth/callback`
|
||||
}
|
||||
});
|
||||
return { data, error };
|
||||
}
|
||||
|
||||
// Sign In
|
||||
async function signIn(email: string, password: string) {
|
||||
const { data, error } = await supabase.auth.signInWithPassword({
|
||||
email,
|
||||
password
|
||||
});
|
||||
return { data, error };
|
||||
}
|
||||
|
||||
// OAuth Sign In
|
||||
async function signInWithOAuth(provider: 'google' | 'github') {
|
||||
const { data, error } = await supabase.auth.signInWithOAuth({
|
||||
provider,
|
||||
options: {
|
||||
redirectTo: `${window.location.origin}/auth/callback`
|
||||
}
|
||||
});
|
||||
return { data, error };
|
||||
}
|
||||
|
||||
// Sign Out
|
||||
async function signOut() {
|
||||
const { error } = await supabase.auth.signOut();
|
||||
return { error };
|
||||
}
|
||||
|
||||
// Auth State Hook
|
||||
function useAuth() {
|
||||
const [user, setUser] = useState<User | null>(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
|
||||
useEffect(() => {
|
||||
supabase.auth.getSession().then(({ data: { session } }) => {
|
||||
setUser(session?.user ?? null);
|
||||
setLoading(false);
|
||||
});
|
||||
|
||||
const { data: { subscription } } = supabase.auth.onAuthStateChange(
|
||||
(_event, session) => {
|
||||
setUser(session?.user ?? null);
|
||||
}
|
||||
);
|
||||
|
||||
return () => subscription.unsubscribe();
|
||||
}, []);
|
||||
|
||||
return { user, loading };
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Edge Functions
|
||||
|
||||
```typescript
|
||||
// supabase/functions/send-email/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';
|
||||
|
||||
const corsHeaders = {
|
||||
'Access-Control-Allow-Origin': '*',
|
||||
'Access-Control-Allow-Headers': 'authorization, x-client-info, apikey, content-type'
|
||||
};
|
||||
|
||||
serve(async (req) => {
|
||||
if (req.method === 'OPTIONS') {
|
||||
return new Response('ok', { headers: corsHeaders });
|
||||
}
|
||||
|
||||
try {
|
||||
const supabase = createClient(
|
||||
Deno.env.get('SUPABASE_URL')!,
|
||||
Deno.env.get('SUPABASE_SERVICE_ROLE_KEY')!
|
||||
);
|
||||
|
||||
const { to, subject, body } = await req.json();
|
||||
|
||||
// Send email via Resend, SendGrid, etc.
|
||||
const response = 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: 'noreply@example.com',
|
||||
to,
|
||||
subject,
|
||||
html: body
|
||||
})
|
||||
});
|
||||
|
||||
const result = await response.json();
|
||||
|
||||
return new Response(JSON.stringify(result), {
|
||||
headers: { ...corsHeaders, 'Content-Type': 'application/json' }
|
||||
});
|
||||
} catch (error) {
|
||||
return new Response(JSON.stringify({ error: error.message }), {
|
||||
status: 500,
|
||||
headers: { ...corsHeaders, 'Content-Type': 'application/json' }
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// Client-Side Aufruf
|
||||
const { data, error } = await supabase.functions.invoke('send-email', {
|
||||
body: {
|
||||
to: 'user@example.com',
|
||||
subject: 'Welcome!',
|
||||
body: '<h1>Welcome to our platform!</h1>'
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Vector Embeddings (AI)
|
||||
|
||||
```sql
|
||||
-- pgvector Extension aktivieren
|
||||
CREATE EXTENSION IF NOT EXISTS vector;
|
||||
|
||||
-- Documents Tabelle mit Embeddings
|
||||
CREATE TABLE documents (
|
||||
id UUID DEFAULT gen_random_uuid() PRIMARY KEY,
|
||||
content TEXT NOT NULL,
|
||||
embedding VECTOR(1536), -- OpenAI ada-002 Dimension
|
||||
metadata JSONB,
|
||||
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
|
||||
);
|
||||
|
||||
-- Index für schnelle Similarity Search
|
||||
CREATE INDEX ON documents USING ivfflat (embedding vector_cosine_ops)
|
||||
WITH (lists = 100);
|
||||
|
||||
-- Similarity Search Funktion
|
||||
CREATE OR REPLACE FUNCTION search_documents(
|
||||
query_embedding VECTOR(1536),
|
||||
match_count INT DEFAULT 5
|
||||
)
|
||||
RETURNS TABLE (
|
||||
id UUID,
|
||||
content TEXT,
|
||||
similarity FLOAT
|
||||
) AS $$
|
||||
BEGIN
|
||||
RETURN QUERY
|
||||
SELECT
|
||||
documents.id,
|
||||
documents.content,
|
||||
1 - (documents.embedding <=> query_embedding) AS similarity
|
||||
FROM documents
|
||||
ORDER BY documents.embedding <=> query_embedding
|
||||
LIMIT match_count;
|
||||
END;
|
||||
$$ LANGUAGE plpgsql;
|
||||
```
|
||||
|
||||
```typescript
|
||||
// Embedding generieren und speichern
|
||||
async function addDocument(content: string) {
|
||||
// Embedding via OpenAI
|
||||
const embeddingResponse = await openai.embeddings.create({
|
||||
model: 'text-embedding-ada-002',
|
||||
input: content
|
||||
});
|
||||
|
||||
const embedding = embeddingResponse.data[0].embedding;
|
||||
|
||||
// In Supabase speichern
|
||||
const { data, error } = await supabase
|
||||
.from('documents')
|
||||
.insert({ content, embedding })
|
||||
.select()
|
||||
.single();
|
||||
|
||||
return data;
|
||||
}
|
||||
|
||||
// Similarity Search
|
||||
async function searchDocuments(query: string) {
|
||||
// Query Embedding
|
||||
const embeddingResponse = await openai.embeddings.create({
|
||||
model: 'text-embedding-ada-002',
|
||||
input: query
|
||||
});
|
||||
|
||||
const queryEmbedding = embeddingResponse.data[0].embedding;
|
||||
|
||||
// Search via RPC
|
||||
const { data, error } = await supabase.rpc('search_documents', {
|
||||
query_embedding: queryEmbedding,
|
||||
match_count: 5
|
||||
});
|
||||
|
||||
return data;
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Fazit
|
||||
|
||||
Supabase bietet 2026:
|
||||
|
||||
1. **PostgreSQL Power**: Enterprise-DB mit RLS, Triggers, Functions
|
||||
2. **Realtime Built-in**: CDC, Broadcast, Presence über WebSockets
|
||||
3. **Edge Functions**: Deno-basiert, global verteilt
|
||||
4. **Vector Search**: pgvector für AI/RAG-Anwendungen
|
||||
|
||||
Eine vollständige Backend-Plattform für moderne Anwendungen.
|
||||
|
||||
---
|
||||
|
||||
## Bildprompts
|
||||
|
||||
1. "Database and real-time connections flowing together, Supabase architecture visualization"
|
||||
2. "PostgreSQL elephant with real-time lightning bolts, modern database concept"
|
||||
3. "Edge functions distributed globally on world map, serverless infrastructure"
|
||||
|
||||
---
|
||||
|
||||
## Quellen
|
||||
|
||||
- [Supabase Documentation](https://supabase.com/docs)
|
||||
- [Supabase Features](https://supabase.com/features)
|
||||
- [Supabase GitHub](https://github.com/supabase/supabase)
|
||||
- [Supabase Review 2026](https://hackceleration.com/supabase-review/)
|
||||
Reference in New Issue
Block a user