Supabase
03 / 03

Realtime & Storage

Realtime & Storage

Supabase Realtime broadcasts database changes over WebSockets. Storage provides S3-compatible file storage with RLS policies. Edge Functions run Deno-based serverless code close to users.

Realtime Subscriptions

// Subscribe to database changes (requires replication enabled on the table)
const channel = supabase
  .channel('posts-changes')
  .on(
    'postgres_changes',
    {
      event: '*',                     // INSERT | UPDATE | DELETE | *
      schema: 'public',
      table: 'posts',
      filter: `author_id=eq.${userId}`,  // server-side filter
    },
    (payload) => {
      switch (payload.eventType) {
        case 'INSERT': addPost(payload.new as Post); break;
        case 'UPDATE': updatePost(payload.new as Post); break;
        case 'DELETE': removePost((payload.old as Post).id); break;
      }
    }
  )
  .subscribe((status, err) => {
    if (status === 'SUBSCRIBED') console.log('Realtime connected');
    if (status === 'CHANNEL_ERROR') console.error('Realtime error', err);
  });

// Cleanup on unmount
return () => { supabase.removeChannel(channel); };

// Broadcast — send custom events to other clients in the same channel
const presenceChannel = supabase.channel('room:lobby');
presenceChannel
  .on('broadcast', { event: 'cursor-move' }, ({ payload }) => {
    updateCursor(payload.userId, payload.x, payload.y);
  })
  .subscribe();

// Send broadcast
await presenceChannel.send({
  type: 'broadcast',
  event: 'cursor-move',
  payload: { userId: user.id, x: 100, y: 200 },
});

Presence — Track Online Users

// Presence — know who is online in real time
const room = supabase.channel('room:project-1', {
  config: { presence: { key: user.id } },
});

room
  .on('presence', { event: 'sync' }, () => {
    const state = room.presenceState<{ name: string; avatar: string }>();
    const onlineUsers = Object.values(state).flat();
    setOnlineUsers(onlineUsers);
  })
  .on('presence', { event: 'join' }, ({ key, newPresences }) => {
    console.log('User joined:', key, newPresences);
  })
  .on('presence', { event: 'leave' }, ({ key, leftPresences }) => {
    console.log('User left:', key, leftPresences);
  })
  .subscribe(async (status) => {
    if (status === 'SUBSCRIBED') {
      await room.track({ name: user.name, avatar: user.avatar });
    }
  });

// Untrack when leaving
await room.untrack();

Storage — File Uploads & Access

// Upload a file
const { data, error } = await supabase.storage
  .from('avatars')
  .upload(`${userId}/avatar.webp`, file, {
    contentType: 'image/webp',
    upsert: true,               // overwrite if exists
    cacheControl: '3600',       // Cache-Control max-age
  });

// Get public URL (bucket must be set to public)
const { data: { publicUrl } } = supabase.storage
  .from('avatars')
  .getPublicUrl(`${userId}/avatar.webp`);

// Signed URL (for private buckets, expires after N seconds)
const { data: { signedUrl } } = await supabase.storage
  .from('private-docs')
  .createSignedUrl(`${userId}/report.pdf`, 3600);

// List files in a folder
const { data: files } = await supabase.storage
  .from('avatars')
  .list(userId, { limit: 20, offset: 0, sortBy: { column: 'created_at', order: 'desc' } });

// Delete
await supabase.storage.from('avatars').remove([`${userId}/old-avatar.webp`]);

// Move / rename
await supabase.storage.from('avatars').move(`${userId}/temp.png`, `${userId}/avatar.png`);

// Storage RLS — in Supabase dashboard SQL editor
// CREATE POLICY "user_owns_avatar"
//   ON storage.objects FOR ALL
//   USING (auth.uid()::text = (storage.foldername(name))[1]);

Edge Functions & Database Functions

// supabase/functions/send-email/index.ts — Deno edge function
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: Request) => {
  const supabase = createClient(
    Deno.env.get('SUPABASE_URL')!,
    Deno.env.get('SUPABASE_SERVICE_ROLE_KEY')!   // service role bypasses RLS
  );

  const { to, subject, body } = await req.json();
  // ... call Resend, SendGrid, etc.

  return new Response(JSON.stringify({ ok: true }), {
    headers: { 'Content-Type': 'application/json' },
  });
});

// Deploy
// npx supabase functions deploy send-email

// Call from client
const { data, error } = await supabase.functions.invoke('send-email', {
  body: { to: 'alice@example.com', subject: 'Welcome!', body: 'Hello' },
});

// Database function + trigger — auto-create profile on signup
-- CREATE FUNCTION public.handle_new_user()
-- RETURNS trigger AS $$
-- BEGIN
--   INSERT INTO public.profiles (id, email, created_at)
--   VALUES (new.id, new.email, now());
--   RETURN new;
-- END;
-- $$ LANGUAGE plpgsql SECURITY DEFINER;
--
-- CREATE TRIGGER on_auth_user_created
--   AFTER INSERT ON auth.users
--   FOR EACH ROW EXECUTE PROCEDURE public.handle_new_user();

Keep your own version of these notes — editable, searchable, and organised by your stack.

Start free