Quick reference Supabase. Setup, authentication, database queries, real-time subscriptions, storage, Row Level Security, dan best practices untuk building modern apps.
# npm
npm install @supabase/supabase-js
# pnpm
pnpm add @supabase/supabase-js
# yarn
yarn add @supabase/supabase-js// 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)# .env.local
NEXT_PUBLIC_SUPABASE_URL=https://xxxxx.supabase.co
NEXT_PUBLIC_SUPABASE_ANON_KEY=eyJhbGc...
SUPABASE_SERVICE_ROLE_KEY=eyJhbGc... # Server-side only// lib/supabase-server.ts
import { createClient } from '@supabase/supabase-js'
export const supabaseAdmin = createClient(
process.env.NEXT_PUBLIC_SUPABASE_URL!,
process.env.SUPABASE_SERVICE_ROLE_KEY!, // Bypass RLS
{
auth: {
autoRefreshToken: false,
persistSession: false
}
}
)// Get all rows
const { data, error } = await supabase
.from('posts')
.select('*')
// Select specific columns
const { data } = await supabase
.from('posts')
.select('id, title, author')
// Filter by condition
const { data } = await supabase
.from('posts')
.select('*')
.eq('status', 'published')
.gt('views', 100)
// Join with other tables
const { data } = await supabase
.from('posts')
.select(`
id,
title,
author:users (name, email)
`)
// Pagination
const { data } = await supabase
.from('posts')
.select('*')
.range(0, 9) // First 10 items// Insert single row
const { data, error } = await supabase
.from('posts')
.insert({ title: 'New Post', content: 'Hello World' })
.select()
// Insert multiple rows
const { data } = await supabase
.from('posts')
.insert([
{ title: 'Post 1' },
{ title: 'Post 2' }
])
.select()
// Upsert (insert or update if exists)
const { data } = await supabase
.from('posts')
.upsert({ id: 1, title: 'Updated' })
.select()// Update by ID
const { data, error } = await supabase
.from('posts')
.update({ title: 'Updated Title' })
.eq('id', 1)
.select()
// Update with conditions
const { data } = await supabase
.from('posts')
.update({ status: 'archived' })
.lt('created_at', '2024-01-01')// Delete by ID
const { error } = await supabase
.from('posts')
.delete()
.eq('id', 1)
// Delete with conditions
const { error } = await supabase
.from('posts')
.delete()
.eq('status', 'draft')
.lt('created_at', '2024-01-01')// Comparison operators
.eq('column', value) // Equal
.neq('column', value) // Not equal
.gt('column', value) // Greater than
.gte('column', value) // Greater than or equal
.lt('column', value) // Less than
.lte('column', value) // Less than or equal
// Pattern matching
.like('column', '%pattern%') // Case-sensitive LIKE
.ilike('column', '%pattern%') // Case-insensitive LIKE
// In array
.in('column', [value1, value2])
// Null checks
.is('column', null)
.not('column', 'is', null)
// Full text search
.textSearch('column', 'keyword')
// Order & Limit
.order('created_at', { ascending: false })
.limit(10)const { data, error } = await supabase.auth.signUp({
email: 'user@example.com',
password: 'password123',
options: {
data: {
full_name: 'John Doe',
age: 25
}
}
})// Email & Password
const { data, error } = await supabase.auth.signInWithPassword({
email: 'user@example.com',
password: 'password123'
})
// Magic Link (Passwordless)
const { data, error } = await supabase.auth.signInWithOtp({
email: 'user@example.com',
options: {
emailRedirectTo: 'https://example.com/auth/callback'
}
})
// OAuth (Google, GitHub, etc.)
const { data, error } = await supabase.auth.signInWithOAuth({
provider: 'google',
options: {
redirectTo: 'https://example.com/auth/callback'
}
})const { error } = await supabase.auth.signOut()// Get user from session
const { data: { user } } = await supabase.auth.getUser()
// Get session
const { data: { session } } = await supabase.auth.getSession()const { data: { subscription } } = supabase.auth.onAuthStateChange(
(event, session) => {
console.log(event, session)
// events: 'SIGNED_IN', 'SIGNED_OUT', 'TOKEN_REFRESHED', etc.
}
)
// Cleanup
subscription.unsubscribe()// Send reset email
const { data, error } = await supabase.auth.resetPasswordForEmail(
'user@example.com',
{
redirectTo: 'https://example.com/reset-password'
}
)
// Update password (after reset)
const { data, error } = await supabase.auth.updateUser({
password: 'new_password'
})const { data, error } = await supabase.auth.updateUser({
data: {
full_name: 'Jane Doe',
avatar_url: 'https://example.com/avatar.jpg'
}
})// Subscribe to all changes
const subscription = supabase
.channel('posts-channel')
.on(
'postgres_changes',
{
event: '*', // '*', 'INSERT', 'UPDATE', 'DELETE'
schema: 'public',
table: 'posts'
},
(payload) => {
console.log('Change received!', payload)
}
)
.subscribe()
// Cleanup
subscription.unsubscribe()const subscription = supabase
.channel('post-1')
.on(
'postgres_changes',
{
event: 'UPDATE',
schema: 'public',
table: 'posts',
filter: 'id=eq.1'
},
(payload) => {
console.log('Post 1 updated!', payload)
}
)
.subscribe()const channel = supabase.channel('room-1')
// Listen to inserts
channel.on(
'postgres_changes',
{ event: 'INSERT', schema: 'public', table: 'messages' },
(payload) => console.log('New message', payload)
)
// Listen to presence
channel.on('presence', { event: 'sync' }, () => {
const state = channel.presenceState()
console.log('Online users:', state)
})
channel.subscribe()// Send message
await channel.send({
type: 'broadcast',
event: 'cursor-pos',
payload: { x: 100, y: 200 }
})
// Listen to broadcast
channel.on('broadcast', { event: 'cursor-pos' }, (payload) => {
console.log('Cursor moved:', payload)
})const channel = supabase.channel('room-1')
// Track user presence
await channel.subscribe(async (status) => {
if (status === 'SUBSCRIBED') {
await channel.track({
user_id: 'user-1',
online_at: new Date().toISOString()
})
}
})
// Listen to presence changes
channel.on('presence', { event: 'sync' }, () => {
const state = channel.presenceState()
console.log('Online:', state)
})
channel.on('presence', { event: 'join' }, ({ key, newPresences }) => {
console.log('User joined:', newPresences)
})
channel.on('presence', { event: 'leave' }, ({ key, leftPresences }) => {
console.log('User left:', leftPresences)
})// Upload file
const { data, error } = await supabase.storage
.from('avatars')
.upload('user-1.png', file, {
cacheControl: '3600',
upsert: true
})
// Upload with path
const { data, error } = await supabase.storage
.from('documents')
.upload('public/doc.pdf', file)// Download file
const { data, error } = await supabase.storage
.from('avatars')
.download('user-1.png')
// Get public URL
const { data } = supabase.storage
.from('avatars')
.getPublicUrl('user-1.png')
console.log(data.publicUrl)
// Create signed URL (private buckets)
const { data, error } = await supabase.storage
.from('private')
.createSignedUrl('secret.pdf', 60) // Valid for 60 secondsconst { data, error } = await supabase.storage
.from('avatars')
.list('public', {
limit: 100,
offset: 0,
sortBy: { column: 'name', order: 'asc' }
})// Delete single file
const { data, error } = await supabase.storage
.from('avatars')
.remove(['user-1.png'])
// Delete multiple files
const { data, error } = await supabase.storage
.from('avatars')
.remove(['user-1.png', 'user-2.png'])// Move file
const { data, error } = await supabase.storage
.from('avatars')
.move('old-path.png', 'new-path.png')
// Copy file
const { data, error } = await supabase.storage
.from('avatars')
.copy('source.png', 'destination.png')-- Enable RLS on table
ALTER TABLE posts ENABLE ROW LEVEL SECURITY;-- Allow anyone to read
CREATE POLICY "Public posts are viewable by everyone"
ON posts FOR SELECT
USING (true);
-- Users can only see their own data
CREATE POLICY "Users can view own posts"
ON posts FOR SELECT
USING (auth.uid() = user_id);
-- Users can insert their own data
CREATE POLICY "Users can insert own posts"
ON posts FOR INSERT
WITH CHECK (auth.uid() = user_id);
-- Users can update their own data
CREATE POLICY "Users can update own posts"
ON posts FOR UPDATE
USING (auth.uid() = user_id)
WITH CHECK (auth.uid() = user_id);
-- Users can delete their own data
CREATE POLICY "Users can delete own posts"
ON posts FOR DELETE
USING (auth.uid() = user_id);
-- Check user role
CREATE POLICY "Only admins can delete"
ON posts FOR DELETE
USING (
auth.uid() IN (
SELECT id FROM users WHERE role = 'admin'
)
);// Use service role key to bypass RLS
import { createClient } from '@supabase/supabase-js'
const supabaseAdmin = createClient(
process.env.NEXT_PUBLIC_SUPABASE_URL!,
process.env.SUPABASE_SERVICE_ROLE_KEY!
)
// Now queries bypass RLS
const { data } = await supabaseAdmin
.from('posts')
.select('*') // Returns all rows regardless of RLS# Using Supabase CLI
supabase functions new my-function// supabase/functions/my-function/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'
serve(async (req) => {
try {
// Get data from request
const { name } = await req.json()
// Initialize Supabase client
const supabase = createClient(
Deno.env.get('SUPABASE_URL') ?? '',
Deno.env.get('SUPABASE_ANON_KEY') ?? ''
)
// Query database
const { data, error } = await supabase
.from('users')
.select('*')
.eq('name', name)
if (error) throw error
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' } }
)
}
})supabase functions deploy my-function// From client
const { data, error } = await supabase.functions.invoke('my-function', {
body: { name: 'John' }
})
// With custom headers
const { data, error } = await supabase.functions.invoke('my-function', {
body: { name: 'John' },
headers: {
'Authorization': `Bearer ${token}`
}
})# Install Supabase CLI
npm install supabase --save-dev
# Login
npx supabase login
# Generate types
npx supabase gen types typescript --project-id YOUR_PROJECT_ID > types/database.ts// types/database.ts is auto-generated
import { Database } from '@/types/database'
// Create typed client
export const supabase = createClient<Database>(
supabaseUrl,
supabaseAnonKey
)
// Now queries are fully typed!
const { data } = await supabase
.from('posts') // Autocomplete available
.select('*')
// data is typed as Post[]import { Database } from '@/types/database'
type Post = Database['public']['Tables']['posts']['Row']
type PostInsert = Database['public']['Tables']['posts']['Insert']
type PostUpdate = Database['public']['Tables']['posts']['Update']
// Use in components
function PostList({ posts }: { posts: Post[] }) {
// ...
}// app/dashboard/page.tsx
import { createServerComponentClient } from '@supabase/auth-helpers-nextjs'
import { cookies } from 'next/headers'
import { redirect } from 'next/navigation'
export default async function Dashboard() {
const supabase = createServerComponentClient({ cookies })
const { data: { session } } = await supabase.auth.getSession()
if (!session) {
redirect('/login')
}
return <div>Protected content</div>
}import { useState, useEffect } from 'react'
function usePagination(table: string, pageSize = 10) {
const [data, setData] = useState([])
const [page, setPage] = useState(0)
const [loading, setLoading] = useState(false)
useEffect(() => {
async function fetchData() {
setLoading(true)
const { data } = await supabase
.from(table)
.select('*')
.range(page * pageSize, (page + 1) * pageSize - 1)
setData(data || [])
setLoading(false)
}
fetchData()
}, [page, table])
return { data, loading, page, setPage }
}async function toggleTodo(id: string, isComplete: boolean) {
// Update UI immediately
setTodos(prev =>
prev.map(t => t.id === id ? { ...t, is_complete: !isComplete } : t)
)
// Update database
const { error } = await supabase
.from('todos')
.update({ is_complete: !isComplete })
.eq('id', id)
// Revert on error
if (error) {
setTodos(prev =>
prev.map(t => t.id === id ? { ...t, is_complete } : t)
)
toast.error('Failed to update')
}
}import { useState, useEffect } from 'react'
import { useDebounce } from 'use-debounce'
function SearchPosts() {
const [query, setQuery] = useState('')
const [debouncedQuery] = useDebounce(query, 500)
const [results, setResults] = useState([])
useEffect(() => {
if (!debouncedQuery) {
setResults([])
return
}
async function search() {
const { data } = await supabase
.from('posts')
.select('*')
.ilike('title', `%${debouncedQuery}%`)
setResults(data || [])
}
search()
}, [debouncedQuery])
return (
<>
<input value={query} onChange={e => setQuery(e.target.value)} />
{results.map(post => <div key={post.id}>{post.title}</div>)}
</>
)
}select('id, title')).single() when fetching one rowcreated_at and updated_at timestamps# Supabase CLI
supabase init # Initialize Supabase project
supabase start # Start local Supabase
supabase stop # Stop local Supabase
supabase db reset # Reset local database
supabase db push # Push migrations to remote
supabase gen types typescript # Generate types
supabase functions new # Create new Edge Function
supabase functions deploy # Deploy Edge FunctionLogin atau daftar akun gratis untuk membaca cheat sheet ini.