Skip to main content

Supabase vs Firebase vs Appwrite: BaaS Platforms in 2026

·PkgPulse Team

TL;DR

Supabase is the default choice for most teams in 2026 — Postgres-based, open source, excellent TypeScript SDK, and the best developer experience of the three. Firebase (Firestore) remains the go-to for mobile-first apps needing Google's global CDN and offline sync. Appwrite is the right choice if self-hosting is a requirement or you want a fully open source BaaS without vendor lock-in.

Key Takeaways

  • Supabase: ~3.2M weekly npm downloads — Postgres + Row Level Security + realtime + storage + edge functions
  • Firebase: ~3.7M weekly npm downloads — Google-backed, NoSQL (Firestore), best offline mobile support
  • Appwrite: ~180K weekly npm downloads — 100% open source, self-hostable, Docker-based
  • Supabase won the "open source Firebase" competition convincingly in 2024-2026
  • Vendor lock-in: Firebase is highest (proprietary NoSQL), Supabase is medium (Postgres is portable), Appwrite is lowest (self-hostable)
  • All three offer authentication, storage, and serverless functions alongside their database

The BaaS Landscape in 2026

Backend-as-a-service platforms let you skip the boilerplate: authentication, database, file storage, and serverless functions in one package. The three main choices have very different philosophies:

PlatformDatabaseHost ModelOpen SourceSelf-Host
SupabasePostgresManaged cloud✅ MIT✅ Docker
FirebaseFirestore (NoSQL)Google Cloud only
AppwriteMariaDB (internal)Managed cloud✅ Apache 2✅ Docker

Supabase

Supabase is a Postgres-based BaaS with a clean TypeScript SDK, Row Level Security for authorization, and a thriving ecosystem.

Database + Row Level Security

import { createClient } from "@supabase/supabase-js"

const supabase = createClient(
  process.env.NEXT_PUBLIC_SUPABASE_URL!,
  process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!
)

// Typed queries via auto-generated types:
type Package = {
  id: string
  name: string
  weekly_downloads: number
  created_at: string
}

// SELECT with filter:
const { data, error } = await supabase
  .from("packages")
  .select("id, name, weekly_downloads")
  .gt("weekly_downloads", 1000000)
  .order("weekly_downloads", { ascending: false })
  .limit(10)

// INSERT:
const { data: newPkg, error: insertError } = await supabase
  .from("packages")
  .insert({ name: "react", weekly_downloads: 25000000 })
  .select()
  .single()

// UPDATE:
await supabase
  .from("packages")
  .update({ weekly_downloads: 26000000 })
  .eq("name", "react")

// DELETE:
await supabase.from("packages").delete().eq("id", pkg.id)

Row Level Security (RLS) — the Supabase superpower:

-- Enable RLS on the table:
ALTER TABLE packages ENABLE ROW LEVEL SECURITY;

-- Policy: users can only see their own watchlist:
CREATE POLICY "Users see own watchlist"
ON watchlist
FOR SELECT
USING (auth.uid() = user_id);

-- Policy: public packages visible to all:
CREATE POLICY "Public packages are viewable by everyone"
ON packages
FOR SELECT
USING (is_public = true);

RLS moves authorization logic into the database — the anon key only sees what the policies allow, even on direct API calls.

Authentication

// Email/password signup:
const { data, error } = await supabase.auth.signUp({
  email: "user@example.com",
  password: "secure-password",
})

// OAuth (GitHub, Google, etc.):
await supabase.auth.signInWithOAuth({
  provider: "github",
  options: { redirectTo: "https://app.example.com/auth/callback" },
})

// Get current session:
const { data: { session } } = await supabase.auth.getSession()
const user = session?.user  // { id, email, ... }

// Server-side (Next.js App Router):
import { createServerClient } from "@supabase/ssr"
import { cookies } from "next/headers"

const supabase = createServerClient(
  process.env.NEXT_PUBLIC_SUPABASE_URL!,
  process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!,
  { cookies: { getAll: () => cookies().getAll() } }
)

const { data: { user } } = await supabase.auth.getUser()

Realtime

// Subscribe to table changes:
const channel = supabase
  .channel("packages-changes")
  .on(
    "postgres_changes",
    { event: "INSERT", schema: "public", table: "packages" },
    (payload) => console.log("New package:", payload.new)
  )
  .on(
    "postgres_changes",
    { event: "UPDATE", schema: "public", table: "packages" },
    (payload) => console.log("Updated:", payload.new)
  )
  .subscribe()

// Cleanup:
supabase.removeChannel(channel)

Storage

// Upload file:
const { data, error } = await supabase.storage
  .from("avatars")
  .upload(`${userId}/avatar.png`, file, {
    upsert: true,
    contentType: "image/png",
  })

// Get public URL:
const { data: { publicUrl } } = supabase.storage
  .from("avatars")
  .getPublicUrl(`${userId}/avatar.png`)

