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 youconst session = await account.createEmailPasswordSession(email, password);
// Store some data — no API endpoint written by youconst 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.8.1The 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 Appwritecd appwritedocker compose up -d
# Check everything is runningdocker compose ps
# Appwrite console: http://your-hostname or https://your-hostnameCreating 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 charsString attribute: "content" — required, max 5000 charsBoolean attribute: "published" — optional, default falseDateTime 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 accessAppwrite’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 appwriteimport { Client, Account, Databases, ID } from "appwrite";
// Initialize clientconst 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 accountasync function register(email, password, name) { return await account.create(ID.unique(), email, password, name);}
// Log inasync function login(email, password) { return await account.createEmailPasswordSession(email, password);}
// Create a documentasync 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 documentsasync 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 subscriptionconst 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 fileasync 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 URLfunction 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.
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-cliappwrite loginappwrite deploy function --functionId your-function-idFunctions 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 versiondocker compose exec appwrite app --version
# Pull new imagesdocker compose pull
# Run migrationsdocker compose run --rm appwrite migrate
# Restartdocker compose up -d
# Verifydocker compose psAlways 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).sqlIdeal 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.