Skip to content

SUPABASE โ€” Backend-as-a-Service Platform

Last reviewed: 2026-06-16

Purpose: Supabase backend-as-a-service platform configuration and setup reference.

Overview

Supabase is an open-source Firebase alternative that provides a full backend stack on top of PostgreSQL. It offers a managed Postgres database, authentication, real-time subscriptions, storage, and Edge Functions โ€” all accessible via REST, GraphQL, or WebSocket APIs.

This article covers project creation, database schema management, authentication (including Row Level Security), real-time subscriptions, storage buckets, and Edge Functions.


1. Project Creation

Via Dashboard

  1. Go to https://supabase.com/dashboard and sign in.
  2. Click New project.
  3. Enter:
  4. Name โ€” e.g. my-app-prod
  5. Database password โ€” strong password (store securely)
  6. Region โ€” choose the region closest to your users
  7. Pricing plan โ€” Free (up to 2 projects) or Pro ($25/month)
  8. Click Create new project โ€” provisioning takes 1โ€“3 minutes.

Via CLI

# Install Supabase CLI (macOS)
brew install supabase/tap/supabase

# Or with npm
npm install -g supabase

# Login
supabase login

# Initialize a local project
supabase init

# Link to an existing remote project
supabase link --project-ref <project-ref>

The project reference (<project-ref>) can be found in the project settings URL: https://supabase.com/dashboard/project/<project-ref>/settings/general.

Connection Details

From the project dashboard โ†’ Settings โ†’ Database:

Parameter Value
Host db.<project-ref>.supabase.co
Port 5432
User postgres
Password (the one you set during creation)
Database postgres

Connection string for external tools:

postgresql://postgres:<password>@db.<project-ref>.supabase.co:5432/postgres

โš ๏ธ Security: Never commit the password or connection string to version control. Use environment variables or a secrets manager.


2. PostgreSQL Database Setup

Supabase gives you a full PostgreSQL instance. You can manage it via:

  • SQL Editor in the Supabase Dashboard
  • psql CLI client
  • GUI tools (PgAdmin, DBeaver, DataGrip)
  • Prisma / Knex / Drizzle ORMs

Initial Schema Example

-- Enable UUID generation
CREATE EXTENSION IF NOT EXISTS "uuid-ossp";

-- Profiles table (extends Supabase auth.users)
CREATE TABLE public.profiles (
  id         UUID PRIMARY KEY REFERENCES auth.users(id) ON DELETE CASCADE,
  username   TEXT UNIQUE NOT NULL,
  avatar_url TEXT,
  updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);

-- Create a trigger to auto-create a profile on sign-up
CREATE OR REPLACE FUNCTION public.handle_new_user()
RETURNS TRIGGER AS $$
BEGIN
  INSERT INTO public.profiles (id, username)
  VALUES (NEW.id, COALESCE(NEW.raw_user_meta_data->>'username', SPLIT_PART(NEW.email, '@', 1)));
  RETURN NEW;
END;
$$ LANGUAGE plpgsql SECURITY DEFINER;

CREATE TRIGGER on_auth_user_created
  AFTER INSERT ON auth.users
  FOR EACH ROW EXECUTE FUNCTION public.handle_new_user();

Useful Extensions

CREATE EXTENSION IF NOT EXISTS "pgcrypto";     -- Cryptographic functions
CREATE EXTENSION IF NOT EXISTS "pg_stat_statements"; -- Query performance
CREATE EXTENSION IF NOT EXISTS "postgis";      -- Geospatial data (Pro plan)
CREATE EXTENSION IF NOT EXISTS "vector";       -- pgvector for embeddings

3. Authentication

Supabase Auth supports multiple sign-in methods out of the box: email/password, magic link, OAuth providers, phone, and anonymous.

Enabling Providers

In the Dashboard โ†’ Authentication โ†’ Providers:

