← All articles
a close up of a computer in a dark room

Appwrite: A Self-Hosted Firebase Alternative You Can Actually Own

Development 2026-02-15 · 11 min read appwrite backend firebase baas authentication docker self-hosted
By Selfhosted Guides Editorial TeamSelf-hosting practitioners covering open source software, home lab infrastructure, and data sovereignty.

Firebase is remarkably convenient. You get authentication, a database, file storage, serverless functions, and push notifications — all through a single SDK. The problem is that you are building on Google's platform, with Google's pricing, Google's rules, and Google's ability to change or deprecate any of it without warning.

Photo by Tyler on Unsplash

When Firebase's Realtime Database pricing caught developers off guard, or when Google shut down its gaming services, or when Cloud Functions cold starts became unacceptable, the response was always the same: find an alternative. But alternatives that match Firebase's breadth of features while letting you self-host are rare.

Appwrite is the strongest contender. It is an open source backend-as-a-service platform that provides databases, authentication, file storage, serverless functions, realtime subscriptions, and messaging — all in a single self-hosted package. You run it on your own infrastructure, and you own everything.

Appwrite backend platform logo

What Appwrite Provides

Appwrite is not a single-feature tool. It is a complete backend platform with these core services:

Each service has a REST API, and Appwrite provides official SDKs for web (JavaScript), Flutter, Apple (Swift), Android (Kotlin), and server-side languages (Node.js, Python, PHP, Dart, Ruby, .NET, Go).

Appwrite vs. Firebase vs. Supabase

These three are the most commonly compared backend platforms. Each has a different philosophy.

Feature Appwrite Firebase Supabase
Self-hosted Yes (Docker) No Yes (Docker)
Database type Document (MariaDB-backed) Document (Firestore) Relational (PostgreSQL)
Auth providers 30+ OAuth + email/phone Google, email/phone, OAuth 20+ OAuth + email/phone
File storage Yes (with transforms) Yes (Cloud Storage) Yes (S3-compatible)
Serverless functions Yes (10+ runtimes) Yes (Node.js, Python) Yes (Deno/Edge)
Realtime Yes (WebSocket) Yes (native) Yes (PostgreSQL LISTEN/NOTIFY)
Pricing (hosted) Free tier + pay-as-you-go Free tier + pay-as-you-go Free tier + $25/mo Pro
Vendor lock-in Low (self-hostable) High (Google Cloud) Low (self-hostable, standard Postgres)
SDK quality Good, improving Excellent, mature Good
Console UI Clean, modern Functional Clean, modern
Community Growing rapidly Massive Large, active

When to choose Appwrite over Firebase

When to choose Appwrite over Supabase

When Appwrite may not be the right choice

Installation with Docker

Appwrite packages everything into Docker containers. The installation is a single command, but understanding what it deploys is important.

System requirements

Quick install

Appwrite provides an installation script that generates a docker-compose.yml tailored to your environment:

docker run -it --rm \
  --volume /var/run/docker.sock:/var/run/docker.sock \
  --volume "$(pwd)"/appwrite:/usr/src/code/appwrite:rw \
  --entrypoint="install" \
  appwrite/appwrite:latest

This interactive installer asks for:

Manual Docker Compose setup

For more control, here is a production-ready docker-compose.yml:

version: "3.8"

x-logging: &x-logging
  logging:
    driver: json-file
    options:
      max-file: "5"
      max-size: "10m"

