install
source · Clone the upstream repo
git clone https://github.com/MacPhobos/research-mind
Claude Code · Install into ~/.claude/skills/
T=$(mktemp -d) && git clone --depth=1 https://github.com/MacPhobos/research-mind "$T" && mkdir -p ~/.claude/skills && cp -r "$T/.claude/skills/toolchains-platforms-backend-supabase" ~/.claude/skills/macphobos-research-mind-toolchains-platforms-backend-supabase && rm -rf "$T"
manifest:
.claude/skills/toolchains-platforms-backend-supabase/skill.mdsource content
Supabase Backend Platform Skill
progressive_disclosure: entry_point: summary: "Open-source Firebase alternative with Postgres, authentication, storage, and realtime" when_to_use: - "When building full-stack applications" - "When auth, database, and storage are required" - "When realtime subscriptions are needed" - "When using Next.js, React, or Vue" quick_start: - "Create project on Supabase console" - "npm install @supabase/supabase-js" - "Initialize client with URL and anon key" - "Use auth, database, storage, realtime" token_estimate: entry: 80-95 full: 5000-6000
Supabase Fundamentals
What is Supabase?
Open-source Firebase alternative built on:
- Postgres Database: Full SQL database with PostgREST API
- Authentication: Built-in auth with multiple providers
- Storage: File storage with image transformations
- Realtime: WebSocket subscriptions to database changes
- Edge Functions: Serverless functions on Deno runtime
- Row Level Security: Postgres RLS for data access control
Project Setup
# Install Supabase client npm install @supabase/supabase-js # Install CLI for local development npm install -D supabase # TypeScript types npm install -D @supabase/supabase-js
Client Initialization
// lib/supabase.ts import { createClient } from '@supabase/supabase-js' const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL! const supabaseAnonKey = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY! export const supabase = createClient(supabaseUrl, supabaseAnonKey) // With TypeScript types import { Database } from '@/types/supabase' export const supabase = createClient<Database>( supabaseUrl, supabaseAnonKey )
Database Operations
PostgREST API Basics
Supabase auto-generates REST API from Postgres schema:
// SELECT * FROM posts const { data, error } = await supabase .from('posts') .select('*') // SELECT with filters const { data } = await supabase .from('posts') .select('*') .eq('status', 'published') .order('created_at', { ascending: false }) .limit(10) // SELECT with joins const { data } = await supabase .from('posts') .select(` *, author:profiles(name, avatar), comments(count) `) // INSERT const { data, error } = await supabase .from('posts') .insert({ title: 'Hello', content: 'World' }) .select() .single() // UPDATE const { data } = await supabase .from('posts') .update({ status: 'published' }) .eq('id', postId) .select() // DELETE const { error } = await supabase .from('posts') .delete() .eq('id', postId) // UPSERT const { data } = await supabase .from('posts') .upsert({ id: 1, title: 'Updated' }) .select()
Advanced Queries
// Full-text search const { data } = await supabase .from('posts') .select('*') .textSearch('title', 'postgresql', { type: 'websearch', config: 'english' }) // Range queries const { data } = await supabase .from('posts') .select('*') .gte('created_at', '2024-01-01') .lte('created_at', '2024-12-31') // Array contains const { data } = await supabase .from('posts') .select('*') .contains('tags', ['postgres', 'supabase']) // JSON operations const { data } = await supabase .from('users') .select('*') .eq('metadata->theme', 'dark') // Count without data const { count } = await supabase .from('posts') .select('*', { count: 'exact', head: true }) // Pagination const pageSize = 10 const page = 2 const { data } = await supabase .from('posts') .select('*') .range(page * pageSize, (page + 1) * pageSize - 1)
Database Functions and RPC
// Call Postgres function const { data, error } = await supabase .rpc('get_trending_posts', { days: 7, min_score: 10 }) // Example function in SQL /* CREATE OR REPLACE FUNCTION get_trending_posts( days INTEGER, min_score INTEGER ) RETURNS TABLE ( id UUID, title TEXT, score INTEGER ) AS $$ BEGIN RETURN QUERY SELECT p.id, p.title, COUNT(v.id)::INTEGER as score FROM posts p LEFT JOIN votes v ON p.id = v.post_id WHERE p.created_at > NOW() - INTERVAL '1 day' * days GROUP BY p.id HAVING COUNT(v.id) >= min_score ORDER BY score DESC; END; $$ LANGUAGE plpgsql; */
Authentication
Email/Password Authentication
// Sign up const { data, error } = await supabase.auth.signUp({ email: 'user@example.com', password: 'secure-password', options: { data: { name: 'John Doe', avatar_url: 'https://...' } } }) // Sign in const { data, error } = await supabase.auth.signInWithPassword({ email: 'user@example.com', password: 'secure-password' }) // Sign out const { error } = await supabase.auth.signOut() // Get current user const { data: { user } } = await supabase.auth.getUser() // Get session const { data: { session } } = await supabase.auth.getSession()
OAuth Providers
// Sign in with OAuth const { data, error } = await supabase.auth.signInWithOAuth({ provider: 'github', options: { redirectTo: 'http://localhost:3000/auth/callback', scopes: 'repo user' } }) // Available providers // github, google, gitlab, bitbucket, azure, discord, facebook, // linkedin, notion, slack, spotify, twitch, twitter, apple
Magic Links
// Send magic link const { data, error } = await supabase.auth.signInWithOtp({ email: 'user@example.com', options: { emailRedirectTo: 'http://localhost:3000/auth/callback' } }) // Verify OTP const { data, error } = await supabase.auth.verifyOtp({ email: 'user@example.com', token: '123456', type: 'email' })
Phone Authentication
// Sign in with phone const { data, error } = await supabase.auth.signInWithOtp({ phone: '+1234567890' }) // Verify phone OTP const { data, error } = await supabase.auth.verifyOtp({ phone: '+1234567890', token: '123456', type: 'sms' })
Auth State Management
// Listen to auth changes supabase.auth.onAuthStateChange((event, session) => { if (event === 'SIGNED_IN') { console.log('User signed in:', session?.user) } if (event === 'SIGNED_OUT') { console.log('User signed out') } if (event === 'TOKEN_REFRESHED') { console.log('Token refreshed') } }) // Update user metadata const { data, error } = await supabase.auth.updateUser({ data: { theme: 'dark' } }) // Change password const { data, error } = await supabase.auth.updateUser({ password: 'new-password' })
Row Level Security (RLS)
RLS Fundamentals
Postgres Row Level Security controls data access at the database level:
-- Enable RLS on table ALTER TABLE posts ENABLE ROW LEVEL SECURITY; -- Policy: Users can read all published posts CREATE POLICY "Public posts are viewable by everyone" ON posts FOR SELECT USING (status = 'published'); -- Policy: Users can only update their own posts CREATE POLICY "Users can update own posts" ON posts FOR UPDATE USING (auth.uid() = author_id); -- Policy: Authenticated users can insert posts CREATE POLICY "Authenticated users can create posts" ON posts FOR INSERT WITH CHECK (auth.uid() = author_id); -- Policy: Users can delete their own posts CREATE POLICY "Users can delete own posts" ON posts FOR DELETE USING (auth.uid() = author_id);
Common RLS Patterns
-- Public read, authenticated write CREATE POLICY "Anyone can view posts" ON posts FOR SELECT USING (true); CREATE POLICY "Authenticated users can create posts" ON posts FOR INSERT WITH CHECK (auth.uid() IS NOT NULL); -- Organization-based access CREATE POLICY "Users can view org data" ON documents FOR SELECT USING ( organization_id IN ( SELECT organization_id FROM memberships WHERE user_id = auth.uid() ) ); -- Role-based access CREATE POLICY "Admins can do anything" ON posts FOR ALL USING ( EXISTS ( SELECT 1 FROM user_roles WHERE user_id = auth.uid() AND role = 'admin' ) ); -- Time-based access CREATE POLICY "View published or scheduled posts" ON posts FOR SELECT USING ( status = 'published' OR (status = 'scheduled' AND publish_at <= NOW()) );
RLS Helper Functions
-- Get current user ID SELECT auth.uid(); -- Get current user JWT SELECT auth.jwt(); -- Get specific claim SELECT auth.jwt()->>'email'; -- Custom claims SELECT auth.jwt()->'app_metadata'->>'role';
Storage
File Upload
// Upload file const { data, error } = await supabase.storage .from('avatars') .upload('public/avatar1.png', file, { cacheControl: '3600', upsert: false }) // Upload with progress const { data, error } = await supabase.storage .from('avatars') .upload('public/avatar1.png', file, { onUploadProgress: (progress) => { console.log(`${progress.loaded}/${progress.total}`) } }) // Upload from URL const { data, error } = await supabase.storage .from('avatars') .uploadToSignedUrl('path', token, file)
File Operations
// Download file const { data, error } = await supabase.storage .from('avatars') .download('public/avatar1.png') // Get public URL const { data } = supabase.storage .from('avatars') .getPublicUrl('public/avatar1.png') // Create signed URL (temporary access) const { data, error } = await supabase.storage .from('avatars') .createSignedUrl('private/document.pdf', 3600) // 1 hour // List files const { data, error } = await supabase.storage .from('avatars') .list('public', { limit: 100, offset: 0, sortBy: { column: 'name', order: 'asc' } }) // Delete file const { data, error } = await supabase.storage .from('avatars') .remove(['public/avatar1.png']) // Move file const { data, error } = await supabase.storage .from('avatars') .move('public/avatar1.png', 'public/avatar2.png')
Image Transformations
// Transform image const { data } = supabase.storage .from('avatars') .getPublicUrl('avatar1.png', { transform: { width: 200, height: 200, resize: 'cover', quality: 80 } }) // Available transformations // width, height, resize (cover|contain|fill), // quality (1-100), format (origin|jpeg|png|webp)
Storage RLS
-- Enable RLS on storage CREATE POLICY "Avatar images are publicly accessible" ON storage.objects FOR SELECT USING (bucket_id = 'avatars' AND (storage.foldername(name))[1] = 'public'); CREATE POLICY "Users can upload their own avatar" ON storage.objects FOR INSERT WITH CHECK ( bucket_id = 'avatars' AND (storage.foldername(name))[1] = auth.uid()::text ); CREATE POLICY "Users can delete their own avatar" ON storage.objects FOR DELETE USING ( bucket_id = 'avatars' AND (storage.foldername(name))[1] = auth.uid()::text );
Realtime Subscriptions
Database Changes
// Subscribe to inserts const channel = supabase .channel('posts-insert') .on( 'postgres_changes', { event: 'INSERT', schema: 'public', table: 'posts' }, (payload) => { console.log('New post:', payload.new) } ) .subscribe() // Subscribe to updates const channel = supabase .channel('posts-update') .on( 'postgres_changes', { event: 'UPDATE', schema: 'public', table: 'posts', filter: 'id=eq.1' }, (payload) => { console.log('Updated:', payload.new) console.log('Previous:', payload.old) } ) .subscribe() // Subscribe to all changes const channel = supabase .channel('posts-all') .on( 'postgres_changes', { event: '*', schema: 'public', table: 'posts' }, (payload) => { console.log('Change:', payload) } ) .subscribe() // Unsubscribe supabase.removeChannel(channel)
Presence (Track Online Users)
// Track presence const channel = supabase.channel('room-1') // Track current user channel .on('presence', { event: 'sync' }, () => { const state = channel.presenceState() console.log('Online users:', state) }) .on('presence', { event: 'join' }, ({ key, newPresences }) => { console.log('User joined:', newPresences) }) .on('presence', { event: 'leave' }, ({ key, leftPresences }) => { console.log('User left:', leftPresences) }) .subscribe(async (status) => { if (status === 'SUBSCRIBED') { await channel.track({ user_id: userId, online_at: new Date().toISOString() }) } }) // Untrack await channel.untrack()
Broadcast (Send Messages)
// Broadcast messages const channel = supabase.channel('chat-room') channel .on('broadcast', { event: 'message' }, (payload) => { console.log('Message:', payload) }) .subscribe() // Send message await channel.send({ type: 'broadcast', event: 'message', payload: { text: 'Hello', user: 'John' } })
Edge Functions
Edge Function Basics
Serverless functions on Deno runtime:
# Create function supabase functions new my-function # Serve locally supabase functions serve # Deploy supabase functions deploy my-function
Edge Function Example
// supabase/functions/my-function/index.ts import { serve } from 'https://deno.land/std@0.177.0/http/server.ts' import { createClient } from 'https://esm.sh/@supabase/supabase-js@2' serve(async (req) => { try { // Get auth header const authHeader = req.headers.get('Authorization')! // Create Supabase client const supabase = createClient( Deno.env.get('SUPABASE_URL') ?? '', Deno.env.get('SUPABASE_ANON_KEY') ?? '', { global: { headers: { Authorization: authHeader } } } ) // Verify user const { data: { user }, error } = await supabase.auth.getUser() if (error) throw error // Process request const { data } = await supabase .from('posts') .select('*') .eq('author_id', user.id) return new Response( JSON.stringify({ data }), { headers: { 'Content-Type': 'application/json' } } ) } catch (error) { return new Response( JSON.stringify({ error: error.message }), { status: 400, headers: { 'Content-Type': 'application/json' } } ) } })
Invoke Edge Function
// From client const { data, error } = await supabase.functions.invoke('my-function', { body: { name: 'John' } }) // With auth const { data, error } = await supabase.functions.invoke('my-function', { headers: { Authorization: `Bearer ${session.access_token}` }, body: { name: 'John' } })
Next.js Integration
App Router Setup
// lib/supabase/client.ts (Client Component) import { createBrowserClient } from '@supabase/ssr' export function createClient() { return createBrowserClient( process.env.NEXT_PUBLIC_SUPABASE_URL!, process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY! ) } // lib/supabase/server.ts (Server Component) import { createServerClient, type CookieOptions } from '@supabase/ssr' import { cookies } from 'next/headers' export async function createClient() { const cookieStore = await cookies() return createServerClient( process.env.NEXT_PUBLIC_SUPABASE_URL!, process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!, { cookies: { get(name: string) { return cookieStore.get(name)?.value }, set(name: string, value: string, options: CookieOptions) { cookieStore.set({ name, value, ...options }) }, remove(name: string, options: CookieOptions) { cookieStore.set({ name, value: '', ...options }) }, }, } ) } // lib/supabase/middleware.ts (Middleware) import { createServerClient, type CookieOptions } from '@supabase/ssr' import { NextResponse, type NextRequest } from 'next/server' export async function updateSession(request: NextRequest) { let response = NextResponse.next({ request: { headers: request.headers, }, }) const supabase = createServerClient( process.env.NEXT_PUBLIC_SUPABASE_URL!, process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!, { cookies: { get(name: string) { return request.cookies.get(name)?.value }, set(name: string, value: string, options: CookieOptions) { request.cookies.set({ name, value, ...options }) response = NextResponse.next({ request: { headers: request.headers }, }) response.cookies.set({ name, value, ...options }) }, remove(name: string, options: CookieOptions) { request.cookies.set({ name, value: '', ...options }) response = NextResponse.next({ request: { headers: request.headers }, }) response.cookies.set({ name, value: '', ...options }) }, }, } ) await supabase.auth.getUser() return response }
Middleware
// middleware.ts import { updateSession } from '@/lib/supabase/middleware' export async function middleware(request: NextRequest) { return await updateSession(request) } export const config = { matcher: [ '/((?!_next/static|_next/image|favicon.ico|.*\\.(?:svg|png|jpg|jpeg|gif|webp)$).*)', ], }
Server Component
// app/posts/page.tsx import { createClient } from '@/lib/supabase/server' export default async function PostsPage() { const supabase = await createClient() const { data: posts } = await supabase .from('posts') .select('*') .order('created_at', { ascending: false }) return ( <div> {posts?.map((post) => ( <article key={post.id}> <h2>{post.title}</h2> <p>{post.content}</p> </article> ))} </div> ) }
Client Component
// app/components/new-post.tsx 'use client' import { useState } from 'react' import { createClient } from '@/lib/supabase/client' export function NewPost() { const [title, setTitle] = useState('') const supabase = createClient() const handleSubmit = async (e: React.FormEvent) => { e.preventDefault() const { data: { user } } = await supabase.auth.getUser() if (!user) return const { error } = await supabase .from('posts') .insert({ title, author_id: user.id }) if (!error) { setTitle('') } } return ( <form onSubmit={handleSubmit}> <input value={title} onChange={(e) => setTitle(e.target.value)} placeholder="Post title" /> <button>Create</button> </form> ) }
Server Actions
// app/actions/posts.ts 'use server' import { revalidatePath } from 'next/cache' import { createClient } from '@/lib/supabase/server' export async function createPost(formData: FormData) { const supabase = await createClient() const { data: { user } } = await supabase.auth.getUser() if (!user) { return { error: 'Not authenticated' } } const title = formData.get('title') as string const { error } = await supabase .from('posts') .insert({ title, author_id: user.id }) if (error) { return { error: error.message } } revalidatePath('/posts') return { success: true } }
TypeScript Type Generation
Generate Types from Database
# Install CLI npm install -D supabase # Login npx supabase login # Link project npx supabase link --project-ref your-project-ref # Generate types npx supabase gen types typescript --project-id your-project-ref > types/supabase.ts # Or from local database npx supabase gen types typescript --local > types/supabase.ts
Use Generated Types
// types/supabase.ts (generated) export type Database = { public: { Tables: { posts: { Row: { id: string title: string content: string | null author_id: string created_at: string } Insert: { id?: string title: string content?: string | null author_id: string created_at?: string } Update: { id?: string title?: string content?: string | null author_id?: string created_at?: string } } } } } // Usage import { createClient } from '@supabase/supabase-js' import { Database } from '@/types/supabase' const supabase = createClient<Database>(url, key) // Type-safe queries const { data } = await supabase .from('posts') // TypeScript knows this table exists .select('title, content') // Autocomplete for columns .single() // data is typed as { title: string; content: string | null }
Supabase CLI and Local Development
Setup Local Development
# Initialize Supabase npx supabase init # Start local Supabase (Postgres, Auth, Storage, etc.) npx supabase start # Stop npx supabase stop # Reset database npx supabase db reset # Status npx supabase status
Database Migrations
# Create migration npx supabase migration new create_posts_table # Edit migration file # supabase/migrations/20240101000000_create_posts_table.sql # Apply migrations npx supabase db push # Pull remote schema npx supabase db pull # Diff local vs remote npx supabase db diff
Migration Example
-- supabase/migrations/20240101000000_create_posts_table.sql CREATE TABLE posts ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), title TEXT NOT NULL, content TEXT, author_id UUID NOT NULL REFERENCES auth.users(id), status TEXT NOT NULL DEFAULT 'draft', created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW() ); -- Enable RLS ALTER TABLE posts ENABLE ROW LEVEL SECURITY; -- Policies CREATE POLICY "Anyone can view published posts" ON posts FOR SELECT USING (status = 'published'); CREATE POLICY "Users can create their own posts" ON posts FOR INSERT WITH CHECK (auth.uid() = author_id); CREATE POLICY "Users can update their own posts" ON posts FOR UPDATE USING (auth.uid() = author_id); -- Indexes CREATE INDEX posts_author_id_idx ON posts(author_id); CREATE INDEX posts_status_idx ON posts(status); -- Trigger for updated_at CREATE TRIGGER set_updated_at BEFORE UPDATE ON posts FOR EACH ROW EXECUTE FUNCTION moddatetime(updated_at);
Security Best Practices
API Key Management
// NEVER expose service_role key in client // Use anon key for client-side const supabase = createClient( process.env.NEXT_PUBLIC_SUPABASE_URL!, process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY! // Public ) // Service role key only on server const supabaseAdmin = createClient( process.env.SUPABASE_URL!, process.env.SUPABASE_SERVICE_ROLE_KEY!, // Secret, bypasses RLS { auth: { persistSession: false } } )
RLS Best Practices
-- Always enable RLS ALTER TABLE posts ENABLE ROW LEVEL SECURITY; -- Default deny (no policy = no access) -- Explicitly grant access with policies -- Test policies as different users SET request.jwt.claims.sub = 'user-id'; SELECT * FROM posts; -- Test as this user -- Disable RLS only for admin operations -- Use service_role key from server, never client
Input Validation
// Validate on client and server function validatePost(data: unknown) { const schema = z.object({ title: z.string().min(1).max(200), content: z.string().max(10000).optional() }) return schema.parse(data) } // Server-side validation in Edge Function serve(async (req) => { const body = await req.json() try { const validated = validatePost(body) // Process validated data } catch (error) { return new Response( JSON.stringify({ error: 'Invalid input' }), { status: 400 } ) } })
Environment Variables
# .env.local NEXT_PUBLIC_SUPABASE_URL=https://xxx.supabase.co NEXT_PUBLIC_SUPABASE_ANON_KEY=eyJ... # Public SUPABASE_SERVICE_ROLE_KEY=eyJ... # Secret, server-only # Production: Use environment variables in hosting platform # Never commit .env files to git
Production Deployment
Database Optimization
-- Add indexes for common queries CREATE INDEX posts_created_at_idx ON posts(created_at DESC); CREATE INDEX posts_author_status_idx ON posts(author_id, status); -- Optimize full-text search CREATE INDEX posts_title_search_idx ON posts USING GIN (to_tsvector('english', title)); -- Analyze query performance EXPLAIN ANALYZE SELECT * FROM posts WHERE author_id = 'xxx'; -- Vacuum and analyze VACUUM ANALYZE posts;
Connection Pooling
// Use connection pooling for serverless import { createClient } from '@supabase/supabase-js' const supabase = createClient(url, key, { db: { schema: 'public', }, auth: { persistSession: true, autoRefreshToken: true, }, global: { headers: { 'x-my-custom-header': 'my-value' }, }, }) // Configure pool in Supabase dashboard // Settings > Database > Connection pooling
Monitoring
// Enable query logging const supabase = createClient(url, key, { global: { fetch: async (url, options) => { console.log('Query:', url) return fetch(url, options) } } }) // Monitor in Supabase Dashboard // - Database performance // - API usage // - Storage usage // - Auth activity
Backup Strategy
# Automatic backups (Pro plan+) # Daily backups with point-in-time recovery # Manual backup pg_dump -h db.xxx.supabase.co -U postgres -d postgres > backup.sql # Restore psql -h db.xxx.supabase.co -U postgres -d postgres < backup.sql
Supabase vs Firebase
Similarities
- Backend-as-a-Service platform
- Authentication with multiple providers
- Realtime data synchronization
- File storage
- Serverless functions
- Generous free tier
Key Differences
Database
- Supabase: PostgreSQL (SQL, full control)
- Firebase: Firestore (NoSQL, limited queries)
Queries
- Supabase: Full SQL, joins, aggregations
- Firebase: Limited filtering, no joins
Security
- Supabase: Row Level Security (Postgres native)
- Firebase: Security Rules (custom syntax)
Open Source
- Supabase: Fully open source, self-hostable
- Firebase: Proprietary, Google-hosted only
Pricing
- Supabase: Compute-based, predictable
- Firebase: Usage-based, can spike
Ecosystem
- Supabase: Postgres ecosystem (extensions, tools)
- Firebase: Google Cloud Platform integration
Migration Considerations
// Firestore collection query const snapshot = await db .collection('posts') .where('status', '==', 'published') .orderBy('createdAt', 'desc') .limit(10) .get() // Equivalent Supabase query const { data } = await supabase .from('posts') .select('*') .eq('status', 'published') .order('created_at', { ascending: false }) .limit(10) // Complex queries easier in Supabase const { data } = await supabase .from('posts') .select(` *, author:profiles!inner(name), comments(count) `) .gte('created_at', startDate) .lte('created_at', endDate) .order('created_at', { ascending: false }) // Firebase would require multiple queries + client-side joins
Advanced Patterns
Optimistic Updates
'use client' import { useState, useOptimistic } from 'react' import { createClient } from '@/lib/supabase/client' export function PostList({ initialPosts }: { initialPosts: Post[] }) { const [posts, setPosts] = useState(initialPosts) const [optimisticPosts, addOptimisticPost] = useOptimistic( posts, (state, newPost: Post) => [...state, newPost] ) const supabase = createClient() const createPost = async (title: string) => { const tempPost = { id: crypto.randomUUID(), title, created_at: new Date().toISOString() } addOptimisticPost(tempPost) const { data } = await supabase .from('posts') .insert({ title }) .select() .single() if (data) { setPosts([...posts, data]) } } return ( <div> {optimisticPosts.map((post) => ( <div key={post.id}>{post.title}</div> ))} </div> ) }
Infinite Scroll
'use client' import { useState, useEffect } from 'react' import { createClient } from '@/lib/supabase/client' const PAGE_SIZE = 20 export function InfinitePostList() { const [posts, setPosts] = useState<Post[]>([]) const [page, setPage] = useState(0) const [hasMore, setHasMore] = useState(true) const supabase = createClient() useEffect(() => { const loadMore = async () => { const { data } = await supabase .from('posts') .select('*') .range(page * PAGE_SIZE, (page + 1) * PAGE_SIZE - 1) .order('created_at', { ascending: false }) if (data) { setPosts([...posts, ...data]) setHasMore(data.length === PAGE_SIZE) } } loadMore() }, [page]) return ( <div> {posts.map((post) => ( <div key={post.id}>{post.title}</div> ))} {hasMore && ( <button onClick={() => setPage(page + 1)}> Load More </button> )} </div> ) }
Debounced Search
'use client' import { useState, useEffect } from 'react' import { createClient } from '@/lib/supabase/client' import { useDebounce } from '@/hooks/use-debounce' export function SearchPosts() { const [query, setQuery] = useState('') const [results, setResults] = useState<Post[]>([]) const debouncedQuery = useDebounce(query, 300) const supabase = createClient() useEffect(() => { if (!debouncedQuery) { setResults([]) return } const search = async () => { const { data } = await supabase .from('posts') .select('*') .textSearch('title', debouncedQuery) .limit(10) if (data) setResults(data) } search() }, [debouncedQuery]) return ( <div> <input value={query} onChange={(e) => setQuery(e.target.value)} placeholder="Search posts..." /> {results.map((post) => ( <div key={post.id}>{post.title}</div> ))} </div> ) }
Summary
Supabase provides a complete backend platform with:
- Postgres Database with REST and GraphQL APIs
- Built-in Authentication with multiple providers
- Row Level Security for granular access control
- File Storage with image transformations
- Realtime Subscriptions for live updates
- Edge Functions for serverless compute
- Next.js Integration with Server and Client Components
- TypeScript Support with auto-generated types
- Local Development with Supabase CLI
- Production Ready with monitoring and backups
Use Supabase when a full-featured backend with the power of Postgres, built-in auth, and realtime capabilities is needed, all with excellent TypeScript and Next.js integration.