Skip to main content
Explore database examples covering CRUD operations, relations, Row Level Security, triggers, functions, and advanced queries.

CRUD Operations

Select Data

import { createClient } from '@supabase/supabase-js'

const supabase = createClient(url, key)

// Select all columns
const { data, error } = await supabase
  .from('posts')
  .select('*')

// Select specific columns
const { data } = await supabase
  .from('posts')
  .select('id, title, author_id')

// Select with filters
const { data } = await supabase
  .from('posts')
  .select('*')
  .eq('author_id', userId)
  .order('created_at', { ascending: false })
  .limit(10)

// Select with text search
const { data } = await supabase
  .from('posts')
  .select('*')
  .textSearch('title', 'supabase', {
    type: 'websearch',
    config: 'english',
  })

// Select with range
const { data } = await supabase
  .from('posts')
  .select('*')
  .range(0, 9) // Get first 10 rows

Insert Data

// Insert single row
const { data, error } = await supabase
  .from('posts')
  .insert({
    title: 'Hello World',
    content: 'This is my first post',
    author_id: userId,
  })
  .select()
  .single()

// Insert multiple rows
const { data, error } = await supabase
  .from('posts')
  .insert([
    { title: 'Post 1', content: 'Content 1' },
    { title: 'Post 2', content: 'Content 2' },
  ])
  .select()

// Upsert (insert or update)
const { data, error } = await supabase
  .from('posts')
  .upsert({
    id: 1,
    title: 'Updated Title',
    content: 'Updated content',
  })
  .select()

Update Data

// Update with filter
const { data, error } = await supabase
  .from('posts')
  .update({ title: 'New Title' })
  .eq('id', postId)
  .select()

// Update multiple rows
const { data, error } = await supabase
  .from('posts')
  .update({ status: 'published' })
  .in('id', [1, 2, 3])
  .select()

// Increment a value
const { data, error } = await supabase.rpc('increment_views', {
  post_id: postId,
})

Delete Data

// Delete with filter
const { error } = await supabase
  .from('posts')
  .delete()
  .eq('id', postId)

// Delete multiple rows
const { error } = await supabase
  .from('posts')
  .delete()
  .in('id', [1, 2, 3])

// Delete all rows (be careful!)
const { error } = await supabase
  .from('posts')
  .delete()
  .neq('id', 0) // This deletes everything

Relations and Joins

One-to-Many

// Get posts with author information
const { data } = await supabase
  .from('posts')
  .select(`
    *,
    author:profiles(
      id,
      username,
      avatar_url
    )
  `)

// Get user with all their posts
const { data } = await supabase
  .from('profiles')
  .select(`
    *,
    posts(
      id,
      title,
      created_at
    )
  `)
  .eq('id', userId)
  .single()

Many-to-Many

// Get posts with tags (through junction table)
const { data } = await supabase
  .from('posts')
  .select(`
    *,
    post_tags(
      tags(
        id,
        name
      )
    )
  `)

// Get tag with all posts
const { data } = await supabase
  .from('tags')
  .select(`
    *,
    post_tags(
      posts(
        id,
        title
      )
    )
  `)
  .eq('id', tagId)

Nested Relations

// Get posts with author and comments
const { data } = await supabase
  .from('posts')
  .select(`
    *,
    author:profiles(*),
    comments(
      id,
      content,
      author:profiles(
        username,
        avatar_url
      )
    )
  `)

Filtering

Comparison Operators

// Equal
const { data } = await supabase.from('posts').select().eq('status', 'published')

// Not equal
const { data } = await supabase.from('posts').select().neq('status', 'draft')

// Greater than
const { data } = await supabase.from('posts').select().gt('views', 1000)

// Greater than or equal
const { data } = await supabase.from('posts').select().gte('views', 1000)

// Less than
const { data } = await supabase.from('posts').select().lt('views', 1000)

// Less than or equal
const { data } = await supabase.from('posts').select().lte('views', 1000)

// In array
const { data } = await supabase.from('posts').select().in('status', ['published', 'archived'])

// Pattern matching
const { data } = await supabase.from('posts').select().like('title', '%supabase%')

// Case-insensitive pattern matching
const { data } = await supabase.from('posts').select().ilike('title', '%supabase%')

// Is null
const { data } = await supabase.from('posts').select().is('deleted_at', null)

Combining Filters

// AND (default)
const { data } = await supabase
  .from('posts')
  .select()
  .eq('status', 'published')
  .gt('views', 100)

// OR
const { data } = await supabase
  .from('posts')
  .select()
  .or('status.eq.published,status.eq.archived')

// NOT
const { data } = await supabase
  .from('posts')
  .select()
  .not('status', 'eq', 'draft')

Aggregation and Counting

// Count rows
const { count } = await supabase
  .from('posts')
  .select('*', { count: 'exact', head: true })

// Count with filter
const { count } = await supabase
  .from('posts')
  .select('*', { count: 'exact', head: true })
  .eq('status', 'published')

// Sum, avg, min, max (using database functions)
const { data } = await supabase.rpc('get_post_stats', {
  author_id: userId,
})

Row Level Security (RLS)

Enable RLS

-- Enable RLS on a table
alter table posts enable row level security;

Basic Policies