services:
  appwrite:
    container_name: appwrite
    image: appwrite/appwrite:latest
    restart: unless-stopped
    <<: *x-logging
    ports:
      - "80:80"
      - "443:443"
    volumes:
      - appwrite-uploads:/storage/uploads
      - appwrite-cache:/storage/cache
      - appwrite-config:/storage/config
      - appwrite-certificates:/storage/certificates
      - appwrite-functions:/storage/functions
    environment:
      - _APP_ENV=production
      - _APP_LOCALE=en
      - _APP_CONSOLE_WHITELIST_ROOT=enabled
      - _APP_CONSOLE_WHITELIST_EMAILS=
      - _APP_CONSOLE_WHITELIST_IPS=
      - _APP_SYSTEM_EMAIL_NAME=Appwrite
      - [email protected]
      - _APP_SYSTEM_RESPONSE_FORMAT=
      - [email protected]
      - _APP_OPTIONS_ABUSE=enabled
      - _APP_OPTIONS_FORCE_HTTPS=enabled
      - _APP_OPENSSL_KEY_V1=your-secret-key-min-128-bits
      - _APP_DOMAIN=appwrite.yourdomain.com
      - _APP_DOMAIN_TARGET=appwrite.yourdomain.com
      - _APP_DOMAIN_FUNCTIONS=functions.yourdomain.com
      - _APP_REDIS_HOST=redis
      - _APP_REDIS_PORT=6379
      - _APP_DB_HOST=mariadb
      - _APP_DB_PORT=3306
      - _APP_DB_SCHEMA=appwrite
      - _APP_DB_USER=appwrite
      - _APP_DB_PASS=your-db-password
      - _APP_INFLUXDB_HOST=influxdb
      - _APP_INFLUXDB_PORT=8086
      - _APP_STORAGE_ANTIVIRUS=enabled
      - _APP_STORAGE_ANTIVIRUS_HOST=clamav
      - _APP_STORAGE_ANTIVIRUS_PORT=3310
      - _APP_SMTP_HOST=smtp.yourdomain.com
      - _APP_SMTP_PORT=587
      - _APP_SMTP_SECURE=tls
      - _APP_SMTP_USERNAME=smtp-user
      - _APP_SMTP_PASSWORD=smtp-password
    depends_on:
      - mariadb
      - redis
      - influxdb
      - clamav

  mariadb:
    container_name: appwrite_mariadb
    image: mariadb:10.11
    restart: unless-stopped
    <<: *x-logging
    environment:
      MYSQL_ROOT_PASSWORD: your-root-password
      MYSQL_DATABASE: appwrite
      MYSQL_USER: appwrite
      MYSQL_PASSWORD: your-db-password
    volumes:
      - appwrite-mariadb:/var/lib/mysql
    command: >
      --innodb-flush-method=fsync
      --innodb-flush-log-at-trx-commit=2

  redis:
    container_name: appwrite_redis
    image: redis:7-alpine
    restart: unless-stopped
    <<: *x-logging
    volumes:
      - appwrite-redis:/data

  influxdb:
    container_name: appwrite_influxdb
    image: appwrite/influxdb:1.5.0
    restart: unless-stopped
    <<: *x-logging
    volumes:
      - appwrite-influxdb:/var/lib/influxdb

  clamav:
    container_name: appwrite_clamav
    image: appwrite/clamav:1.2.0
    restart: unless-stopped
    <<: *x-logging
    volumes:
      - appwrite-clamav:/var/lib/clamav

volumes:
  appwrite-uploads:
  appwrite-cache:
  appwrite-config:
  appwrite-certificates:
  appwrite-functions:
  appwrite-mariadb:
  appwrite-redis:
  appwrite-influxdb:
  appwrite-clamav:

Note that the full Appwrite stack includes several supporting services: MariaDB for data storage, Redis for caching and pub/sub, InfluxDB for usage metrics, and ClamAV for antivirus scanning of uploaded files. This is more infrastructure than a typical self-hosted app, but it all runs within Docker and is managed as a unit.

Starting Appwrite

docker compose up -d

Access the console at https://appwrite.yourdomain.com. Create your admin account on first visit.

Like what you're reading? Subscribe to Self-Hosted Weekly — free weekly guides in your inbox.

Working with Databases

Appwrite uses a document-based model built on top of MariaDB. You define collections (similar to tables), add attributes (similar to columns), and store documents (similar to rows).

Creating a collection

In the Appwrite console:

  1. Navigate to Databases and create a new database
  2. Create a collection within the database (e.g., "posts")
  3. Define attributes: title (string), content (string), published (boolean), created_at (datetime)
  4. Create indexes for attributes you will query on

Using the SDK

import { Client, Databases, ID } from 'appwrite';