Provider Configuration Required
Email / Password Built-in (no extra config)
GitHub OAuth app credentials
Google OAuth client ID + secret
Magic Link Enabled by default with email
Phone / SMS Twilio credentials
Apple / Discord / etc. Respective OAuth credentials

Sign-Up / Sign-In Examples

Email & Password

// Client-side (Supabase JS client)
import { createClient } from '@supabase/supabase-js'

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

// Sign up
const { data, error } = await supabase.auth.signUp({
  email: 'user@example.com',
  password: 'secure-password',
  options: {
    data: {
      username: 'johndoe',
    }
  }
})

// Sign in
const { data, error } = await supabase.auth.signInWithPassword({
  email: 'user@example.com',
  password: 'secure-password'
})

// Sign out
const { error } = await supabase.auth.signOut()

OAuth (GitHub)

// Redirect the user to GitHub's consent screen
const { data, error } = await supabase.auth.signInWithOAuth({
  provider: 'github',
  options: {
    redirectTo: 'https://myapp.com/auth/callback'
  }
})
const { data, error } = await supabase.auth.signInWithOtp({
  email: 'user@example.com',
  options: {
    emailRedirectTo: 'https://myapp.com/auth/callback'
  }
})

Managing Users

  • Dashboard โ†’ Authentication โ†’ Users โ€” view, search, delete users.
  • Admin API via the service_role key (server-side only):
// Server-side โ€” never expose service_role key to the browser
const supabaseAdmin = createClient(
  process.env.SUPABASE_URL,
  process.env.SUPABASE_SERVICE_ROLE_KEY, // service_role, not anon
  { auth: { autoRefreshToken: false, persistSession: false } }
)

// List users
const { data, error } = await supabaseAdmin.auth.admin.listUsers()

// Delete a user
const { data, error } = await supabaseAdmin.auth.admin.deleteUser(userId)

4. Row Level Security (RLS)

RLS is the cornerstone of Supabase security. It enforces data access policies directly in PostgreSQL, ensuring users can only read/write rows they are authorized to โ€” even when queries come from the client-side JavaScript SDK.

Enabling RLS on a Table

ALTER TABLE public.profiles ENABLE ROW LEVEL SECURITY;
ALTER TABLE public.posts     ENABLE ROW LEVEL SECURITY;
ALTER TABLE public.comments  ENABLE ROW LEVEL SECURITY;

Essential Helper Functions

Supabase provides built-in SQL functions accessible inside RLS policies:

  • auth.uid() โ€” returns the UUID of the currently authenticated user
  • auth.email() โ€” returns the email of the authenticated user
  • auth.role() โ€” returns the role (e.g., authenticated, anon)
-- Helper: Check if the user owns the resource
CREATE OR REPLACE FUNCTION public.is_owner(resource_owner_id UUID)
RETURNS BOOLEAN AS $$
  SELECT resource_owner_id = auth.uid();
$$ LANGUAGE sql STABLE;

RLS Policy Examples

Profiles Table

-- 1. Users can read ANY profile (public profiles)
CREATE POLICY "Anyone can read profiles"
  ON public.profiles
  FOR SELECT
  USING (true);

-- 2. Users can update ONLY their own profile
CREATE POLICY "Users can update own profile"
  ON public.profiles
  FOR UPDATE
  USING (auth.uid() = id)
  WITH CHECK (auth.uid() = id);

-- 3. Insert is handled by the trigger; but if needed:
CREATE POLICY "Users can insert own profile"
  ON public.profiles
  FOR INSERT
  WITH CHECK (auth.uid() = id);

Posts Table (Ownership)

CREATE TABLE public.posts (
  id        UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
  title     TEXT NOT NULL,
  content   TEXT NOT NULL,
  author_id UUID NOT NULL REFERENCES auth.users(id) ON DELETE CASCADE,
  published BOOLEAN NOT NULL DEFAULT false,
  created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);

ALTER TABLE public.posts ENABLE ROW LEVEL SECURITY;

