Skip to content
SumGuy's Ramblings
Go back

Appwrite: Your Own Firebase, Minus the Google Surveillance Subscription

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:

  1. Your app gets traffic and the bill arrives
  2. Google announces another product sunset (Firebase has been on the rumored list more than once)
  3. 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?

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:

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:

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

  1. Open the Appwrite console at your hostname
  2. Sign up for the first account (becomes the owner)
  3. Create Project → Enter a name
  4. Note the Project ID — you’ll need it in your SDK code

Create a database and collection:

  1. DatabasesCreate Database → Note the Database ID
  2. 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
  1. Indexes → Add an index on created_at for 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

FeatureAppwriteSupabasePocketBase
Database typeDocument (JSON)PostgreSQLSQLite
Resource usageHigh (multi-container)HighVery low (single binary)
Auth providers30+20+Standard
RealtimeWebSocket eventsPostgreSQL LISTENSSE
FunctionsBuilt-inEdge Functions (Deno)JS hooks only
Setup complexityMediumMediumVery low
Best forFull-featured appsSQL power usersLightweight 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:

It’s overkill for:

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.


Share this post on:

Previous Post
Stable Diffusion vs ComfyUI vs Fooocus: AI Image Generation at Home
Next Post
Linux Suspend and Hibernate: Teaching Your Machine to Take a Nap Without Dying