const client = new Client()
  .setEndpoint('https://appwrite.yourdomain.com/v1')
  .setProject('your-project-id');

const databases = new Databases(client);

// Create a document
const post = await databases.createDocument(
  'your-database-id',
  'posts',
  ID.unique(),
  {
    title: 'Getting Started with Appwrite',
    content: 'Appwrite is a self-hosted backend platform...',
    published: true,
    created_at: new Date().toISOString()
  }
);

// Query documents
const posts = await databases.listDocuments(
  'your-database-id',
  'posts',
  [
    Query.equal('published', true),
    Query.orderDesc('created_at'),
    Query.limit(10)
  ]
);

Query capabilities

Appwrite supports filtering, sorting, pagination, and full-text search:

The query language is intentionally simpler than SQL. For complex analytics queries, you may want to replicate data to a dedicated analytics database.

Relationships

Appwrite supports document relationships: one-to-one, one-to-many, and many-to-many. Define these as relationship attributes on your collections. Related documents can be fetched in a single query with configurable depth.

Authentication

Authentication is one of Appwrite's strongest features. It handles the tedious parts — session management, token refresh, OAuth flows, email verification — so you do not have to.

Email and password

import { Client, Account, ID } from 'appwrite';

const client = new Client()
  .setEndpoint('https://appwrite.yourdomain.com/v1')
  .setProject('your-project-id');

const account = new Account(client);

// Sign up
await account.create(
  ID.unique(),
  '[email protected]',
  'secure-password',
  'Jane Doe'
);

// Sign in
await account.createEmailPasswordSession(
  '[email protected]',
  'secure-password'
);

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

OAuth providers

Appwrite supports 30+ OAuth providers out of the box:

Enable any provider in the Appwrite console under Auth > Settings by adding your OAuth credentials. The SDK handles the redirect flow automatically.

Other auth methods

Session management

Appwrite manages sessions automatically. Sessions persist across page reloads (stored as secure HTTP-only cookies), and the SDK handles token refresh transparently.

File Storage

Appwrite's storage service handles file uploads with built-in features that you would otherwise need to build yourself.

Buckets and permissions

Files are organized into buckets. Each bucket has its own permissions, size limits, and allowed file types:

import { Client, Storage, ID } from 'appwrite';

const storage = new Storage(client);

// Upload a file
const file = await storage.createFile(
  'photos',  // bucket ID
  ID.unique(),
  document.getElementById('fileInput').files[0]
);

// Get file URL
const url = storage.getFileView('photos', file.$id);

// Get a preview (resized, format-converted)
const thumbnail = storage.getFilePreview(
  'photos',
  file.$id,
  200,   // width
  200,   // height
  'center', // gravity
  90     // quality
);

Image transformations

Appwrite can transform images on the fly:

These transformations are cached after first generation, so subsequent requests are fast.

Antivirus scanning

When ClamAV is enabled (included in the default Docker setup), every uploaded file is scanned for malware before being stored. Files that fail the scan are rejected automatically.

Serverless Functions

Appwrite Functions let you run backend code without managing servers beyond Appwrite itself.

Supported runtimes

Creating a function

// functions/send-welcome-email/src/main.js
import { Client, Users } from 'node-appwrite';

export default async ({ req, res, log, error }) => {
  const client = new Client()
    .setEndpoint(process.env.APPWRITE_FUNCTION_API_ENDPOINT)
    .setProject(process.env.APPWRITE_FUNCTION_PROJECT_ID)
    .setKey(process.env.APPWRITE_API_KEY);

  const payload = JSON.parse(req.body);
  const userId = payload.userId;

  const users = new Users(client);
  const user = await users.get(userId);

  log(`Sending welcome email to ${user.email}`);

  // Your email sending logic here

  return res.json({ success: true });
};

Triggers

Functions can be triggered by:

Event-driven triggers are particularly powerful. For example, trigger a function whenever a new user signs up to send a welcome email, or whenever a document is created to run validation logic.

Realtime Subscriptions

Appwrite's realtime feature uses WebSockets to push changes to connected clients instantly.