// Signed URL (private bucket):
const { data: { signedUrl } } = await supabase.storage
  .from("private-docs")
  .createSignedUrl("report.pdf", 3600)  // 1 hour expiry

Supabase Edge Functions:

// supabase/functions/send-notification/index.ts
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 { packageName } = await req.json()

  const supabase = createClient(
    Deno.env.get("SUPABASE_URL")!,
    Deno.env.get("SUPABASE_SERVICE_ROLE_KEY")!
  )

  // Deno-based edge functions with full Supabase access
  const { data } = await supabase
    .from("subscribers")
    .select("email")
    .eq("package_name", packageName)

  // Send notifications...
  return new Response(JSON.stringify({ sent: data?.length }))
})

Firebase (Firestore)

Firebase is Google's BaaS — the most mature platform but with a proprietary NoSQL data model and no self-hosting option.

Firestore Data Model

Firestore uses a document/collection model — different from Supabase's relational Postgres:

import { initializeApp } from "firebase/app"
import {
  getFirestore,
  collection,
  doc,
  getDoc,
  getDocs,
  setDoc,
  addDoc,
  updateDoc,
  deleteDoc,
  query,
  where,
  orderBy,
  limit,
  onSnapshot,
} from "firebase/firestore"

const app = initializeApp({
  apiKey: process.env.NEXT_PUBLIC_FIREBASE_API_KEY,
  authDomain: "myapp.firebaseapp.com",
  projectId: "myapp",
})

const db = getFirestore(app)

// Documents live in collections, not tables:
// packages/{packageId}
// users/{userId}/watchlist/{packageId}  (subcollection)

// Read a document:
const docRef = doc(db, "packages", "react")
const docSnap = await getDoc(docRef)
if (docSnap.exists()) {
  console.log(docSnap.data())  // { name: "react", downloads: 25000000 }
}

// Query a collection:
const q = query(
  collection(db, "packages"),
  where("weeklyDownloads", ">", 1000000),
  orderBy("weeklyDownloads", "desc"),
  limit(10)
)
const querySnapshot = await getDocs(q)
querySnapshot.forEach((doc) => console.log(doc.id, doc.data()))

// Write a document:
await setDoc(doc(db, "packages", "react"), {
  name: "react",
  weeklyDownloads: 25000000,
  updatedAt: new Date(),
})

// Real-time listener:
const unsubscribe = onSnapshot(
  query(collection(db, "packages"), where("featured", "==", true)),
  (snapshot) => {
    snapshot.docChanges().forEach((change) => {
      if (change.type === "added") console.log("Added:", change.doc.data())
      if (change.type === "modified") console.log("Modified:", change.doc.data())
      if (change.type === "removed") console.log("Removed:", change.doc.data())
    })
  }
)

Firebase Authentication

import {
  getAuth,
  signInWithEmailAndPassword,
  createUserWithEmailAndPassword,
  signInWithPopup,
  GoogleAuthProvider,
  onAuthStateChanged,
} from "firebase/auth"

const auth = getAuth(app)

// Email/password:
await createUserWithEmailAndPassword(auth, "user@example.com", "password")
await signInWithEmailAndPassword(auth, "user@example.com", "password")

// Google OAuth:
const provider = new GoogleAuthProvider()
await signInWithPopup(auth, provider)

// Auth state listener:
onAuthStateChanged(auth, (user) => {
  if (user) {
    console.log("Logged in:", user.uid, user.email)
  } else {
    console.log("Logged out")
  }
})

Firestore Security Rules

Firebase's equivalent of Supabase RLS — document-level access control:

// firestore.rules
rules_version = '2';
service cloud.firestore {
  match /databases/{database}/documents {
    // Users can only read/write their own data:
    match /users/{userId}/{document=**} {
      allow read, write: if request.auth != null && request.auth.uid == userId;
    }

    // Packages are publicly readable:
    match /packages/{packageId} {
      allow read: if true;
      allow write: if request.auth != null && get(/databases/$(database)/documents/users/$(request.auth.uid)).data.isAdmin == true;
    }
  }
}

