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
- Go to https://supabase.com/dashboard and sign in.
- Click New project.
- Enter:
- Name โ e.g.
my-app-prod - Database password โ strong password (store securely)
- Region โ choose the region closest to your users
- Pricing plan โ Free (up to 2 projects) or Pro ($25/month)
- 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:
โ ๏ธ 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 |
| 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'
}
})
Magic Link
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_rolekey (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 userauth.email()โ returns the email of the authenticated userauth.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.
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:
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:
- Create two test users via the Dashboard โ Authentication โ Users โ Invite.
- Assign them to different organizations via the SQL Editor (INSERT into
organization_members). - Use the Supabase client or the API playground in the Dashboard to run queries as each user.
- Verify:
- User A cannot see User B's draft articles
- A non-member cannot create an article in the organization
- An admin can update any article in their org
- 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