import { Client } from 'appwrite';

const client = new Client()
  .setEndpoint('https://appwrite.yourdomain.com/v1')
  .setProject('your-project-id');

// Subscribe to changes in a collection
const unsubscribe = client.subscribe(
  'databases.your-db-id.collections.messages.documents',
  (response) => {
    console.log('Document changed:', response.payload);

    if (response.events.includes('databases.*.collections.*.documents.*.create')) {
      console.log('New message:', response.payload);
    }
  }
);

// Later: unsubscribe
unsubscribe();

You can subscribe to:

Realtime subscriptions respect Appwrite's permission model. Users only receive events for resources they have access to.

Permissions Model

Appwrite's permission system is straightforward compared to Firebase's security rules or Supabase's row-level security:

import { Permission, Role } from 'appwrite';

// Anyone can read, only the creator can update/delete
await databases.createDocument(
  'db-id',
  'posts',
  ID.unique(),
  { title: 'My Post', content: '...' },
  [
    Permission.read(Role.any()),
    Permission.update(Role.user(userId)),
    Permission.delete(Role.user(userId))
  ]
);

Available roles:

Permissions are set per-document and per-file, giving you fine-grained control without writing policy files.

Backup and Recovery

What to back up

Database backup

# Backup MariaDB
docker exec appwrite_mariadb \
  mysqldump -u root -p'your-root-password' --all-databases > appwrite-db-backup.sql

# Restore
cat appwrite-db-backup.sql | docker exec -i appwrite_mariadb \
  mysql -u root -p'your-root-password'

Full backup script

#!/bin/bash
BACKUP_DIR="/backups/appwrite/$(date +%Y-%m-%d)"
mkdir -p "$BACKUP_DIR"

# Database
docker exec appwrite_mariadb \
  mysqldump -u root -p'your-root-password' --all-databases \
  > "$BACKUP_DIR/db.sql"

# Uploaded files
docker run --rm \
  -v appwrite-uploads:/data \
  -v "$BACKUP_DIR":/backup \
  alpine tar czf /backup/uploads.tar.gz /data

echo "Backup completed: $BACKUP_DIR"

Run this daily via cron. For offsite backups, sync the backup directory to an S3-compatible storage or a remote server.

Upgrading

Appwrite releases new versions regularly. To upgrade:

# Pull new images
docker compose pull

# Recreate containers
docker compose up -d

Appwrite handles database migrations automatically on startup. Always back up before upgrading — especially before major version bumps.

Check the migration guide for version-specific instructions. Some major versions require running a migration command.

Reverse Proxy Considerations

Appwrite includes its own Traefik-based reverse proxy and handles TLS certificates via Let's Encrypt. If you are already running a reverse proxy (Caddy, Nginx, Traefik), you have two options:

Option 1: Let Appwrite handle TLS directly — Expose ports 80 and 443 to Appwrite. This is the simplest approach.

Option 2: Run behind your existing proxy — Change Appwrite to listen on a non-standard port and proxy to it:

# In docker-compose.yml, change ports:
ports:
  - "8080:80"

Then in your reverse proxy:

appwrite.yourdomain.com {
    reverse_proxy localhost:8080
}

Set _APP_OPTIONS_FORCE_HTTPS=disabled in this case, since your outer proxy handles TLS.

Resource Usage in Practice

Appwrite is heavier than a single-purpose tool because it bundles multiple services. Expect the full stack to use:

If RAM is tight, disable ClamAV and InfluxDB (usage metrics). The core services — Appwrite, MariaDB, and Redis — run comfortably on 2 GB.

Honest Trade-offs

Appwrite is great if you:

Consider alternatives if you:

The bottom line: Appwrite is the most complete self-hosted Firebase alternative available. It covers authentication, databases, storage, functions, and realtime in a single cohesive platform. The trade-off is resource usage — running the full stack requires more RAM than lightweight alternatives. But if you want one platform that handles your entire backend and you want to own the infrastructure, Appwrite delivers.

Get free weekly tips in your inbox. Subscribe to Self-Hosted Weekly