Skip to content
supabase-auth-storage-realtime-core logo

Supabase Auth + Storage + Realtime Core

supabase-auth-storage-realtime-core

Implement Supabase Auth (signUp, signIn, OAuth, session management),

SKILL.md

Full skill instructions

Supabase Auth + Storage + Realtime Core

Overview

Implement the three pillars that turn a Supabase database into a full application backend: user authentication (email/​password, OAuth, magic links, session lifecycle), file storage (uploads, downloads, signed URLs, bucket-level RLS policies), and real-time subscriptions (Postgres change events, client-to-client broadcast, presence tracking). Every operation integrates with Row-Level Security through auth.uid().

Prerequisites

  • Supabase project created at supabase.com/​dashboard
  • @supabase/​supabase-js v2 installed (npm install @supabase/​supabase-js)
  • SUPABASE_URL and SUPABASE_ANON_KEY available from project Settings > API
  • For Python: pip install supabase (wraps postgrest-py, gotrue-py, storage3, realtime-py)

Instructions

Step 1: Auth — User Registration, Login, and OAuth

Initialize the client and implement the three primary auth flows: email/​password, OAuth provider, and passwordless magic link.

TypeScript

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

const supabase = createClient(
  process.env.SUPABASE_URL!,
  process.env.SUPABASE_ANON_KEY!
)

// ── Sign up a new user ──
const { data: signUpData, error: signUpError } = await supabase.auth.signUp({
  email: '[email protected]',
  password: 'secure-password-123',
  options: {
    data: { username: 'newuser', full_name: 'New User' },  // → raw_user_meta_data
  },
})
// If email confirmation enabled: data.user exists but data.session is null
// If email confirmation disabled: both data.user and data.session are present

// ── Sign in with password ──
const { data: signInData, error: signInError } = await supabase.auth.signInWithPassword({
  email: '[email protected]',
  password: 'secure-password-123',
})
const { user, session } = signInData
// session.access_token → JWT for authenticated API calls

// ── Sign in with OAuth (Google) ──
const { data: oauthData, error: oauthError } = await supabase.auth.signInWithOAuth({
  provider: 'google',
  options: {
    redirectTo: 'https://myapp.com/​auth/​callback',
    queryParams: { access_type: 'offline', prompt: 'consent' },
  },
})
// Redirect user to oauthData.url in the browser

// ── Sign in with GitHub ──
const { data, error } = await supabase.auth.signInWithOAuth({
  provider: 'github',
  options: { redirectTo: 'https://myapp.com/​auth/​callback' },
})

// ── Passwordless magic link ──
const { error: otpError } = await supabase.auth.signInWithOtp({
  email: '[email protected]',
  options: { emailRedirectTo: 'https://myapp.com/​auth/​callback' },
})

// ── Handle OAuth/​magic link callback (in /​auth/​callback route) ──
const { data: { session: cbSession }, error: cbError } =
  await supabase.auth.exchangeCodeForSession(code)

Session management — every app needs these:

// Get current session (reads from local storage, no network call)
const { data: { session } } = await supabase.auth.getSession()

// Get current user (validates JWT against server)
const { data: { user } } = await supabase.auth.getUser()

// Listen for auth state changes — critical for reactive UIs
const { data: { subscription } } = supabase.auth.onAuthStateChange(
  (event, session) => {
    // event: 'SIGNED_IN' | 'SIGNED_OUT' | 'TOKEN_REFRESHED' | 'USER_UPDATED'
    //        'INITIAL_SESSION' | 'PASSWORD_RECOVERY' | 'MFA_CHALLENGE_VERIFIED'
    console.log('Auth event:', event, session?.user?.email)
  }
)
// Clean up when component unmounts
subscription.unsubscribe()

// Sign out (clears session from storage)
await supabase.auth.signOut()

// Password reset (sends email with reset link)
await supabase.auth.resetPasswordForEmail('[email protected]', {
  redirectTo: 'https://myapp.com/​auth/​reset-password',
})

Python

from supabase import create_client

supabase = create_client(
    "https://your-project.supabase.co",
    "your-anon-key"
)

# Sign up
result = supabase.auth.sign_up({
    "email": "[email protected]",
    "password": "secure-password-123",
    "options": {"data": {"username": "newuser"}},
})

# Sign in with password
result = supabase.auth.sign_in_with_password({
    "email": "[email protected]",
    "password": "secure-password-123",
})
access_token = result.session.access_token

# Get current session
session = supabase.auth.get_session()

