Supabase
02 / 03

Supabase Fundamentals

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