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.5.7The 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.