Supabase (Legacy)
Supabase is an open-source Firebase alternative that provides a suite of tools for building modern applications. In earlier versions of VibeReference, Supabase served as the foundation for database, authentication, storage, and realtime features. VibeReference now defaults to Clerk (auth/billing) and Convex (database/functions/storage). See docs/convex-setup-workflow.md.
Introduction to Supabase
Supabase combines several powerful open-source tools into a developer-friendly platform:
- PostgreSQL Database: Robust relational database with JSONB support and extensions
- Auth: User management and multiple authentication methods
- Storage: File storage and management with security rules
- Realtime: Live data updates via WebSockets
- Edge Functions: Serverless functions for custom backend logic
- Vector: AI vector embeddings and similarity search
Setting Up Supabase in VibeReference
Configuration
To connect your Next.js application to Supabase:
// lib/supabase/server.ts
import { createServerClient } from '@supabase/ssr';
import { cookies } from 'next/headers';
import { type CookieOptions } from '@supabase/ssr';
export function createClient() {
const cookieStore = 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) {
try {
cookieStore.set({ name, value, ...options });
} catch (error) {
// Handle errors if cookies cannot be set
}
},
remove(name: string, options: CookieOptions) {
try {
cookieStore.delete({ name, ...options });
} catch (error) {
// Handle errors if cookies cannot be removed
}
},
},
}
);
}
// lib/supabase/client.ts
import { createBrowserClient } from '@supabase/ssr';
export function createClient() {
return createBrowserClient(
process.env.NEXT_PUBLIC_SUPABASE_URL!,
process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!
);
}
Middleware for Auth
// middleware.ts
import { NextRequest, NextResponse } from 'next/server';
import { createServerClient } from '@supabase/ssr';
export async function middleware(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: any) {
request.cookies.set({
name,
value,
...options,
});
response = NextResponse.next({
request: {
headers: request.headers,
},
});
response.cookies.set({
name,
value,
...options,
});
},
remove(name: string, options: any) {
request.cookies.delete({
name,
...options,
});
response = NextResponse.next({
request: {
headers: request.headers,
},
});
response.cookies.delete({
name,
...options,
});
},
},
}
);
// Refresh session if expired
await supabase.auth.getSession();
return response;
}
Authentication
Supabase provides multiple authentication options:
Email/Password Authentication
'use client'
import { useState } from 'react';
import { createClient } from '@/lib/supabase/client';
export default function SignUpForm() {
const [email, setEmail] = useState('');
const [password, setPassword] = useState('');
const [error, setError] = useState('');
const [loading, setLoading] = useState(false);
const supabase = createClient();
async function handleSignUp(e: React.FormEvent) {
e.preventDefault();
setLoading(true);
setError('');
try {
const { error } = await supabase.auth.signUp({
email,
password,
options: {
emailRedirectTo: `${window.location.origin}/auth/callback`,
},
});
if (error) throw error;
// Success message or redirect
} catch (error: any) {
setError(error.message);
} finally {
setLoading(false);
}
}
return (
<form onSubmit={handleSignUp}>
{/* Form fields */}
<input
type="email"
value={email}
onChange={(e) => setEmail(e.target.value)}
required
/>
<input
type="password"
value={password}
onChange={(e) => setPassword(e.target.value)}
required
/>
<button type="submit" disabled={loading}>
{loading ? 'Signing up...' : 'Sign Up'}
</button>
{error && <p className="error">{error}</p>}
</form>
);
}
OAuth Authentication
'use client'
import { createClient } from '@/lib/supabase/client';
export default function OAuthButtons() {
const supabase = createClient();
async function signInWithProvider(provider: 'google' | 'github') {
await supabase.auth.signInWithOAuth({
provider,
options: {
redirectTo: `${window.location.origin}/auth/callback`,
},
});
}
return (
<div>
<button onClick={() => signInWithProvider('google')}>
Sign in with Google
</button>
<button onClick={() => signInWithProvider('github')}>
Sign in with GitHub
</button>
</div>
);
}
Auth Callback Handler
// app/auth/callback/route.ts
import { createRouteHandlerClient } from '@supabase/auth-helpers-nextjs';
import { cookies } from 'next/headers';
import { NextRequest, NextResponse } from 'next/server';
export async function GET(request: NextRequest) {
const requestUrl = new URL(request.url);
const code = requestUrl.searchParams.get('code');
if (code) {
const cookieStore = cookies();
const supabase = createRouteHandlerClient({ cookies: () => cookieStore });
await supabase.auth.exchangeCodeForSession(code);
}
// URL to redirect to after sign in
return NextResponse.redirect(new URL('/dashboard', request.url));
}
Database Operations
Basic CRUD Operations
// Server component example
import { createClient } from '@/lib/supabase/server';
export default async function TasksList() {
const supabase = createClient();
const { data: tasks, error } = await supabase
.from('tasks')
.select('id, title, status, created_at')
.order('created_at', { ascending: false });
if (error) {
console.error('Error fetching tasks:', error);
return <div>Error loading tasks</div>;
}
return (
<ul>
{tasks.map((task) => (
<li key={task.id}>
<h3>{task.title}</h3>
<span>Status: {task.status}</span>
</li>
))}
</ul>
);
}
Advanced Queries
// Complex query with joins and filters
const { data: projects, error } = await supabase
.from('projects')
.select(`
id,
name,
description,
created_at,
team_members:project_members(
user_id,
role,
users(id, name, avatar_url)
)
`)
.eq('is_archived', false)
.in('status', ['active', 'planning'])
.order('created_at', { ascending: false })
.limit(10);
Server Actions
// app/actions/tasks.ts
'use server'
import { createClient } from '@/lib/supabase/server';
import { revalidatePath } from 'next/cache';
import { z } from 'zod';
const taskSchema = z.object({
title: z.string().min(3).max(100),
description: z.string().optional(),
status: z.enum(['todo', 'in_progress', 'completed']).default('todo'),
due_date: z.string().optional(),
});
export async function createTask(formData: FormData) {
const supabase = createClient();
try {
// Extract form data
const newTask = {
title: formData.get('title') as string,
description: formData.get('description') as string,
status: formData.get('status') as 'todo' | 'in_progress' | 'completed',
due_date: formData.get('due_date') as string,
};
// Validate data
const validatedTask = taskSchema.parse(newTask);
// Get user ID (assumes authenticated user)
const { data: { session } } = await supabase.auth.getSession();
if (!session) {
return { success: false, error: 'Not authenticated' };
}
// Insert task with user_id
const { data, error } = await supabase
.from('tasks')
.insert({
...validatedTask,
user_id: session.user.id,
})
.select()
.single();
if (error) throw error;
// Revalidate the tasks list page
revalidatePath('/dashboard/tasks');
return { success: true, task: data };
} catch (error: any) {
console.error('Error creating task:', error);
return { success: false, error: error.message };
}
}
Row Level Security
Implementing security policies in PostgreSQL:
-- Enable RLS on the tasks table
ALTER TABLE public.tasks ENABLE ROW LEVEL SECURITY;
-- Policy for selecting tasks (users can only see their own tasks)
CREATE POLICY select_own_tasks ON public.tasks
FOR SELECT
USING (user_id = auth.uid());
-- Policy for inserting tasks (users can only create tasks for themselves)
CREATE POLICY insert_own_tasks ON public.tasks
FOR INSERT
WITH CHECK (user_id = auth.uid());
-- Policy for updating tasks (users can only update their own tasks)
CREATE POLICY update_own_tasks ON public.tasks
FOR UPDATE
USING (user_id = auth.uid());
-- Policy for deleting tasks (users can only delete their own tasks)
CREATE POLICY delete_own_tasks ON public.tasks
FOR DELETE
USING (user_id = auth.uid());
Realtime Subscriptions
Listen for database changes in real-time:
'use client'
import { useEffect, useState } from 'react';
import { createClient } from '@/lib/supabase/client';
export default function RealtimeTasks() {
const [tasks, setTasks] = useState<any[]>([]);
const supabase = createClient();
useEffect(() => {
// Fetch initial tasks
const fetchTasks = async () => {
const { data } = await supabase
.from('tasks')
.select('*')
.order('created_at', { ascending: false });
if (data) setTasks(data);
};
fetchTasks();
// Set up realtime subscription
const channel = supabase
.channel('table:tasks')
.on(
'postgres_changes',
{
event: '*',
schema: 'public',
table: 'tasks',
},
(payload) => {
if (payload.eventType === 'INSERT') {
setTasks((prev) => [payload.new, ...prev]);
} else if (payload.eventType === 'UPDATE') {
setTasks((prev) =>
prev.map((task) => (task.id === payload.new.id ? payload.new : task))
);
} else if (payload.eventType === 'DELETE') {
setTasks((prev) => prev.filter((task) => task.id !== payload.old.id));
}
}
)
.subscribe();
// Clean up subscription on unmount
return () => {
supabase.removeChannel(channel);
};
}, [supabase]);
return (
<ul>
{tasks.map((task) => (
<li key={task.id}>{task.title}</li>
))}
</ul>
);
}
Storage
Managing files with Supabase Storage:
'use client'
import { useState } from 'react';
import { createClient } from '@/lib/supabase/client';
export default function FileUploader() {
const [file, setFile] = useState<File | null>(null);
const [uploading, setUploading] = useState(false);
const [uploadError, setUploadError] = useState('');
const [downloadUrl, setDownloadUrl] = useState('');
const supabase = createClient();
async function handleUpload() {
if (!file) return;
setUploading(true);
setUploadError('');
try {
// Generate a unique file path
const fileExt = file.name.split('.').pop();
const fileName = `${Date.now()}.${fileExt}`;
const filePath = `user-uploads/${fileName}`;
// Upload the file
const { error: uploadError } = await supabase.storage
.from('documents')
.upload(filePath, file);
if (uploadError) throw uploadError;
// Get the public URL
const { data } = supabase.storage
.from('documents')
.getPublicUrl(filePath);
setDownloadUrl(data.publicUrl);
} catch (error: any) {
setUploadError(error.message);
} finally {
setUploading(false);
}
}
return (
<div>
<input
type="file"
onChange={(e) => setFile(e.target.files?.[0] || null)}
/>
<button
onClick={handleUpload}
disabled={!file || uploading}
>
{uploading ? 'Uploading...' : 'Upload'}
</button>
{uploadError && <p className="error">{uploadError}</p>}
{downloadUrl && (
<div>
<p>File uploaded successfully!</p>
<a href={downloadUrl} target="_blank" rel="noreferrer">
View File
</a>
</div>
)}
</div>
);
}
Edge Functions
Deploying serverless functions with Supabase:
// /supabase/functions/generate-summary/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.32.0';
const corsHeaders = {
'Access-Control-Allow-Origin': '*',
'Access-Control-Allow-Methods': 'POST',
'Access-Control-Allow-Headers': 'Authorization, X-Client-Info, Content-Type',
};
serve(async (req) => {
// Handle CORS preflight request
if (req.method === 'OPTIONS') {
return new Response(null, { headers: corsHeaders });
}
try {
const { text } = await req.json();
if (!text) {
return new Response(
JSON.stringify({ error: 'Text is required' }),
{ status: 400, headers: { 'Content-Type': 'application/json', ...corsHeaders } }
);
}
// Initialize Supabase client with JWT token from request
const supabaseClient = createClient(
Deno.env.get('SUPABASE_URL') ?? '',
Deno.env.get('SUPABASE_ANON_KEY') ?? '',
{
global: {
headers: { Authorization: req.headers.get('Authorization')! },
},
}
);
// Get the current authenticated user
const { data: { user } } = await supabaseClient.auth.getUser();
if (!user) {
return new Response(
JSON.stringify({ error: 'Unauthorized' }),
{ status: 401, headers: { 'Content-Type': 'application/json', ...corsHeaders } }
);
}
// Process the text (e.g., with a third-party API like OpenAI)
const summary = 'This is a summary of the provided text.'; // Replace with actual implementation
// Store the result
await supabaseClient
.from('summaries')
.insert({
user_id: user.id,
original_text: text,
summary,
});
return new Response(
JSON.stringify({ summary }),
{ headers: { 'Content-Type': 'application/json', ...corsHeaders } }
);
} catch (error) {
return new Response(
JSON.stringify({ error: error.message }),
{ status: 500, headers: { 'Content-Type': 'application/json', ...corsHeaders } }
);
}
});
Type Safety with Database Types
Generate TypeScript types from your database schema:
# Install Supabase CLI
npm install -g supabase
# Login to Supabase
supabase login
# Generate types
supabase gen types typescript --project-id your-project-id > lib/database.types.ts
Using the generated types:
// lib/database.types.ts (generated)
export type Json = string | number | boolean | null | { [key: string]: Json | undefined } | Json[]
export interface Database {
public: {
Tables: {
tasks: {
Row: {
id: string
created_at: string
title: string
description: string | null
status: 'todo' | 'in_progress' | 'completed'
user_id: string
due_date: string | null
}
Insert: {
id?: string
created_at?: string
title: string
description?: string | null
status?: 'todo' | 'in_progress' | 'completed'
user_id: string
due_date?: string | null
}
Update: {
id?: string
created_at?: string
title?: string
description?: string | null
status?: 'todo' | 'in_progress' | 'completed'
user_id?: string
due_date?: string | null
}
}
// Other tables...
}
// Views, functions, etc...
}
}
// Using the types
import { createClient } from '@supabase/supabase-js';
import { Database } from '@/lib/database.types';
const supabase = createClient<Database>(
process.env.NEXT_PUBLIC_SUPABASE_URL!,
process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!
);
// Now you get type checking for table names, columns, and enums
const { data, error } = await supabase
.from('tasks')
.select('id, title, status')
.eq('status', 'todo'); // Type-checked: 'status' must be 'todo', 'in_progress', or 'completed'