# Sign out
supabase.auth.sign_out()

Step 2: Storage — Upload, Download, and Secure with Bucket Policies

Supabase Storage organizes files into buckets. Public buckets serve files via CDN URLs; private buckets require signed URLs or authenticated requests.

TypeScript

// ── Upload a file ──
const file = new File(['hello world'], 'hello.txt', { type: 'text/​plain' })
const { data, error } = await supabase.storage
  .from('avatars')  // bucket name
  .upload('user123/​avatar.png', file, {
    cacheControl: '3600',
    upsert: false,       // true → overwrite existing
    contentType: 'image/​png',
  })
// data.path → 'user123/​avatar.png'

// ── Download a file ──
const { data: blob, error: dlError } = await supabase.storage
  .from('avatars')
  .download('user123/​avatar.png')
// blob is a Blob object — use URL.createObjectURL(blob) for display

// ── Get public URL (public buckets only, no auth required) ──
const { data: { publicUrl } } = supabase.storage
  .from('avatars')
  .getPublicUrl('user123/​avatar.png')
// publicUrl → 'https://<project>.supabase.co/​storage/​v1/​object/​public/​avatars/​user123/​avatar.png'

// ── Create signed URL (private buckets, time-limited access) ──
const { data: signedUrlData, error: signError } = await supabase.storage
  .from('documents')
  .createSignedUrl('reports/​q4-2025.pdf', 3600)  // expires in 1 hour
// signedUrlData.signedUrl → one-time use URL with token parameter

// ── List files in a path ──
const { data: files, error: listError } = await supabase.storage
  .from('documents')
  .list('reports', {
    limit: 100,
    offset: 0,
    sortBy: { column: 'name', order: 'asc' },
  })

// ── Delete files ──
const { error: removeError } = await supabase.storage
  .from('documents')
  .remove(['reports/​old-report.pdf', 'reports/​draft.docx'])

Bucket RLS policies — enforce access control in SQL migrations:

-- Create buckets (run in a migration or SQL editor)
INSERT INTO storage.buckets (id, name, public)
VALUES ('avatars', 'avatars', true);   -- public: anyone can read

INSERT INTO storage.buckets (id, name, public)
VALUES ('documents', 'documents', false);  -- private: signed URLs only

-- Allow authenticated users to upload to their own folder
-- Convention: store files at <user_id>/​filename.ext
CREATE POLICY "avatar_upload"
  ON storage.objects FOR INSERT
  WITH CHECK (
    bucket_id = 'avatars'
    AND auth.uid()::text = (storage.foldername(name))[1]
  );

-- Allow anyone to view avatars (public bucket)
CREATE POLICY "avatar_public_read"
  ON storage.objects FOR SELECT
  USING (bucket_id = 'avatars');

-- Allow users to manage only their own documents (all operations)
CREATE POLICY "documents_user_crud"
  ON storage.objects FOR ALL
  USING (
    bucket_id = 'documents'
    AND auth.uid()::text = (storage.foldername(name))[1]
  )
  WITH CHECK (
    bucket_id = 'documents'
    AND auth.uid()::text = (storage.foldername(name))[1]
  );

-- Allow users to delete only files they uploaded
CREATE POLICY "documents_owner_delete"
  ON storage.objects FOR DELETE
  USING (
    bucket_id = 'documents'
    AND auth.uid() = owner
  );

Python

# Upload
with open("report.pdf", "rb") as f:
    result = supabase.storage.from_("documents").upload(
        "user123/​report.pdf", f,
        {"content-type": "application/​pdf", "cache-control": "3600"}
    )

# Download
data = supabase.storage.from_("documents").download("user123/​report.pdf")

# Public URL
url = supabase.storage.from_("avatars").get_public_url("user123/​avatar.png")

# Signed URL (3600 seconds)
result = supabase.storage.from_("documents").create_signed_url(
    "user123/​report.pdf", 3600
)
signed_url = result["signedURL"]

# List files
files = supabase.storage.from_("documents").list("user123")

# Delete
supabase.storage.from_("documents").remove(["user123/​old-file.pdf"])

Step 3: Realtime — Postgres Changes, Broadcast, and Presence

Supabase Realtime provides three channel types: database change listeners, client-to-client broadcast, and presence tracking for online status.

Postgres Changes (listen to INSERT/​UPDATE/​DELETE on tables):