-- Authors can read their own posts; anyone can read published posts
CREATE POLICY "Anyone can read published posts"
  ON public.posts
  FOR SELECT
  USING (published = true OR auth.uid() = author_id);

-- Authors can create posts
CREATE POLICY "Authenticated users can create posts"
  ON public.posts
  FOR INSERT
  WITH CHECK (auth.role() = 'authenticated' AND auth.uid() = author_id);

-- Authors can update their own posts
CREATE POLICY "Authors can update own posts"
  ON public.posts
  FOR UPDATE
  USING (auth.uid() = author_id);

-- Authors can delete their own posts
CREATE POLICY "Authors can delete own posts"
  ON public.posts
  FOR DELETE
  USING (auth.uid() = author_id);

Comments Table (with Join on Author)

CREATE TABLE public.comments (
  id         UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
  post_id    UUID NOT NULL REFERENCES public.posts(id) ON DELETE CASCADE,
  author_id  UUID NOT NULL REFERENCES auth.users(id) ON DELETE CASCADE,
  body       TEXT NOT NULL,
  created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);

ALTER TABLE public.comments ENABLE ROW LEVEL SECURITY;

-- Anyone can read comments on published posts
CREATE POLICY "Anyone can read comments on published posts"
  ON public.comments
  FOR SELECT
  USING (
    EXISTS (
      SELECT 1 FROM public.posts
      WHERE posts.id = comments.post_id
      AND (posts.published = true OR posts.author_id = auth.uid())
    )
  );

-- Authenticated users can comment
CREATE POLICY "Authenticated users can comment"
  ON public.comments
  FOR INSERT
  WITH CHECK (auth.role() = 'authenticated' AND auth.uid() = author_id);

-- Users can edit their own comments (10-minute window)
CREATE POLICY "Users can edit own comments within 10 minutes"
  ON public.comments
  FOR UPDATE
  USING (auth.uid() = author_id AND NOW() - created_at < INTERVAL '10 minutes');

Testing RLS Policies

Use the Supabase SQL Editor to test policies as specific users:

-- Switch to the user role (requires session setup)
-- Better approach: test via the Dashboard's Policy Tester or the client SDK

-- Direct check: what does auth.uid() return?
SELECT auth.uid();  -- NULL unless called within an authenticated session

For client-side testing:

// Supabase JS client automatically injects the session token
// RLS policies run server-side โ€” no client config needed
const { data, error } = await supabase
  .from('posts')
  .select('*')
  .eq('published', true)  -- public posts only

5. Real-Time Subscriptions

Supabase Realtime broadcasts database changes to connected clients over WebSocket.

Enable Realtime on a Table

Via Dashboard โ†’ Database โ†’ Replication โ†’ enable replica identity for the table.

-- Or via SQL:
ALTER TABLE public.posts REPLICA IDENTITY FULL;

Then subscribe in the client:

// Subscribe to all changes on the 'posts' table
const subscription = supabase
  .channel('posts-channel')
  .on(
    'postgres_changes',
    {
      event: '*',           -- 'INSERT' | 'UPDATE' | 'DELETE' | '*'
      schema: 'public',
      table: 'posts',
      filter: 'author_id=eq.550e8400-e29b-41d4-a716-446655440000' -- optional filter
    },
    (payload) => {
      console.log('Change received!', payload)
      // payload.new    -> new row (for INSERT/UPDATE)
      // payload.old    -> old row (for UPDATE/DELETE)
      // payload.event_type -> 'INSERT' | 'UPDATE' | 'DELETE'
    }
  )
  .subscribe()

// Unsubscribe when done
supabase.removeChannel(subscription)

Presence & Broadcast

