Appwrite: A Self-Hosted Firebase Alternative You Can Actually Own
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.
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.
What Appwrite Provides
Appwrite is not a single-feature tool. It is a complete backend platform with these core services:
- Databases — Document-based storage with collections, attributes, indexes, and query capabilities
- Authentication — Email/password, OAuth (30+ providers), phone/SMS, magic links, anonymous sessions, and JWT tokens
- Storage — File uploads with buckets, permissions, image transformations (resize, crop, format conversion), and antivirus scanning
- Functions — Serverless functions triggered by events, schedules, or HTTP calls, supporting 10+ runtimes (Node.js, Python, Dart, PHP, Ruby, and more)
- Realtime — WebSocket subscriptions to any resource change (database documents, file uploads, auth events)
- Messaging — Push notifications, email, and SMS through integrated providers
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
- You want full control over your data and infrastructure
- You need to comply with data residency requirements (GDPR, HIPAA)
- You want predictable costs without usage-based surprises
- You are building a Flutter app (Appwrite has first-class Dart/Flutter support)
- You do not want vendor lock-in to Google Cloud
When to choose Appwrite over Supabase
- You prefer a document-based data model over relational SQL
- You want a broader selection of serverless function runtimes
- You need built-in image transformations on uploaded files
- You want push notifications and messaging as a built-in service
- You prefer Appwrite's permission model (simpler than Supabase's row-level security)
When Appwrite may not be the right choice
- You need complex relational queries with joins — Supabase (PostgreSQL) is stronger here
- You have an existing PostgreSQL schema you want to expose as an API — Supabase does this natively
- You need the absolute largest ecosystem of tutorials and Stack Overflow answers — Firebase wins on community size
- You need mature, battle-tested production reliability at massive scale — Firebase has years of Google infrastructure behind it
Installation with Docker
Appwrite packages everything into Docker containers. The installation is a single command, but understanding what it deploys is important.
System requirements
- Minimum: 2 CPU cores, 4 GB RAM, 10 GB storage
- Recommended: 4 CPU cores, 8 GB RAM, 50+ GB storage
- Docker: Version 20.10+ with Docker Compose v2
- Architecture: x86_64 or ARM64
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:
- HTTP and HTTPS ports
- Your domain name
- Secret keys (auto-generated if left blank)
- SMTP configuration (optional, for email)
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:
- Navigate to Databases and create a new database
- Create a collection within the database (e.g., "posts")
- Define attributes: title (string), content (string), published (boolean), created_at (datetime)
- 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:
Query.equal(),Query.notEqual()— Exact matchQuery.greaterThan(),Query.lessThan()— Range queriesQuery.search()— Full-text search on indexed string attributesQuery.orderAsc(),Query.orderDesc()— SortingQuery.limit(),Query.offset(),Query.cursorAfter()— Pagination
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:
- Google, Apple, Microsoft, GitHub, GitLab, Bitbucket
- Facebook, Twitter/X, Discord, Spotify, Twitch
- Amazon, Dropbox, Slack, Notion, LinkedIn
- And many more
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
- Phone/SMS — OTP codes via Twilio or Vonage
- Magic links — Passwordless email login
- Anonymous sessions — Let users interact before signing up, then convert to a full account
- JWT tokens — For server-to-server authentication
- Multi-factor authentication — TOTP-based MFA support
- Teams — Built-in team and role management for organizing users
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:
- Resize — Specify width and height
- Crop — Center, top, bottom, left, right gravity
- Format conversion — Convert between JPEG, PNG, WebP, AVIF, and GIF
- Quality — Compress for bandwidth savings
- Border radius — Round corners
- Background color — For transparent images
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
- Node.js 18, 20, 21
- Python 3.9, 3.10, 3.11, 3.12
- PHP 8.0, 8.1, 8.2, 8.3
- Ruby 3.0, 3.1, 3.2, 3.3
- Dart 2.18, 3.0, 3.1, 3.3
- Deno 1.35, 1.40
- Swift 5.8, 5.9
- Kotlin 1.8, 1.9
- Java 11, 17, 18
- .NET 6.0, 7.0
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:
- Events — Database changes, auth events, storage uploads, or any Appwrite system event
- Schedule — Cron expressions (e.g.,
0 */6 * * *for every 6 hours) - HTTP — Direct API calls to the function endpoint
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:
- Database changes — New, updated, or deleted documents in specific collections
- File changes — Uploads and deletions in storage buckets
- Auth events — User creation, session changes
- Function executions — Completion of serverless function runs
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:
Role.any()— Anyone, including unauthenticated usersRole.guests()— Only unauthenticated usersRole.users()— Any authenticated userRole.user(id)— A specific userRole.team(id)— Members of a specific teamRole.team(id, role)— Members with a specific role in a teamRole.label(label)— Users with a specific label
Permissions are set per-document and per-file, giving you fine-grained control without writing policy files.
Backup and Recovery
What to back up
- MariaDB — Contains all your application data (databases, collections, documents, user accounts)
- Uploaded files — The
appwrite-uploadsvolume - Configuration — Your
docker-compose.ymland environment variables - Certificates — The
appwrite-certificatesvolume (Let's Encrypt certs)
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:
- Idle: 1.5-2 GB RAM across all containers
- Light use (a few users): 2-3 GB RAM
- Moderate use (dozens of users, active functions): 4-6 GB RAM
- ClamAV alone: ~1 GB RAM (can be disabled if you do not need antivirus scanning)
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:
- Want a complete backend platform without stitching together individual services
- Are building mobile or web apps that need auth, database, storage, and functions
- Want to self-host everything and avoid vendor lock-in
- Use Flutter (Appwrite has best-in-class Flutter/Dart support)
- Need a simpler permissions model than Firebase security rules or Postgres RLS
- Want built-in image transformations and antivirus scanning
Consider alternatives if you:
- Need complex SQL queries with joins and aggregations (Supabase with PostgreSQL)
- Already have a PostgreSQL database and want to expose it as an API (Supabase, PostgREST)
- Need the largest possible ecosystem and community support (Firebase)
- Are running on a very constrained server under 2 GB RAM (Appwrite's stack is heavy)
- Want to use your database directly alongside the API layer (Supabase lets you connect to Postgres directly)
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.