// Subscribe to new messages in a chat table
const channel = supabase
  .channel('chat-room')
  .on(
    'postgres_changes',
    {
      event: 'INSERT',
      schema: 'public',
      table: 'messages',
    },
    (payload) => {
      console.log('New message:', payload.new)
      // payload.new → full row data
      // payload.old → null for INSERT
    }
  )
  .on(
    'postgres_changes',
    {
      event: 'UPDATE',
      schema: 'public',
      table: 'messages',
      filter: 'room_id=eq.42',  // server-side filter
    },
    (payload) => {
      console.log('Updated:', payload.old, '→', payload.new)
    }
  )
  .on(
    'postgres_changes',
    {
      event: 'DELETE',
      schema: 'public',
      table: 'messages',
    },
    (payload) => {
      console.log('Deleted:', payload.old)
      // payload.new → null for DELETE
    }
  )
  .subscribe((status) => {
    console.log('Channel status:', status)
    // 'SUBSCRIBED' | 'TIMED_OUT' | 'CLOSED' | 'CHANNEL_ERROR'
  })

// Enable Realtime on the table (required one-time setup in SQL)
// ALTER PUBLICATION supabase_realtime ADD TABLE messages;

// Unsubscribe when done
supabase.removeChannel(channel)

RLS integration — Realtime respects row-level security:

-- Only receive changes for rows the user owns
CREATE POLICY "users_own_messages"
  ON messages FOR SELECT
  USING (auth.uid() = user_id);

-- The Realtime listener will only fire for rows passing this policy

Broadcast (client-to-client, no database involved):

const room = supabase.channel('collab-room')

// Listen for cursor movements from other users
room.on('broadcast', { event: 'cursor-move' }, ({ payload }) => {
  console.log(`User ${payload.userId} at (${payload.x}, ${payload.y})`)
})

// Subscribe first, then send
room.subscribe((status) => {
  if (status === 'SUBSCRIBED') {
    // Send cursor position to all other clients
    room.send({
      type: 'broadcast',
      event: 'cursor-move',
      payload: { userId: 'abc', x: 120, y: 340 },
    })
  }
})

Presence (track who is online):

const room = supabase.channel('app-presence')

// Sync event fires whenever the presence state changes
room.on('presence', { event: 'sync' }, () => {
  const state = room.presenceState()
  // state → { 'user-abc': [{ user_id: 'abc', online_at: '...' }], ... }
  const onlineUsers = Object.keys(state)
  console.log('Online:', onlineUsers.length, 'users')
})

room.on('presence', { event: 'join' }, ({ key, newPresences }) => {
  console.log('Joined:', key, newPresences)
})

room.on('presence', { event: 'leave' }, ({ key, leftPresences }) => {
  console.log('Left:', key, leftPresences)
})

// Subscribe and track this user's presence
room.subscribe(async (status) => {
  if (status === 'SUBSCRIBED') {
    await room.track({
      user_id: currentUser.id,
      username: currentUser.email,
      online_at: new Date().toISOString(),
    })
  }
})

// Untrack when leaving (e.g., on component unmount)
await room.untrack()

Python Realtime:

# Python realtime uses callbacks (requires running event loop)
def handle_insert(payload):
    print("New row:", payload["new"])

channel = supabase.channel("room")
channel.on_postgres_changes(
    event="INSERT",
    schema="public",
    table="messages",
    callback=handle_insert,
)
channel.subscribe()

# When done
supabase.remove_channel(channel)

Output

  • Auth: user registration, password login, OAuth flow (Google/​GitHub), magic link, session lifecycle with onAuthStateChange, password reset
  • Storage: file upload/​download, public URLs for CDN-served assets, time-limited signed URLs for private files, bucket-level RLS policies using auth.uid() and storage.foldername()
  • Realtime: Postgres change subscriptions with server-side filters, broadcast channels for client-to-client messaging, presence tracking for online status

Error Handling

ErrorCauseSolution
AuthApiError: User already registeredDuplicate email signupUse signInWithPassword or check existence first
AuthApiError: Invalid login credentialsWrong email or passwordVerify credentials; check email confirmation status
AuthApiError: Email not confirmedUser has not clicked confirmation linkResend with resend({ type: 'signup', email })
StorageApiError: Bucket not foundBucket does not existCreate via dashboard or INSERT INTO storage.buckets
StorageApiError: new row violates row-level securityRLS policy blocking the operationVerify storage.objects policies match the user and bucket
StorageApiError: The resource already existsFile exists and upsert: falseSet upsert: true to overwrite or use a unique path
Realtime: channel error or TIMED_OUTNetwork issues or Realtime not enabledCheck ALTER PUBLICATION supabase_realtime ADD TABLE <name>
Realtime: too many channelsExceeded concurrent channel limitUnsubscribe unused channels with removeChannel()