Realtime also supports presence (who's online) and arbitrary message broadcast:

const channel = supabase.channel('room-1', {
  config: {
    presence: { key: authUser.id },
  }
})

// Track presence
channel.on('presence', { event: 'sync' }, () => {
  const state = channel.presenceState()
  console.log('Online users:', state)
})

channel.on('presence', { event: 'join' }, ({ key, newPresences }) => {
  console.log('User joined:', key)
})

// Subscribe
channel.subscribe(async (status) => {
  if (status === 'SUBSCRIBED') {
    await channel.track({
      user_name: authUser.email,
      online_at: new Date().toISOString()
    })
  }
})

6. Storage Buckets

Supabase Storage provides S3-compatible object storage.

Creating a Bucket

Via Dashboard โ†’ Storage โ†’ New bucket.

Or via SQL / API:

-- SQL in SQL Editor
INSERT INTO storage.buckets (id, name, public)
VALUES ('avatars', 'avatars', true);
// Via JS client
const { data, error } = await supabase.storage.createBucket('documents', {
  public: false,
  allowedMimeTypes: ['application/pdf'],
  fileSizeLimit: 10485760, -- 10 MB
})

Uploading & Downloading Files

// Upload
const avatarFile = event.target.files[0]
const { data, error } = await supabase.storage
  .from('avatars')
  .upload(`public/${userId}.jpg`, avatarFile, {
    cacheControl: '3600',
    upsert: true
  })

// Download
const { data, error } = await supabase.storage
  .from('avatars')
  .download(`public/${userId}.jpg`)

// Get public URL (only works for public buckets)
const { data } = supabase.storage
  .from('avatars')
  .getPublicUrl(`public/${userId}.jpg`)
// Returns: https://<project-ref>.supabase.co/storage/v1/object/public/avatars/public/<userId>.jpg

// List files
const { data, error } = await supabase.storage
  .from('avatars')
  .list('public/', {
    limit: 100,
    offset: 0,
    sortBy: { column: 'name', order: 'asc' }
  })

Storage RLS Policies

Storage has its own RLS policies, managed via SQL on the storage.objects table:

-- Allow authenticated users to read objects in the 'avatars' bucket
CREATE POLICY "Authenticated users can read avatars"
  ON storage.objects
  FOR SELECT
  USING (
    bucket_id = 'avatars'
    AND auth.role() = 'authenticated'
  );

-- Allow users to upload to their own folder
CREATE POLICY "Users can upload own avatar"
  ON storage.objects
  FOR INSERT
  WITH CHECK (
    bucket_id = 'avatars'
    AND auth.role() = 'authenticated'
    AND (storage.foldername(name))[1] = auth.uid()::text
  );

-- Allow users to delete their own files
CREATE POLICY "Users can delete own files"
  ON storage.objects
  FOR DELETE
  USING (
    bucket_id = 'avatars'
    AND auth.uid()::text = (storage.foldername(name))[1]
  );

7. Edge Functions

Supabase Edge Functions are server-side TypeScript/JavaScript functions running on Deno. They replace traditional backend endpoints.

Creating an Edge Function

# Create a new function locally
supabase functions new hello-world

# Files created in: supabase/functions/hello-world/index.ts

Example function (supabase/functions/hello-world/index.ts):

import { serve } from 'https://deno.land/std@0.177.0/http/server.ts'

console.log('Hello from Edge Function!')

serve(async (req) => {
  const { name } = await req.json()
  const data = {
    message: `Hello ${name || 'World'}!`,
  }

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

Using the Supabase Client Inside a Function

import { serve } from 'https://deno.land/std@0.177.0/http/server.ts'
import { createClient } from 'https://esm.sh/@supabase/supabase-js@2'

serve(async (req) => {
  const supabase = createClient(
    Deno.env.get('SUPABASE_URL')!,
    Deno.env.get('SUPABASE_SERVICE_ROLE_KEY')! // service_role โ€” bypasses RLS
  )

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

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

Deploying

# Deploy all functions
supabase functions deploy

# Deploy a specific function
supabase functions deploy hello-world

# Set environment variables
supabase secrets set MY_API_KEY=xyz123
supabase secrets unset MY_API_KEY
supabase secrets list

Edge Function URL format:

https://<project-ref>.functions.supabase.co/hello-world


8. Complete RLS Policy Example

Below is a full, production-realistic schema for a multi-tenant blog with comments, likes, and per-user authorization.

-- ============================================================
-- SCHEMA: Multi-tenant Blog with RLS
-- ============================================================

-- Extensions
CREATE EXTENSION IF NOT EXISTS "uuid-ossp";

-- Tables
CREATE TABLE public.organizations (
  id         UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
  name       TEXT NOT NULL,
  slug       TEXT UNIQUE NOT NULL,
  created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);

CREATE TABLE public.organization_members (
  organization_id UUID NOT NULL REFERENCES public.organizations(id) ON DELETE CASCADE,
  user_id         UUID NOT NULL REFERENCES auth.users(id) ON DELETE CASCADE,
  role            TEXT NOT NULL DEFAULT 'member' CHECK (role IN ('admin', 'member', 'viewer')),
  joined_at       TIMESTAMPTZ NOT NULL DEFAULT NOW(),
  PRIMARY KEY (organization_id, user_id)
);

CREATE TABLE public.articles (
  id              UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
  organization_id UUID NOT NULL REFERENCES public.organizations(id) ON DELETE CASCADE,
  title           TEXT NOT NULL,
  body            TEXT NOT NULL,
  author_id       UUID NOT NULL REFERENCES auth.users(id),
  published       BOOLEAN NOT NULL DEFAULT false,
  created_at      TIMESTAMPTZ NOT NULL DEFAULT NOW(),
  updated_at      TIMESTAMPTZ NOT NULL DEFAULT NOW()
);

CREATE TABLE public.likes (
  article_id UUID NOT NULL REFERENCES public.articles(id) ON DELETE CASCADE,
  user_id    UUID NOT NULL REFERENCES auth.users(id) ON DELETE CASCADE,
  created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
  PRIMARY KEY (article_id, user_id)
);

-- Enable RLS on all tables
ALTER TABLE public.organizations         ENABLE ROW LEVEL SECURITY;
ALTER TABLE public.organization_members  ENABLE ROW LEVEL SECURITY;
ALTER TABLE public.articles              ENABLE ROW LEVEL SECURITY;
ALTER TABLE public.likes                 ENABLE ROW LEVEL SECURITY;

-- ============================================================
-- RLS POLICIES
-- ============================================================

-- --- ORGANIZATIONS ---
-- Anyone can see organizations
CREATE POLICY "Anyone can view organizations"
  ON public.organizations FOR SELECT USING (true);

-- Only org admins can update
CREATE POLICY "Admins can update organization"
  ON public.organizations FOR UPDATE
  USING (
    EXISTS (
      SELECT 1 FROM public.organization_members
      WHERE organization_id = organizations.id
        AND user_id = auth.uid()
        AND role = 'admin'
    )
  );

-- --- ORGANIZATION MEMBERS ---
-- Members can view other members in their org
CREATE POLICY "Members can view org members"
  ON public.organization_members FOR SELECT
  USING (
    organization_id IN (
      SELECT organization_id FROM public.organization_members
      WHERE user_id = auth.uid()
    )
  );

-- Admins can add/remove members
CREATE POLICY "Admins can manage members"
  ON public.organization_members FOR INSERT
  WITH CHECK (
    organization_id IN (
      SELECT organization_id FROM public.organization_members
      WHERE user_id = auth.uid() AND role = 'admin'
    )
  );

CREATE POLICY "Admins can remove members"
  ON public.organization_members FOR DELETE
  USING (
    organization_id IN (
      SELECT organization_id FROM public.organization_members
      WHERE user_id = auth.uid() AND role = 'admin'
    )
  );

-- --- ARTICLES ---
-- Published articles visible to everyone; draft articles visible only to org members
CREATE POLICY "Read articles"
  ON public.articles FOR SELECT
  USING (
    published = true
    OR (
      published = false
      AND organization_id IN (
        SELECT organization_id FROM public.organization_members
        WHERE user_id = auth.uid()
      )
    )
  );

-- Org members (admin, member) can create articles
CREATE POLICY "Org members can create articles"
  ON public.articles FOR INSERT
  WITH CHECK (
    auth.role() = 'authenticated'
    AND organization_id IN (
      SELECT organization_id FROM public.organization_members
      WHERE user_id = auth.uid() AND role IN ('admin', 'member')
    )
    AND author_id = auth.uid()
  );

-- Authors or org admins can update
CREATE POLICY "Update articles"
  ON public.articles FOR UPDATE
  USING (
    auth.uid() = author_id
    OR organization_id IN (
      SELECT organization_id FROM public.organization_members
      WHERE user_id = auth.uid() AND role = 'admin'
    )
  );

-- Authors or org admins can delete
CREATE POLICY "Delete articles"
  ON public.articles FOR DELETE
  USING (
    auth.uid() = author_id
    OR organization_id IN (
      SELECT organization_id FROM public.organization_members
      WHERE user_id = auth.uid() AND role = 'admin'
    )
  );

-- --- LIKES ---
-- Anyone can see likes on published articles
CREATE POLICY "Read likes on published articles"
  ON public.likes FOR SELECT
  USING (
    EXISTS (
      SELECT 1 FROM public.articles
      WHERE articles.id = likes.article_id
      AND (articles.published = true OR articles.author_id = auth.uid())
    )
  );

-- Authenticated users can like
CREATE POLICY "Authenticated users can like"
  ON public.likes FOR INSERT
  WITH CHECK (
    auth.role() = 'authenticated'
    AND user_id = auth.uid()
  );

-- Users can unlike (delete their own like)
CREATE POLICY "Users can unlike"
  ON public.likes FOR DELETE
  USING (user_id = auth.uid());

-- ============================================================
-- TRIGGERS
-- ============================================================

-- Auto-update updated_at
CREATE OR REPLACE FUNCTION public.update_updated_at()
RETURNS TRIGGER AS $$
BEGIN
  NEW.updated_at = NOW();
  RETURN NEW;
END;
$$ LANGUAGE plpgsql;

CREATE TRIGGER articles_updated_at
  BEFORE UPDATE ON public.articles
  FOR EACH ROW EXECUTE FUNCTION public.update_updated_at();

Testing the RLS Setup

To verify that the RLS policies work as expected:

  1. Create two test users via the Dashboard โ†’ Authentication โ†’ Users โ†’ Invite.
  2. Assign them to different organizations via the SQL Editor (INSERT into organization_members).
  3. Use the Supabase client or the API playground in the Dashboard to run queries as each user.
  4. Verify:
  5. User A cannot see User B's draft articles
  6. A non-member cannot create an article in the organization
  7. An admin can update any article in their org
  8. A user can only delete their own likes

9. Security Best Practices

Practice Details
Always enable RLS Without RLS, anyone with the anon key can read/write all data
Use the anon key on clients The anon key is safe for browser/mobile clients โ€” it's the RLS policies that enforce security
Keep the service_role key secret The service_role key bypasses all RLS โ€” never expose it client-side
Validate input server-side Even with RLS, validate data in Edge Functions or server endpoints
Use HTTPS Supabase API enforces TLS; ensure your client uses https://
Set up rate limiting Use Supabase's built-in rate limiting or a reverse proxy
Monitor auth events Enable webhooks for Auth events (sign-up, sign-in, etc.)

10. Useful CLI Commands

# Status
supabase status

# Start local development
supabase start

# Stop local development
supabase stop

# Run database migrations
supabase db push

# Generate TypeScript types from your schema
supabase gen types typescript --linked > types/supabase.ts

# Pull schema changes from remote
supabase db pull

# View logs
supabase functions logs hello-world

References