๐ŸŒ Detecting your locationโ€ฆ

Supabase Guide 2026: Postgres, Auth, Real-Time and Vector Search

โฑ๏ธ8 min read  ยท  1,547 words

Supabase is the open-source Firebase alternative that gives you a PostgreSQL database, authentication, real-time subscriptions, edge functions, and storage โ€” all in one platform. In 2026, Supabase powers startups and enterprises alike with its developer-friendly DX and powerful PostgreSQL foundation. This guide covers everything from project setup to production.

Supabase vs Firebase: Which Should You Pick?

This is the comparison most people are actually making, so it is worth settling before the tutorial starts.

  Supabase Firebase
Database PostgreSQL — relational, SQL Firestore — NoSQL documents
Queries Full SQL, joins, views, triggers Limited; no joins
Self-hosting Yes, fully open source No
Vendor lock-in Low — it is a Postgres database you can export High
Real-time Postgres replication Native, more mature
Offline sync Not built in Strong — a genuine advantage
Vector search Built in via pgvector Requires a separate service
Free tier 500 MB database, 2 projects Generous reads/writes

Pick Supabase if your data has relationships, you want to write SQL, or you care about being able to leave. Pick Firebase if you need offline-first mobile sync, which is the one thing Supabase genuinely does not do well.

The deeper argument for Supabase is that it is Postgres. If the project outgrows the platform, you take a standard database with you. Leaving Firestore means rewriting your data layer.

What Supabase Provides

  • PostgreSQL database โ€” full Postgres with direct SQL access
  • Auto-generated REST API โ€” PostgREST turns your tables into REST endpoints
  • Real-time subscriptions โ€” listen to table changes via WebSocket
  • Authentication โ€” email, OAuth (Google, GitHub, Apple), phone OTP
  • Storage โ€” file uploads with CDN and row-level access policies
  • Edge Functions โ€” Deno-based serverless functions at the edge
  • Vector search โ€” pgvector extension for AI embeddings

Setup

npm install @supabase/supabase-js

# Or create project at supabase.com (free tier: 500MB DB, 1GB storage)

# Environment variables
SUPABASE_URL=https://xyzcompany.supabase.co
SUPABASE_ANON_KEY=eyJhbGciOiJIUzI1NiIs...  # public, safe for frontend
SUPABASE_SERVICE_ROLE_KEY=eyJ...              # private, server only!

Database Operations

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

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

// SELECT
const { data: users, error } = await supabase
  .from('users')
  .select('id, name, email, created_at')
  .eq('active', true)
  .order('created_at', { ascending: false })
  .limit(10);

// SELECT with relations (foreign table)
const { data: posts } = await supabase
  .from('posts')
  .select(`
    id,
    title,
    content,
    users(name, email),
    categories(name)
  `)
  .eq('published', true);

// INSERT
const { data, error } = await supabase
  .from('posts')
  .insert({
    title: 'My Post',
    content: 'Content here',
    author_id: userId,
    published: false,
  })
  .select()
  .single();

// UPDATE
const { data: updated } = await supabase
  .from('posts')
  .update({ published: true })
  .eq('id', postId)
  .select()
  .single();

// DELETE (soft delete via RLS is better)
await supabase.from('posts').delete().eq('id', postId);

// UPSERT
await supabase
  .from('user_settings')
  .upsert({ user_id: userId, theme: 'dark', language: 'en' })
  .onConflict('user_id');

Row Level Security (RLS)

-- Enable RLS
ALTER TABLE posts ENABLE ROW LEVEL SECURITY;

-- Users can read all published posts
CREATE POLICY "Anyone can read published posts"
  ON posts FOR SELECT
  USING (published = true);

-- Users can only see their own draft posts
CREATE POLICY "Users see own drafts"
  ON posts FOR SELECT
  USING (author_id = auth.uid() AND published = false);

-- Users can only insert/update their own posts
CREATE POLICY "Users manage own posts"
  ON posts FOR ALL
  USING (author_id = auth.uid());

-- Admin bypass (service role key bypasses RLS)
-- Use service role key in server-side code for admin operations

Authentication

// Sign up
const { data, error } = await supabase.auth.signUp({
  email: 'alice@example.com',
  password: 'securepassword',
  options: {
    data: { full_name: 'Alice Chen' }  // stored in user metadata
  }
});

// Sign in
const { data: session } = await supabase.auth.signInWithPassword({
  email: 'alice@example.com',
  password: 'securepassword'
});

// OAuth (Google, GitHub, etc.)
await supabase.auth.signInWithOAuth({
  provider: 'github',
  options: {
    redirectTo: `${window.location.origin}/auth/callback`
  }
});

// Get current user
const { data: { user } } = await supabase.auth.getUser();

// Listen to auth state changes
supabase.auth.onAuthStateChange((event, session) => {
  if (event === 'SIGNED_IN') setUser(session?.user ?? null);
  if (event === 'SIGNED_OUT') setUser(null);
});

// Sign out
await supabase.auth.signOut();

// Server-side auth (Next.js)
import { createServerClient } from '@supabase/ssr';
import { cookies } from 'next/headers';

export function createServerSupabase() {
  const cookieStore = cookies();
  return createServerClient(
    process.env.SUPABASE_URL!,
    process.env.SUPABASE_ANON_KEY!,
    { cookies: { get: (name) => cookieStore.get(name)?.value } }
  );
}