Firebase strengths:

  • Best offline-first mobile support (Firestore's local cache + sync)
  • Google's global infrastructure — fastest cold starts anywhere
  • Firebase Auth has most OAuth providers out of the box
  • Firebase App Check for mobile app integrity
  • 10 years of production battle-testing

Firebase limitations:

  • Firestore's NoSQL model requires denormalization for complex queries
  • No SQL — JOINs, aggregations, and complex filtering are cumbersome
  • Vendor lock-in: migrating away from Firestore is painful
  • Security rules become complex for sophisticated access patterns
  • Pricing is unpredictable at scale (per-read/write pricing)

Appwrite

Appwrite is a fully open source BaaS — you can run it on your own infrastructure with Docker, or use Appwrite Cloud.

import { Client, Account, Databases, Storage, Query } from "appwrite"

const client = new Client()
  .setEndpoint("https://cloud.appwrite.io/v1")  // or your self-hosted endpoint
  .setProject(process.env.NEXT_PUBLIC_APPWRITE_PROJECT_ID!)

const account = new Account(client)
const databases = new Databases(client)
const storage = new Storage(client)

// Authentication:
await account.create("user-id", "user@example.com", "password", "Display Name")
const session = await account.createEmailPasswordSession("user@example.com", "password")

// Get current user:
const user = await account.get()

// Database queries:
const documents = await databases.listDocuments(
  "database-id",
  "packages-collection-id",
  [
    Query.greaterThan("weeklyDownloads", 1000000),
    Query.orderDesc("weeklyDownloads"),
    Query.limit(10),
  ]
)

// Create a document:
const doc = await databases.createDocument(
  "database-id",
  "packages-collection-id",
  "unique()",  // auto-generate ID
  {
    name: "react",
    weeklyDownloads: 25000000,
    version: "18.2.0",
  }
)

// File upload:
const file = await storage.createFile(
  "avatars-bucket-id",
  "unique()",
  inputFile,
)

// Get file URL:
const url = storage.getFileView("avatars-bucket-id", file.$id)

Self-hosting Appwrite:

# docker-compose.yml — complete Appwrite stack:
version: "3"
services:
  appwrite:
    image: appwrite/appwrite:1.6
    ports:
      - "80:80"
      - "443:443"
    environment:
      - _APP_ENV=production
      - _APP_OPENSSL_KEY_V1=${APPWRITE_KEY}
      - _APP_REDIS_HOST=appwrite-redis
      - _APP_DB_HOST=appwrite-mariadb
      # ... additional config
  appwrite-redis:
    image: redis:7
  appwrite-mariadb:
    image: mariadb:10.11
    environment:
      MYSQL_ROOT_PASSWORD: ${MARIADB_PASSWORD}

Appwrite's distinguishing features:

  • Complete ownership: your data, your server, your infrastructure
  • Built-in functions with multiple runtime support (Node.js, Python, PHP, etc.)
  • Teams and memberships for multi-tenant apps
  • Webhooks for all events
  • 100% open source — audit the code, contribute, fork

Feature Comparison

FeatureSupabaseFirebaseAppwrite
DatabasePostgresFirestore (NoSQL)MariaDB (via collections API)
SQL support✅ Full SQL❌ (query API only)
Realtime✅ Postgres changes✅ Native
Auth✅ Excellent✅ Excellent✅ Good
Storage
Edge/functions✅ Deno✅ Cloud Functions✅ Multiple runtimes
Self-hosting✅ Docker✅ Docker
Open source✅ MIT✅ Apache 2
TypeScript SDK✅ Excellent✅ Good✅ Good
Offline support⚠️ Limited✅ Excellent⚠️ Limited
Free tier2 projects, 500MB DBSpark plan (generous)1 project, 75K requests
Vendor lock-inLow (Postgres)HighNone (self-host)

Database Model: The Key Differentiator

This is the most important choice:

Supabase (Postgres):

-- Full relational model — JOINs, aggregations, CTEs
SELECT
  p.name,
  p.weekly_downloads,
  COUNT(w.user_id) as watcher_count,
  AVG(r.rating) as avg_rating
FROM packages p
LEFT JOIN watchlists w ON w.package_id = p.id
LEFT JOIN reviews r ON r.package_id = p.id
WHERE p.weekly_downloads > 1000000
GROUP BY p.id
ORDER BY watcher_count DESC;

Firebase (Firestore):

// Can't JOIN — must denormalize:
// packages/{id}: { name, downloads, watcherCount, avgRating }  ← computed at write time
// Must update watcherCount whenever someone adds to watchlist (transaction or Cloud Function)

Appwrite:

// Collection-based — no JOINs:
const packages = await databases.listDocuments(
  dbId, "packages",
  [Query.greaterThan("weeklyDownloads", 1000000)]
)
// Relationships require multiple queries or denormalization

When to Use Each

Choose Supabase if:

  • You're building a relational data model (most web apps)
  • You want Postgres features: JOINs, CTEs, full-text search, PostGIS
  • TypeScript-first with generated types from your schema
  • You want open source with managed hosting and an escape hatch

Choose Firebase if:

  • Building a mobile-first app needing offline-first sync
  • Your data model is document-based (social feeds, chat, IoT events)
  • Google's global infrastructure and Firebase Auth's breadth matter
  • You're comfortable with NoSQL denormalization patterns

Choose Appwrite if:

  • Full data sovereignty — you run it on your servers
  • GDPR/compliance requirements mandate on-premises
  • You want to avoid vendor lock-in entirely
  • Multi-runtime serverless functions (Python, PHP alongside Node.js)

Methodology

npm download data from npm registry (February 2026). Feature comparison based on Supabase SDK v2.x, Firebase JS SDK v10.x, and Appwrite SDK 14.x documentation. Self-hosting characteristics based on official Docker setup guides.

Compare backend package ecosystems on PkgPulse →

Comments

Stay Updated

Get the latest package insights, npm trends, and tooling tips delivered to your inbox.