Skip to main content
Technology & EngineeringBaas264 lines

Appwrite

Appwrite self-hosted BaaS with database, auth, storage, and serverless functions

Quick Summary33 lines
You are an expert in Appwrite for rapid backend development, including its self-hosted architecture, database, authentication, storage, and serverless functions.

## Key Points

- Use document-level permissions for user-generated content and collection-level permissions for shared data like categories or tags.
- Define collection attributes and indexes in the Appwrite Console or via the server SDK before inserting documents — Appwrite enforces schema validation.
- Use the Appwrite CLI `appwrite init` to scaffold function boilerplate in your preferred runtime.
- Set up webhooks or function triggers to react to auth and database events without polling.
- For self-hosted deployments, place Appwrite behind a reverse proxy (Traefik, Nginx) with TLS termination.
- **Forgetting to create attributes/indexes**: Unlike schemaless databases, Appwrite requires you to define attributes on collections before you can store or query those fields.
- **Hitting rate limits**: Appwrite enforces abuse protection by default; increase limits in `.env` for high-traffic self-hosted deployments.
- **Permission model confusion**: Mixing document-level and collection-level security modes on the same collection leads to unexpected access patterns — pick one model per collection.
- **Large file uploads without chunked upload**: For files over 5 MB, use the chunked upload API provided by the SDK.
- **Not configuring SMTP**: Email-based auth features (verification, password reset) silently fail without an SMTP provider configured.

## Quick Example

```bash
npm install -g appwrite-cli

appwrite login
appwrite init project
```

```bash
# Deploy via CLI
appwrite functions createDeployment \
  --functionId=<FUNCTION_ID> \
  --entrypoint='src/main.js' \
  --code='./functions/hello'
```
skilldb get baas-skills/AppwriteFull skill: 264 lines
Paste into your CLAUDE.md or agent config

Appwrite — Backend as a Service

You are an expert in Appwrite for rapid backend development, including its self-hosted architecture, database, authentication, storage, and serverless functions.

Core Philosophy

Overview

Appwrite is an open-source, self-hosted Backend-as-a-Service platform. It provides a REST and GraphQL API layer over a MariaDB-backed document database, user authentication with 30+ OAuth providers, file storage with built-in image transformations, serverless functions in multiple runtimes, and real-time event subscriptions. It can also be used via Appwrite Cloud for a managed experience.

Setup & Configuration

Self-Hosted Installation

# Docker Compose install (recommended)
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

# Appwrite will be available at http://localhost:80

CLI Setup

npm install -g appwrite-cli

appwrite login
appwrite init project

Client SDK (Web)

import { Client, Account, Databases, Storage } from 'appwrite';

const client = new Client()
  .setEndpoint('https://cloud.appwrite.io/v1')  // or self-hosted URL
  .setProject('<PROJECT_ID>');

const account = new Account(client);
const databases = new Databases(client);
const storage = new Storage(client);

Server SDK (Node.js)

import { Client, Databases, Users } from 'node-appwrite';

const client = new Client()
  .setEndpoint('https://cloud.appwrite.io/v1')
  .setProject('<PROJECT_ID>')
  .setKey('<API_KEY>');  // server-side API key

const databases = new Databases(client);
const users = new Users(client);

Core Patterns

Authentication

// Sign up
const user = await account.create(
  ID.unique(),
  'user@example.com',
  'secure-password',
  'Alice'  // optional name
);

// Create email session (sign in)
const session = await account.createEmailPasswordSession(
  'user@example.com',
  'secure-password'
);

// OAuth sign in
account.createOAuth2Session(
  OAuthProvider.Github,
  'http://localhost:3000/success',  // success redirect
  'http://localhost:3000/failure'   // failure redirect
);

// Get current user
const currentUser = await account.get();

// Sign out
await account.deleteSession('current');

Database

import { ID, Query } from 'appwrite';

const DATABASE_ID = 'main';
const COLLECTION_ID = 'posts';

// Create a document
const post = await databases.createDocument(
  DATABASE_ID,
  COLLECTION_ID,
  ID.unique(),
  {
    title: 'My Post',
    body: 'Content here',
    authorId: currentUser.$id,
    published: true,
  }
);

// Get a document
const doc = await databases.getDocument(DATABASE_ID, COLLECTION_ID, documentId);

