import { createClient, SupabaseClient } from '@supabase/supabase-js'
// ============================================
// Types (Database definitions)
// ============================================
export type Json =
| string
| number
| boolean
| null
| { [key: string]: Json | undefined }
| Json[]
export interface Database {
public: {
Tables: {
todos: {
Row: {
id: number
user_id: string
task: string
is_completed: boolean
inserted_at: string
}
Insert: {
id?: number
user_id: string
task: string
is_completed?: boolean
inserted_at?: string
}
Update: {
id?: number
user_id?: string
task?: string
is_completed?: boolean
inserted_at?: string
}
}
profiles: {
Row: {
id: string
username: string | null
avatar_url: string | null
updated_at: string | null
}
Insert: {
id: string
username?: string | null
avatar_url?: string | null
updated_at?: string | null
}
Update: {
id?: string
username?: string | null
avatar_url?: string | null
updated_at?: string | null
}
}
}
}
}
// ============================================
// Client Setup
// ============================================
const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL!
const supabaseAnonKey = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!
// Simple client (client-side)
export const supabase = createClient<Database>(supabaseUrl, supabaseAnonKey)
// Service role client (server-side only - bypasses RLS)
// export const supabaseAdmin = createClient<Database>(
// supabaseUrl,
// process.env.SUPABASE_SERVICE_ROLE_KEY!
// )
// ============================================
// Auth Helpers
// ============================================
export const authService = {
async signInWithEmail(email: string) {
// Magic link login
const { data, error } = await supabase.auth.signInWithOtp({
email,
options: {
emailRedirectTo: 'http://localhost:3000/auth/callback',
},
})
return { data, error }
},
async signInWithGoogle() {
const { data, error } = await supabase.auth.signInWithOAuth({
provider: 'google',
})
return { data, error }
},
async signOut() {
const { error } = await supabase.auth.signOut()
return { error }
},
async getUser() {
const { data: { user } } = await supabase.auth.getUser()
return user
},
}
// ============================================
// Data Services
// ============================================
export const todoService = {
async getTodos() {
const { data, error } = await supabase
.from('todos')
.select('*')
.order('is_completed', { ascending: true })
.order('inserted_at', { ascending: false })
if (error) throw error
return data
},
async createTodo(task: string) {
const user = await authService.getUser()
if (!user) throw new Error('Not authenticated')
const { data, error } = await supabase
.from('todos')
.insert({ task, user_id: user.id })
.select()
.single()
if (error) throw error
return data
},
async toggleTodo(id: number, isCompleted: boolean) {
const { data, error } = await supabase
.from('todos')
.update({ is_completed: isCompleted })
.eq('id', id)
.select()
.single()
if (error) throw error
return data
},
async deleteTodo(id: number) {
const { error } = await supabase
.from('todos')
.delete()
.eq('id', id)
if (error) throw error
return true
}
}
// ============================================
// Real-time Subscriptions
// ============================================
export function subscribeToTodos(callback: (payload: any) => void) {
const subscription = supabase
.channel('public:todos')
.on(
'postgres_changes',
{ event: '*', schema: 'public', table: 'todos' },
callback
)
.subscribe()
return () => {
supabase.removeChannel(subscription)
}
}
// ============================================
// Storage Helpers
// ============================================
export const storageService = {
async uploadAvatar(file: File) {
const user = await authService.getUser()
if (!user) throw new Error('Not authenticated')
const fileExt = file.name.split('.').pop()
const filePath = `${user.id}.${fileExt}`
const { error: uploadError } = await supabase.storage
.from('avatars')
.upload(filePath, file, { upsert: true })
if (uploadError) throw uploadError
// Get public URL
const { data } = supabase.storage
.from('avatars')
.getPublicUrl(filePath)
return data.publicUrl
}
}