Supabase Fundamentals
Supabase is an open-source Firebase alternative. It provides a PostgreSQL database, Auth, realtime subscriptions, storage, and edge functions — all accessible via a type-safe JavaScript client.
Client Setup & Type Generation
# Install
npm install @supabase/supabase-js
# For Next.js (SSR/SSG with cookie-based auth)
npm install @supabase/ssr
# Generate TypeScript types from your database schema
npx supabase login
npx supabase gen types typescript --project-id YOUR_PROJECT_ID > src/types/database.types.ts
# Or from local Supabase dev instance
npx supabase gen types typescript --local > src/types/database.types.ts// src/lib/supabase.ts — browser client
import { createClient } from '@supabase/supabase-js';
import type { Database } from '@/types/database.types';
export const supabase = createClient<Database>(
process.env.NEXT_PUBLIC_SUPABASE_URL!,
process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!
);
// src/lib/supabase-server.ts — server-side client (Next.js App Router)
import { createServerClient } from '@supabase/ssr';
import { cookies } from 'next/headers';
export function createSupabaseServerClient() {
const cookieStore = 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)
);
},
},
}
);
}Authentication — Email, OAuth, Magic Link
// Email + password
const { data, error } = await supabase.auth.signUp({
email: 'user@example.com',
password: 'password123',
options: {
data: { full_name: 'Alice Smith', role: 'user' }, // stored in raw_user_meta_data
emailRedirectTo: 'https://myapp.com/auth/callback',
},
});
const { data, error } = await supabase.auth.signInWithPassword({
email: 'user@example.com',
password: 'password123',
});
// OAuth providers (GitHub, Google, Discord, etc.)
await supabase.auth.signInWithOAuth({
provider: 'github',
options: {
redirectTo: 'https://myapp.com/auth/callback',
scopes: 'read:user user:email',
},
});
// Magic link (passwordless)
await supabase.auth.signInWithOtp({
email: 'user@example.com',
options: { emailRedirectTo: 'https://myapp.com/auth/callback' },
});
// Phone OTP
await supabase.auth.signInWithOtp({ phone: '+1234567890' });
await supabase.auth.verifyOtp({ phone: '+1234567890', token: '123456', type: 'sms' });
// Get current session & user
const { data: { session } } = await supabase.auth.getSession();
const { data: { user } } = await supabase.auth.getUser();
// Listen to auth state changes
supabase.auth.onAuthStateChange((event, session) => {
// events: SIGNED_IN, SIGNED_OUT, TOKEN_REFRESHED, USER_UPDATED, PASSWORD_RECOVERY
if (event === 'SIGNED_IN') console.log('User:', session?.user);
});
await supabase.auth.signOut();Database Queries
// SELECT with joins
const { data: posts, error } = await supabase
.from('posts')
.select(`
id, title, body, created_at,
author:profiles(id, username, avatar_url),
tags(name)
`)
.eq('status', 'published')
.order('created_at', { ascending: false })
.range(0, 19) // pagination (rows 0–19)
.returns<PostWithAuthor[]>(); // narrow the inferred type
// Filters
.eq('id', userId) // WHERE id = $1
.neq('status', 'deleted') // WHERE status != $1
.gt('views', 100) / .gte('views', 100)
.in('role', ['admin', 'moderator'])
.ilike('name', '%alice%') // case-insensitive LIKE
.is('deleted_at', null)
.not('status', 'eq', 'hidden')
.or('role.eq.admin,role.eq.moderator')
.filter('metadata->country', 'eq', 'US') // JSONB field
// INSERT — returns inserted row
const { data: post, error } = await supabase
.from('posts')
.insert({ title: 'Hello', body: 'World', author_id: user.id })
.select()
.single();
// UPSERT
await supabase
.from('profiles')
.upsert({ id: user.id, username: 'alice', updated_at: new Date().toISOString() },
{ onConflict: 'id' });
// UPDATE
await supabase
.from('posts')
.update({ status: 'published', published_at: new Date().toISOString() })
.eq('id', postId)
.eq('author_id', user.id); // always scope to current user
// DELETE
await supabase.from('posts').delete().eq('id', postId);
// Count
const { count } = await supabase
.from('posts')
.select('*', { count: 'exact', head: true })
.eq('status', 'published');Row Level Security (RLS)
-- Enable RLS — without this, all rows are accessible to anon
ALTER TABLE posts ENABLE ROW LEVEL SECURITY;
-- SELECT: users read only their own posts
CREATE POLICY "users_read_own_posts"
ON posts FOR SELECT
USING (auth.uid() = author_id);
-- SELECT: anyone reads published posts
CREATE POLICY "public_read_published"
ON posts FOR SELECT
USING (status = 'published');
-- INSERT: author_id must match the authenticated user
CREATE POLICY "users_insert_own_posts"
ON posts FOR INSERT
WITH CHECK (auth.uid() = author_id);
-- UPDATE: only author can update their own post
CREATE POLICY "users_update_own_posts"
ON posts FOR UPDATE
USING (auth.uid() = author_id)
WITH CHECK (auth.uid() = author_id);
-- Admin bypass — check custom claim from JWT metadata
CREATE POLICY "admins_full_access"
ON posts
USING ((auth.jwt() ->> 'user_metadata')::jsonb ->> 'role' = 'admin');
-- Helper function — reuse in multiple policies
CREATE FUNCTION is_admin() RETURNS boolean AS $$
SELECT (auth.jwt() ->> 'user_metadata')::jsonb ->> 'role' = 'admin';
$$ LANGUAGE sql STABLE;Keep your own version of these notes — editable, searchable, and organised by your stack.
Start free