Examples

Full auth + protected upload flow:

// 1. Sign in
const { data: { session } } = await supabase.auth.signInWithPassword({
  email: '[email protected]',
  password: 'secure-password-123',
})

// 2. Upload avatar to user's folder (RLS enforces ownership)
const { data } = await supabase.storage
  .from('avatars')
  .upload(`${session.user.id}/​avatar.png`, file, { upsert: true })

// 3. Get public URL for display
const { data: { publicUrl } } = supabase.storage
  .from('avatars')
  .getPublicUrl(`${session.user.id}/​avatar.png`)

// 4. Subscribe to profile changes in real time
const channel = supabase
  .channel('profile-updates')
  .on('postgres_changes', {
    event: 'UPDATE',
    schema: 'public',
    table: 'profiles',
    filter: `id=eq.${session.user.id}`,
  }, (payload) => {
    console.log('Profile updated:', payload.new)
  })
  .subscribe()

Resources

Next Steps

For common Supabase errors and debugging, see supabase-common-errors. For database queries and CRUD operations, see supabase-crud-core.

More skills from jeremylongshore

openrouter-compliance-review logo
jeremylongshore/claude-code-plugins-plus-skills

openrouter-compliance-review

Review OpenRouter integration for regulatory compliance (SOC2, GDPR,

2.8K 0
View
maintainx-data-handling logo
jeremylongshore/claude-code-plugins-plus-skills

maintainx-data-handling

Data synchronization, ETL patterns, and data management for MaintainX.

2.8K 0
View
groq-core-workflow-a logo
jeremylongshore/claude-code-plugins-plus-skills

groq-core-workflow-a

Execute Groq primary workflow: chat completions with tool use and JSON

2.8K 0
View
validating-csrf-protection logo
jeremylongshore/claude-code-plugins-plus-skills

validating-csrf-protection

Validate CSRF protection implementations for security gaps. Use when

2.8K 0
View
api-contract logo
jeremylongshore/claude-code-plugins-plus-skills

api-contract

Configure this skill should be used when the user asks about "API contract",

2.8K 0
View
ideogram-incident-runbook logo
jeremylongshore/claude-code-plugins-plus-skills

ideogram-incident-runbook

Execute Ideogram incident response with triage, mitigation, and postmortem.

2.8K 0
View
gamma-sdk-patterns logo
jeremylongshore/claude-code-plugins-plus-skills

gamma-sdk-patterns

Reusable patterns for the Gamma REST API (no SDK exists).

2.8K 0
View
deepgram-sdk-patterns logo
jeremylongshore/claude-code-plugins-plus-skills

deepgram-sdk-patterns

Apply production-ready Deepgram SDK patterns for TypeScript and Python.

2.8K 0
View
openrouter-upgrade-migration logo
jeremylongshore/claude-code-plugins-plus-skills

openrouter-upgrade-migration

Migrate to OpenRouter from direct provider APIs or upgrade between SDK/model

2.8K 0
View
openrouter-common-errors logo
jeremylongshore/claude-code-plugins-plus-skills

openrouter-common-errors

Diagnose and fix common OpenRouter API errors. Use when encountering

2.8K 0
View
linear-cost-tuning logo
jeremylongshore/claude-code-plugins-plus-skills

linear-cost-tuning

Optimize Linear API usage, reduce unnecessary calls, and maximize

2.8K 0
View

Popular AI tools

Kaiber logo
Video

Kaiber

Generate, edit, and beat-sync AI video with leading models in one workspace.

Paid
View
Vimcal logo
Productivity

Vimcal

The world's fastest calendar for remote work

Free
View

Transform Your Design with AI Designer by ImgCreator.ai

Freemium
View
Akool AI logo
Content & writing

Akool AI

Revolutionizing Video Production with AI-Powered Creativity

Paid
View

Extend an image past the frame and let AI fill the new aspect ratio.

Freemium
View
StarByFace logo
Security

StarByFace

Discover your celebrity doppelgänger with StarByFace!

Free
View
C

ChainClarity explains 700+ crypto whitepapers in plain English, with layered summaries, comparisons, research tools, alerts, and a $4.99 Pro plan.

Freemium
View
Opus Clip logo
Coding & apps

Opus Clip

Opus.ai: Revolutionize Your Web Experience

Free
View