Real-Time Subscriptions

// Subscribe to table changes
const subscription = supabase
  .channel('posts-changes')
  .on(
    'postgres_changes',
    { event: '*', schema: 'public', table: 'posts' },
    (payload) => {
      console.log('Change received:', payload.eventType, payload.new);
      if (payload.eventType === 'INSERT') addPost(payload.new);
      if (payload.eventType === 'UPDATE') updatePost(payload.new);
      if (payload.eventType === 'DELETE') removePost(payload.old);
    }
  )
  .subscribe();

// Cleanup
subscription.unsubscribe();

// Broadcast (custom events between clients)
const channel = supabase.channel('room-1');
channel
  .on('broadcast', { event: 'cursor-pos' }, ({ payload }) => {
    updateCursor(payload.userId, payload.x, payload.y);
  })
  .subscribe();

// Send broadcast
await channel.send({
  type: 'broadcast',
  event: 'cursor-pos',
  payload: { userId, x: mouseX, y: mouseY }
});

Edge Functions

// supabase/functions/send-email/index.ts
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 { userId, subject, body } = await req.json();

  const supabase = createClient(
    Deno.env.get('SUPABASE_URL')!,
    Deno.env.get('SUPABASE_SERVICE_ROLE_KEY')!  // admin access
  );

  const { data: user } = await supabase
    .from('users')
    .select('email')
    .eq('id', userId)
    .single();

  // Send email via Resend
  await fetch('https://api.resend.com/emails', {
    method: 'POST',
    headers: {
      Authorization: `Bearer ${Deno.env.get('RESEND_API_KEY')}`,
      'Content-Type': 'application/json',
    },
    body: JSON.stringify({
      from: 'app@myapp.com',
      to: user.email,
      subject,
      html: body,
    }),
  });

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

Vector Search (AI/Semantic)

-- Enable pgvector
CREATE EXTENSION IF NOT EXISTS vector;

-- Add embedding column
ALTER TABLE articles ADD COLUMN embedding vector(384);

-- Create vector index
CREATE INDEX ON articles USING ivfflat (embedding vector_cosine_ops) WITH (lists = 100);

-- Semantic search function
CREATE OR REPLACE FUNCTION semantic_search(query_embedding vector(384), match_count int = 10)
RETURNS TABLE(id bigint, title text, similarity float)
LANGUAGE plpgsql AS $$
BEGIN
  RETURN QUERY
  SELECT
    articles.id,
    articles.title,
    1 - (articles.embedding <=> query_embedding) AS similarity
  FROM articles
  WHERE articles.embedding IS NOT NULL
  ORDER BY articles.embedding <=> query_embedding
  LIMIT match_count;
END;
$$;

// Call semantic search
const { data: results } = await supabase.rpc('semantic_search', {
  query_embedding: await generateEmbedding(searchQuery),
  match_count: 10
});

Supabase in 2026 is the fastest way to build a production backend. You get PostgreSQL power, real-time subscriptions, auth, and edge functions with a developer experience better than Firebase โ€” and the data is actually yours. Start with the free tier (unlimited API calls, 500MB database), migrate to Pro ($25/month) when you need more resources.

Frequently Asked Questions

Is Supabase free?

There is a free tier covering two projects with 500 MB of database storage each, which is enough for a side project or a prototype. Free projects pause after a week of inactivity and resume when you open them. Paid plans start around $25 per month per project.

Is Supabase production-ready?

Yes. The underlying database is PostgreSQL, which has been production-grade for decades. The pieces built around it — auth, storage, edge functions — are younger, so read the changelog before depending on a recently added feature.

Do I still need a backend with Supabase?

Often not. Row Level Security lets you enforce access rules in the database itself, so the client can query it directly and safely. You still want a server for anything involving a secret — payment webhooks, third-party API keys, work that must not be tampered with — and edge functions cover most of those cases.

What is Row Level Security and do I have to use it?

RLS is a Postgres feature that filters rows per user according to policies you define. With Supabase you should treat it as mandatory: the anon key is public and ships in your client, so without RLS your tables are readable by anyone who opens developer tools. Enable it on every table before launch.

Can I migrate an existing PostgreSQL database to Supabase?

Yes — it is standard Postgres, so a pg_dump and restore works. Afterwards you need to enable RLS and write policies, because a database imported from elsewhere will not have them.

How does Supabase handle vector search for AI apps?

Through the pgvector extension, so embeddings live in the same database as the rest of your data and can be filtered with ordinary SQL in the same query. That removes the usual need to run a separate vector database alongside your primary one.

What are the main limitations?

No built-in offline sync, a smaller ecosystem than Firebase, and free projects pausing when idle. Connection limits also matter on smaller plans — use the pooler for serverless deployments or you will exhaust them.

MD Rafikul Islam

Written by

MD Rafikul Islam is a software developer and the editor of TechPulse. He writes about developer tooling, hardware, and the practical decisions that come up in day-to-day engineering work โ€” which laptop to buy, which framework to commit to, why a build broke at 2am. He tests the tools he writes about and says plainly when something is not worth the money. Corrections and corrections requests are welcome at rony.yf25@gmail.com.

โœ๏ธ Leave a Comment

Your email address will not be published. Required fields are marked *