Firebase Is Great Until It Isn’t
Firebase solves the “I just want to ship this app without building a backend” problem elegantly. Authentication, a real-time database, file storage, cloud functions — all with client SDKs that just work. For prototypes and side projects, it’s phenomenal.
Then one of three things happens:
- Your app gets traffic and the bill arrives
- Google announces another product sunset (Firebase has been on the rumored list more than once)
- Your data is in a proprietary format in Google’s infrastructure and you realize you have no exit strategy
Appwrite is the self-hosted answer. All the same capabilities — auth, databases, storage, functions, real-time — running on your own hardware. Your data stays yours. No cold start bills. No platform risk.
What BaaS Is and Why Self-Hosting It Makes Sense
Backend-as-a-Service gives your frontend app a complete backend without writing server-side code. Instead of spinning up API routes, a database, and an auth system, you call an SDK:
// Log in a user — no backend code written by you
const session = await account.createEmailPasswordSession(email, password);
// Store some data — no API endpoint written by you
const doc = await databases.createDocument(DB_ID, COLLECTION_ID, ID.unique(), {
title: "My note",
content: "BaaS is convenient"
});
Why self-host instead of using the managed cloud version?
- Cost: Appwrite Cloud has pricing. Running it yourself on a $10 VPS or your home server costs whatever your hardware costs.
- Data sovereignty: Medical apps, internal tools, anything with compliance requirements — your data doesn’t go to a cloud provider.
- Customization: Run it on the same server as your other services, behind your own reverse proxy, with your own backup strategy.
The tradeoff: you’re responsible for uptime, updates, and backups. For a side project or internal tool, that’s a fair trade.
Appwrite’s Feature Set
Before diving into setup, here’s what you’re actually getting:
- Authentication: Email/password, magic links, OAuth (Google, GitHub, Discord, 30+ providers), phone/SMS, anonymous sessions
- Databases: Document database (JSON documents) with collections, indexes, and a query language
- Storage: File buckets with MIME type restrictions, file size limits, image transformations, encryption at rest
- Functions: Serverless functions in Node.js, Python, PHP, Ruby, Dart, and others — triggered by events or HTTP
- Realtime: Subscribe to database/storage events with WebSockets — real-time sync without polling
- Messaging: Push notifications, email, SMS (requires provider keys)
- Teams and Permissions: Role-based access at the collection, document, and file level
For a typical CRUD app or mobile app backend, this covers everything.
Docker Compose Deployment
Appwrite uses Docker and comes with a one-command installer:
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:1.5.7
The installer prompts for:
- HTTP port (default 80)
- HTTPS port (default 443)
- Hostname (your domain or
localhost) - Let’s Encrypt email (optional)
It generates a complete docker-compose.yml with all the required services. The full stack includes:
# What the generated docker-compose.yml runs:
services:
appwrite: # Main API server
appwrite-worker-*: # Multiple worker containers for background tasks
mariadb: # Primary database
redis: # Caching and pub/sub
telegraf: # Metrics collection
influxdb: # Metrics storage
traefik: # Reverse proxy (built-in)
Yes, it’s a lot of containers. Appwrite is not a lightweight stack — this is a full-featured backend platform. Minimum realistic requirements: 2 CPU cores, 4GB RAM. Runs fine on a $20/month VPS or a decent home server.
# Start Appwrite
cd appwrite
docker compose up -d
# Check everything is running
docker compose ps
# Appwrite console: http://your-hostname or https://your-hostname
Creating a Project via the Console
- Open the Appwrite console at your hostname
- Sign up for the first account (becomes the owner)
- Create Project → Enter a name
- Note the Project ID — you’ll need it in your SDK code
Create a database and collection:
- Databases → Create Database → Note the Database ID
- Create Collection → Configure attributes (schema):
String attribute: "title" — required, max 255 chars
String attribute: "content" — required, max 5000 chars
Boolean attribute: "published" — optional, default false
DateTime attribute: "created_at" — required
- Indexes → Add an index on
created_atfor efficient sorting
Configure permissions on the collection:
Roles → Create:
- Any: can create (for open registration apps)
- Users: can read own documents
- Admins: full access
Appwrite’s permission system uses roles like any, users, user:{id}, team:{id}, and label:{name}. This is document-level — every document can have individual permissions.
JavaScript SDK Quickstart
npm install appwrite
import { Client, Account, Databases, ID } from 'appwrite';
// Initialize client
const client = new Client()
.setEndpoint('https://your-appwrite-instance.com/v1')
.setProject('your-project-id');
const account = new Account(client);
const databases = new Databases(client);
const DB_ID = 'your-database-id';
const COLLECTION_ID = 'your-collection-id';
// Create an account
async function register(email, password, name) {
return await account.create(ID.unique(), email, password, name);
}
// Log in
async function login(email, password) {
return await account.createEmailPasswordSession(email, password);
}
// Create a document
async function createNote(title, content) {
return await databases.createDocument(
DB_ID,
COLLECTION_ID,
ID.unique(), // auto-generate ID
{
title,
content,
published: false,
created_at: new Date().toISOString()
}
);
}
// Query documents
async function getNotes() {
const { documents } = await databases.listDocuments(
DB_ID,
COLLECTION_ID,
[
Query.equal('published', true),
Query.orderDesc('created_at'),
Query.limit(25)
]
);
return documents;
}
// Real-time subscription
const unsubscribe = client.subscribe(
`databases.${DB_ID}.collections.${COLLECTION_ID}.documents`,
(response) => {
if (response.events.includes('databases.*.collections.*.documents.*.create')) {
console.log('New document:', response.payload);
}
}
);
// Unsubscribe when done
// unsubscribe();
File Storage
import { Storage, InputFile } from 'appwrite';
const storage = new Storage(client);
const BUCKET_ID = 'your-bucket-id';
// Upload a file
async function uploadFile(file) {
return await storage.createFile(
BUCKET_ID,
ID.unique(),
InputFile.fromBlob(file, file.name)
);
}
// Get a file preview URL (with image transformations)
function getImageUrl(fileId, width = 400) {
return storage.getFilePreview(BUCKET_ID, fileId, width);
}
// Direct download URL
function getDownloadUrl(fileId) {
return storage.getFileDownload(BUCKET_ID, fileId);
}
Buckets have configurable permissions, max file size, and allowed MIME types. You can restrict a bucket to only accept image/jpeg and image/png with a 5MB max — built into the bucket settings, no validation code required.
Appwrite Functions
Serverless functions run on the same instance. Trigger them from the console, via HTTP, or on Appwrite events.
// functions/send-welcome-email/src/main.js
import { Client, Messaging } 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(req.headers['x-appwrite-key']);
// Triggered on new user registration
const userId = req.body.userId;
log(`Sending welcome email to user ${userId}`);
// Your email logic here
return res.json({ success: true });
};
Deploy via CLI:
npm install -g appwrite-cli
appwrite login
appwrite deploy function --functionId your-function-id
Functions are isolated per runtime, with configurable CPU/memory limits. Cold starts are non-zero but acceptable for webhook-style use cases.
Appwrite vs Supabase vs PocketBase
| Feature | Appwrite | Supabase | PocketBase |
|---|---|---|---|
| Database type | Document (JSON) | PostgreSQL | SQLite |
| Resource usage | High (multi-container) | High | Very low (single binary) |
| Auth providers | 30+ | 20+ | Standard |
| Realtime | WebSocket events | PostgreSQL LISTEN | SSE |
| Functions | Built-in | Edge Functions (Deno) | JS hooks only |
| Setup complexity | Medium | Medium | Very low |
| Best for | Full-featured apps | SQL power users | Lightweight projects |
Choose Appwrite for mobile apps or frontend-heavy apps where you want a full feature set and don’t need PostgreSQL specifically.
Choose Supabase if you want SQL, need complex queries, or are building something that grows into a real database-backed application.
Choose PocketBase if you want the absolute minimum — single binary, SQLite, runs on a $5 VPS, perfect for side projects where Appwrite’s multi-container overhead is overkill.
Resource Requirements and Upgrading
Minimum: 2 vCPU, 4GB RAM, 20GB storage Recommended for production: 4 vCPU, 8GB RAM, SSD storage
Upgrading Appwrite:
cd appwrite
# Check current version
docker compose exec appwrite app --version
# Pull new images
docker compose pull
# Run migrations
docker compose run --rm appwrite migrate
# Restart
docker compose up -d
# Verify
docker compose ps
Always back up the MariaDB database before upgrading:
docker compose exec mariadb sh -c \
'exec mysqldump -u appwrite -p"$MARIADB_ROOT_PASSWORD" appwrite' \
> appwrite-backup-$(date +%Y%m%d).sql
Ideal Use Cases
Appwrite is genuinely well-suited for:
- Mobile app backends: React Native, Flutter — the SDKs are first-class
- Side projects and prototypes: Get auth + database + storage in an afternoon
- Internal tools: Employee directories, document management, ticketing systems
- JAMstack frontends that need a backend: Next.js app with Appwrite for the data layer
It’s overkill for:
- Simple CRUD APIs where a Python/FastAPI backend takes an hour to build
- Static sites with no user data
- Anything that needs complex SQL joins — use Supabase instead
For its sweet spot — “I need auth, file storage, and a database for my app without writing server code” — Appwrite is solid and the self-hosted version is genuinely production-ready.