Backend Infrastructure
Earna AI Console uses Supabase as the backend for authentication, PostgreSQL database, real-time subscriptions, and file storage for the GPT-4o powered chat platform. This guide covers complete setup, schema design, RLS policies, and best practices.
Overview
Supabase provides:
- Authentication: Email/password, OAuth, magic links
- Database: PostgreSQL with Row Level Security (RLS)
- Storage: S3-compatible file storage
- Real-time: WebSocket subscriptions for live updates
- Edge Functions: Serverless functions (optional)
- Vector Database: pgvector for embeddings (optional)
Architecture
Setup
Create Supabase Project
- Go to supabase.com and create an account
- Create a new project
- Choose a region close to your users
- Save your database password securely
Get API Keys
Navigate to Settings → API in your Supabase dashboard:
# Add to .env.local
NEXT_PUBLIC_SUPABASE_URL=https://your-project.supabase.co
NEXT_PUBLIC_SUPABASE_ANON_KEY=your-anon-key
SUPABASE_SERVICE_ROLE_KEY=your-service-role-key
Install Client Libraries
pnpm add @supabase/supabase-js @supabase/ssr
Configure Client
Create the Supabase client configuration:
// lib/supabase/client.ts
import { createBrowserClient } from '@supabase/ssr'
import type { Database } from '@/types/database'
export function createClient() {
return createBrowserClient<Database>(
process.env.NEXT_PUBLIC_SUPABASE_URL!,
process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!
)
}
// lib/supabase/server.ts
import { createServerClient } from '@supabase/ssr'
import { cookies } from 'next/headers'
import type { Database } from '@/types/database'
export async function createServerSupabaseClient() {
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)
)
},
},
}
)
}
Database Schema
Core Tables
Core Database Tables
-- Users table (extends Supabase auth.users)
CREATE TABLE public.users (
id UUID PRIMARY KEY REFERENCES auth.users(id) ON DELETE CASCADE,
email TEXT UNIQUE,
username TEXT UNIQUE,
full_name TEXT,
avatar_url TEXT,
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
is_anonymous BOOLEAN DEFAULT false,
subscription_tier TEXT DEFAULT 'free',
daily_message_count INTEGER DEFAULT 0,
daily_pro_message_count INTEGER DEFAULT 0,
last_message_reset TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
settings JSONB DEFAULT '{}',
metadata JSONB DEFAULT '{}'
);
-- Chats table
CREATE TABLE public.chats (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
user_id UUID REFERENCES public.users(id) ON DELETE CASCADE,
title TEXT,
model TEXT DEFAULT 'gpt-4o',
system_prompt TEXT,
temperature FLOAT DEFAULT 0.7,
max_tokens INTEGER DEFAULT 4096,
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
is_archived BOOLEAN DEFAULT false,
metadata JSONB DEFAULT '{}'
);
-- Messages table
CREATE TABLE public.messages (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
chat_id UUID REFERENCES public.chats(id) ON DELETE CASCADE,
role TEXT NOT NULL CHECK (role IN ('user', 'assistant', 'system', 'tool')),
content TEXT NOT NULL,
model TEXT,
tokens_used INTEGER,
cost DECIMAL(10,6),
attachments JSONB DEFAULT '[]',
tool_calls JSONB DEFAULT '[]',
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
metadata JSONB DEFAULT '{}'
);
-- Subscriptions table
CREATE TABLE public.subscriptions (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
user_id UUID REFERENCES public.users(id) ON DELETE CASCADE,
stripe_customer_id TEXT UNIQUE,
stripe_subscription_id TEXT UNIQUE,
plan TEXT NOT NULL,
status TEXT NOT NULL,
current_period_start TIMESTAMP WITH TIME ZONE,
current_period_end TIMESTAMP WITH TIME ZONE,
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
metadata JSONB DEFAULT '{}'
);
-- Settings table
CREATE TABLE public.settings (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
user_id UUID REFERENCES public.users(id) ON DELETE CASCADE,
key TEXT NOT NULL,
value JSONB NOT NULL,
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
UNIQUE(user_id, key)
);
-- Create indexes for performance
CREATE INDEX idx_chats_user_id ON public.chats(user_id);
CREATE INDEX idx_messages_chat_id ON public.messages(chat_id);
CREATE INDEX idx_messages_created_at ON public.messages(created_at DESC);
CREATE INDEX idx_subscriptions_user_id ON public.subscriptions(user_id);
CREATE INDEX idx_settings_user_id ON public.settings(user_id);
Row Level Security (RLS)
Always enable RLS on all tables to ensure data security. Never expose the service role key to the client.
-- Enable RLS on all tables
ALTER TABLE public.users ENABLE ROW LEVEL SECURITY;
ALTER TABLE public.chats ENABLE ROW LEVEL SECURITY;
ALTER TABLE public.messages ENABLE ROW LEVEL SECURITY;
ALTER TABLE public.subscriptions ENABLE ROW LEVEL SECURITY;
ALTER TABLE public.settings ENABLE ROW LEVEL SECURITY;
-- Users policies
CREATE POLICY "Users can view own profile"
ON public.users FOR SELECT
USING (auth.uid() = id);
CREATE POLICY "Users can update own profile"
ON public.users FOR UPDATE
USING (auth.uid() = id);
-- Chats policies
CREATE POLICY "Users can view own chats"
ON public.chats FOR SELECT
USING (auth.uid() = user_id);
CREATE POLICY "Users can create own chats"
ON public.chats FOR INSERT
WITH CHECK (auth.uid() = user_id);
CREATE POLICY "Users can update own chats"
ON public.chats FOR UPDATE
USING (auth.uid() = user_id);
CREATE POLICY "Users can delete own chats"
ON public.chats FOR DELETE
USING (auth.uid() = user_id);
-- Messages policies
CREATE POLICY "Users can view messages in own chats"
ON public.messages FOR SELECT
USING (
EXISTS (
SELECT 1 FROM public.chats
WHERE chats.id = messages.chat_id
AND chats.user_id = auth.uid()
)
);
CREATE POLICY "Users can create messages in own chats"
ON public.messages FOR INSERT
WITH CHECK (
EXISTS (
SELECT 1 FROM public.chats
WHERE chats.id = messages.chat_id
AND chats.user_id = auth.uid()
)
);
-- Subscriptions policies
CREATE POLICY "Users can view own subscription"
ON public.subscriptions FOR SELECT
USING (auth.uid() = user_id);
-- Settings policies
CREATE POLICY "Users can manage own settings"
ON public.settings FOR ALL
USING (auth.uid() = user_id);
Authentication
Setup
Authentication Setup
// lib/auth/config.ts
import { createClient } from '@/lib/supabase/client'
export const authConfig = {
providers: ['email', 'google', 'github'],
redirectTo: process.env.NEXT_PUBLIC_APP_URL || 'http://localhost:3000',
emailRedirectTo: '/auth/callback',
// Session configuration
session: {
expiresIn: 60 * 60 * 24 * 7, // 7 days
refreshThreshold: 60 * 60, // Refresh if less than 1 hour left
},
// Rate limiting
rateLimits: {
signUp: { points: 5, duration: 60 * 60 }, // 5 signups per hour
signIn: { points: 10, duration: 60 * 60 }, // 10 attempts per hour
}
}
Storage
Configure and use Supabase Storage for file uploads:
// lib/storage/config.ts
export const storageConfig = {
buckets: {
chatFiles: 'chat-files',
avatars: 'avatars',
documents: 'documents'
},
limits: {
maxFileSize: 10 * 1024 * 1024, // 10MB
allowedMimeTypes: [
'image/jpeg',
'image/png',
'image/gif',
'application/pdf',
'text/plain',
'application/json'
]
}
}
// lib/storage/upload.ts
import { createClient } from '@/lib/supabase/client'
export async function uploadFile(
file: File,
bucket: string,
path?: string
) {
const supabase = createClient()
const fileName = path || `${Date.now()}-${file.name}`
const { data, error } = await supabase.storage
.from(bucket)
.upload(fileName, file, {
cacheControl: '3600',
upsert: false
})
if (error) throw error
// Get public URL
const { data: { publicUrl } } = supabase.storage
.from(bucket)
.getPublicUrl(data.path)
return { path: data.path, publicUrl }
}
export async function deleteFile(bucket: string, path: string) {
const supabase = createClient()
const { error } = await supabase.storage
.from(bucket)
.remove([path])
if (error) throw error
}
export async function getSignedUrl(
bucket: string,
path: string,
expiresIn = 3600
) {
const supabase = createClient()
const { data, error } = await supabase.storage
.from(bucket)
.createSignedUrl(path, expiresIn)
if (error) throw error
return data.signedUrl
}
Real-time Subscriptions
// hooks/use-realtime.ts
import { useEffect } from 'react'
import { createClient } from '@/lib/supabase/client'
export function useRealtimeMessages(chatId: string, onNewMessage: (message: any) => void) {
const supabase = createClient()
useEffect(() => {
const channel = supabase
.channel(`messages:${chatId}`)
.on(
'postgres_changes',
{
event: 'INSERT',
schema: 'public',
table: 'messages',
filter: `chat_id=eq.${chatId}`
},
(payload) => {
onNewMessage(payload.new)
}
)
.subscribe()
return () => {
supabase.removeChannel(channel)
}
}, [chatId])
}
// Presence for collaborative features
export function usePresence(chatId: string) {
const supabase = createClient()
const [presences, setPresences] = useState({})
useEffect(() => {
const channel = supabase.channel(`presence:${chatId}`)
channel
.on('presence', { event: 'sync' }, () => {
setPresences(channel.presenceState())
})
.on('presence', { event: 'join' }, ({ key, newPresences }) => {
console.log('User joined:', key, newPresences)
})
.on('presence', { event: 'leave' }, ({ key, leftPresences }) => {
console.log('User left:', key, leftPresences)
})
.subscribe(async (status) => {
if (status === 'SUBSCRIBED') {
await channel.track({
online_at: new Date().toISOString(),
user_id: user?.id,
})
}
})
return () => {
channel.untrack()
supabase.removeChannel(channel)
}
}, [chatId])
return presences
}
Query Patterns
// lib/queries/chat-queries.ts
import { createServerSupabaseClient } from '@/lib/supabase/server'
export async function getUserChats(userId: string) {
const supabase = await createServerSupabaseClient()
const { data, error } = await supabase
.from('chats')
.select(`
*,
messages (
id,
role,
content,
created_at
)
`)
.eq('user_id', userId)
.eq('is_archived', false)
.order('updated_at', { ascending: false })
.limit(20)
return { data, error }
}
export async function getChatWithMessages(chatId: string) {
const supabase = await createServerSupabaseClient()
const { data, error } = await supabase
.from('chats')
.select(`
*,
messages (
*
)
`)
.eq('id', chatId)
.single()
if (data) {
data.messages.sort((a, b) =>
new Date(a.created_at).getTime() - new Date(b.created_at).getTime()
)
}
return { data, error }
}
export async function searchMessages(query: string, userId: string) {
const supabase = await createServerSupabaseClient()
const { data, error } = await supabase
.rpc('search_messages', {
search_query: query,
user_id: userId
})
return { data, error }
}
Performance Optimization
Connection Pooling
// lib/supabase/pool.ts
import { createClient } from '@supabase/supabase-js'
class SupabasePool {
private pool: ReturnType<typeof createClient>[] = []
private currentIndex = 0
private maxConnections = 5
constructor() {
for (let i = 0; i < this.maxConnections; i++) {
this.pool.push(
createClient(
process.env.NEXT_PUBLIC_SUPABASE_URL!,
process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!
)
)
}
}
getClient() {
const client = this.pool[this.currentIndex]
this.currentIndex = (this.currentIndex + 1) % this.maxConnections
return client
}
}
export const supabasePool = new SupabasePool()
Caching Strategies
// lib/cache/supabase-cache.ts
import { unstable_cache } from 'next/cache'
export const getCachedUser = unstable_cache(
async (userId: string) => {
const supabase = await createServerSupabaseClient()
const { data } = await supabase
.from('users')
.select('*')
.eq('id', userId)
.single()
return data
},
['user'],
{
revalidate: 60 * 5, // 5 minutes
tags: ['user']
}
)
Security Best Practices
Never expose the service role key in client-side code. Always use it only in server-side API routes.
- Always enable RLS on all tables
- Use service role key only in server-side code
- Validate input before database operations
- Implement rate limiting for sensitive operations
- Use prepared statements to prevent SQL injection
- Audit sensitive operations with audit logs
- Encrypt sensitive data before storing
- Regular backups and point-in-time recovery
Monitoring
// lib/monitoring/supabase-metrics.ts
export async function trackDatabaseOperation({
operation,
table,
duration,
success,
error
}: DatabaseMetrics) {
// Log to your monitoring service
console.log({
timestamp: new Date().toISOString(),
operation,
table,
duration,
success,
error: error?.message
})
}
Next Steps
- API Routes - Complete API documentation
- Security - Security best practices
- Deployment - Production deployment guide
- Architecture - System design overview
Last updated on