// List with queries
const results = await databases.listDocuments(
  DATABASE_ID,
  COLLECTION_ID,
  [
    Query.equal('published', true),
    Query.orderDesc('$createdAt'),
    Query.limit(10),
  ]
);

// Update a document
await databases.updateDocument(DATABASE_ID, COLLECTION_ID, documentId, {
  title: 'Updated Title',
});

// Delete a document
await databases.deleteDocument(DATABASE_ID, COLLECTION_ID, documentId);

Permissions

import { Permission, Role } from 'appwrite';

// Create document with permissions
await databases.createDocument(
  DATABASE_ID,
  COLLECTION_ID,
  ID.unique(),
  { title: 'Private Post', body: 'Secret' },
  [
    Permission.read(Role.user(currentUser.$id)),
    Permission.update(Role.user(currentUser.$id)),
    Permission.delete(Role.user(currentUser.$id)),
  ]
);

// Collection-level permissions (set in console or via server SDK)
// - Document-level: each document has its own permissions
// - Collection-level: all documents share the collection's permissions

Real-time Subscriptions

const unsubscribe = client.subscribe(
  [`databases.${DATABASE_ID}.collections.${COLLECTION_ID}.documents`],
  (response) => {
    if (response.events.includes('databases.*.collections.*.documents.*.create')) {
      console.log('New document:', response.payload);
    }
    if (response.events.includes('databases.*.collections.*.documents.*.delete')) {
      console.log('Deleted document:', response.payload);
    }
  }
);

// Cleanup
unsubscribe();

Storage

import { ID } from 'appwrite';

const BUCKET_ID = 'avatars';

// Upload a file
const file = await storage.createFile(BUCKET_ID, ID.unique(), fileInput);

// Get file preview (with transformations)
const previewUrl = storage.getFilePreview(
  BUCKET_ID,
  file.$id,
  400,  // width
  400,  // height
  'center', // gravity
  80    // quality
);

// Download a file
const result = await storage.getFileDownload(BUCKET_ID, file.$id);

// Delete a file
await storage.deleteFile(BUCKET_ID, file.$id);

Serverless Functions

// functions/hello/src/main.js
export default async ({ req, res, log, error }) => {
  log('Function invoked');

  if (req.method === 'GET') {
    return res.json({ message: 'Hello from Appwrite Functions!' });
  }

  const body = JSON.parse(req.body);
  return res.json({ received: body });
};
# Deploy via CLI
appwrite functions createDeployment \
  --functionId=<FUNCTION_ID> \
  --entrypoint='src/main.js' \
  --code='./functions/hello'

Best Practices

  • Use document-level permissions for user-generated content and collection-level permissions for shared data like categories or tags.
  • Define collection attributes and indexes in the Appwrite Console or via the server SDK before inserting documents — Appwrite enforces schema validation.
  • Use the Appwrite CLI appwrite init to scaffold function boilerplate in your preferred runtime.
  • Set up webhooks or function triggers to react to auth and database events without polling.
  • For self-hosted deployments, place Appwrite behind a reverse proxy (Traefik, Nginx) with TLS termination.

Common Pitfalls

  • Forgetting to create attributes/indexes: Unlike schemaless databases, Appwrite requires you to define attributes on collections before you can store or query those fields.
  • Hitting rate limits: Appwrite enforces abuse protection by default; increase limits in .env for high-traffic self-hosted deployments.
  • Permission model confusion: Mixing document-level and collection-level security modes on the same collection leads to unexpected access patterns — pick one model per collection.
  • Large file uploads without chunked upload: For files over 5 MB, use the chunked upload API provided by the SDK.
  • Not configuring SMTP: Email-based auth features (verification, password reset) silently fail without an SMTP provider configured.

Anti-Patterns

Over-engineering for hypothetical requirements. Building for scenarios that may never materialize adds complexity without value. Solve the problem in front of you first.

Ignoring the existing ecosystem. Reinventing functionality that mature libraries already provide wastes time and introduces risk.

Premature abstraction. Creating elaborate frameworks before having enough concrete cases to know what the abstraction should look like produces the wrong abstraction.

Neglecting error handling at system boundaries. Internal code can trust its inputs, but boundaries with external systems require defensive validation.

Skipping documentation. What is obvious to you today will not be obvious to your colleague next month or to you next year.

Install this skill directly: skilldb add baas-skills

Get CLI access →