-- Allow users to read their own posts
create policy "Users can view their own posts"
  on posts for select
  using (auth.uid() = author_id);

-- Allow users to insert their own posts
create policy "Users can create posts"
  on posts for insert
  with check (auth.uid() = author_id);

-- Allow users to update their own posts
create policy "Users can update their own posts"
  on posts for update
  using (auth.uid() = author_id);

-- Allow users to delete their own posts
create policy "Users can delete their own posts"
  on posts for delete
  using (auth.uid() = author_id);

Advanced Policies

-- Public read, authenticated write
create policy "Public posts are viewable by everyone"
  on posts for select
  using (true);

create policy "Authenticated users can create posts"
  on posts for insert
  with check (auth.role() = 'authenticated');

-- Admins can do anything
create policy "Admins have full access"
  on posts for all
  using (
    exists (
      select 1 from profiles
      where id = auth.uid()
      and role = 'admin'
    )
  );

-- Collaborative editing
create policy "Collaborators can edit posts"
  on posts for update
  using (
    auth.uid() = author_id
    or exists (
      select 1 from post_collaborators
      where post_id = posts.id
      and user_id = auth.uid()
    )
  );

Database Functions

Create Function

-- Increment views counter
create or replace function increment_views(post_id bigint)
returns void
language plpgsql
security definer
as $$
begin
  update posts
  set views = views + 1
  where id = post_id;
end;
$$;

Call Function from Client

// Call function
const { data, error } = await supabase.rpc('increment_views', {
  post_id: 123,
})

// Function that returns data
const { data, error } = await supabase.rpc('get_popular_posts', {
  min_views: 1000,
})

Triggers

Auto-Update Timestamp

-- Create trigger function
create or replace function update_updated_at_column()
returns trigger
language plpgsql
as $$
begin
  new.updated_at = now();
  return new;
end;
$$;

-- Create trigger
create trigger update_posts_updated_at
  before update on posts
  for each row
  execute function update_updated_at_column();
-- Auto-create profile on user signup
create or replace function handle_new_user()
returns trigger
language plpgsql
security definer
as $$
begin
  insert into public.profiles (id, username, avatar_url)
  values (
    new.id,
    new.raw_user_meta_data->>'username',
    new.raw_user_meta_data->>'avatar_url'
  );
  return new;
end;
$$;

create trigger on_auth_user_created
  after insert on auth.users
  for each row
  execute function handle_new_user();
-- Add tsvector column
alter table posts
  add column fts tsvector
  generated always as (
    to_tsvector('english', coalesce(title, '') || ' ' || coalesce(content, ''))
  ) stored;

-- Create index
create index posts_fts_idx on posts using gin(fts);

Search from Client

// Full-text search
const { data } = await supabase
  .from('posts')
  .select('*')
  .textSearch('fts', 'supabase & postgres', {
    type: 'websearch',
    config: 'english',
  })

Transactions

// Use a database function for transactions
const { data, error } = await supabase.rpc('transfer_funds', {
  from_account: 123,
  to_account: 456,
  amount: 100,
})

// SQL function for transaction
/*
create or replace function transfer_funds(
  from_account bigint,
  to_account bigint,
  amount numeric
)
returns void
language plpgsql
as $$
begin
  -- Deduct from sender
  update accounts
  set balance = balance - amount
  where id = from_account;
  
  -- Add to receiver
  update accounts
  set balance = balance + amount
  where id = to_account;
  
  -- Insert transaction record
  insert into transactions (from_account, to_account, amount)
  values (from_account, to_account, amount);
end;
$$;
*/

Pagination

// Offset pagination
const pageSize = 10
const page = 2

const { data, error } = await supabase
  .from('posts')
  .select('*')
  .range((page - 1) * pageSize, page * pageSize - 1)

// Cursor-based pagination
const { data, error } = await supabase
  .from('posts')
  .select('*')
  .gt('created_at', lastPostTimestamp)
  .order('created_at', { ascending: false })
  .limit(10)

Real-time Subscriptions

// Subscribe to all changes
const channel = supabase
  .channel('posts')
  .on(
    'postgres_changes',
    { event: '*', schema: 'public', table: 'posts' },
    (payload) => {
      console.log('Change received!', payload)
    }
  )
  .subscribe()

// Subscribe to specific events
const channel = supabase
  .channel('posts')
  .on(
    'postgres_changes',
    { event: 'INSERT', schema: 'public', table: 'posts' },
    (payload) => {
      console.log('New post:', payload.new)
    }
  )
  .on(
    'postgres_changes',
    { event: 'UPDATE', schema: 'public', table: 'posts' },
    (payload) => {
      console.log('Updated post:', payload.new)
    }
  )
  .on(
    'postgres_changes',
    { event: 'DELETE', schema: 'public', table: 'posts' },
    (payload) => {
      console.log('Deleted post:', payload.old)
    }
  )
  .subscribe()

// Unsubscribe
supabase.removeChannel(channel)

Example Apps

Todo App

Complete CRUD operations with RLS

Employee Database

Advanced queries and relations

Blog Example

Full-text search and pagination

Next.js Slack Clone

Real-time subscriptions

Next Steps

Database Docs

Learn more about Postgres

Todo Tutorial

Build your first app

Row Level Security

Secure your data

Realtime Examples

Explore real-time patterns