Appwrite Patterns
Quick Guide: Use Appwrite as your open-source BaaS for authentication, structured data (TablesDB), file storage, serverless functions, and realtime subscriptions. Always initialize service classes from a sharedClientinstance, set permissions explicitly on every row (nothing is accessible by default), and use the server SDK (node-appwrite) with API key auth only on the backend.
<critical_requirements>
CRITICAL: Before Using This Skill
All code must follow project conventions in CLAUDE.md (kebab-case, named exports, import ordering, import type, named constants)
(You MUST set permissions explicitly on every row and file — Appwrite grants NO access by default, so omitting permissions makes data inaccessible)
(You MUST use node-appwrite with API key auth on the server and appwrite with session auth on the client — NEVER expose API keys in client bundles)
(You MUST always check for AppwriteException on every SDK call — Appwrite throws exceptions, it does NOT return { data, error } tuples)
(You MUST use ID.unique() for auto-generated IDs — passing a raw string creates a custom ID, not an auto-generated one)
(You MUST use the TablesDB API for new projects — the legacy Databases/collections/documents API is deprecated and receives only security patches)
</critical_requirements>
Auto-detection: Appwrite, appwrite, node-appwrite, TablesDB, Account, Databases, Permission, Role, ID.unique, Query.equal, createEmailPasswordSession, realtime.subscribe, Channel.files, Channel.tablesdb, APPWRITE_FUNCTION
When to use:
- Setting up an Appwrite client or server SDK with TypeScript
- Implementing authentication (email/password, OAuth, magic URL, phone OTP, anonymous sessions)
- Performing CRUD on TablesDB tables (rows, queries, permissions)
- Uploading and serving files from Storage buckets with access control
- Building serverless functions triggered by events, schedules, or HTTP
- Subscribing to realtime changes on rows, files, or account events
- Managing team memberships and role-based permissions
Key patterns covered:
- Client and server SDK initialization with typed service classes
- Auth flows: sign up, sign in, OAuth, magic URL, session management
- TablesDB operations with
Queryclass for filtering, pagination, ordering - Permission model:
Permission.read(Role.any()),Role.user(),Role.team() - Storage: upload, download, preview with image transforms, bucket permissions
- Serverless functions:
export default async ({ req, res, log, error }) => {} - Realtime subscriptions via
Channelhelpers andrealtime.subscribe()
When NOT to use:
- Direct database connections or SQL queries (Appwrite is API-only, no raw SQL)
- Complex relational joins across many tables (Appwrite supports relationships but not arbitrary SQL joins)
- Applications requiring server-side realtime (Appwrite Realtime is client SDK only)
Detailed Resources:
- For decision frameworks and anti-patterns, see [reference.md](reference.md)
Client & Queries:
- [examples/core.md](examples/core.md) — Client setup, TablesDB CRUD, error handling, permissions
Authentication:
- [examples/auth.md](examples/auth.md) — Full auth flows, OAuth, magic URL, sessions, teams
Storage & Functions:
- [examples/storage.md](examples/storage.md) — File upload, download, previews, bucket permissions
- [examples/functions.md](examples/functions.md) — Serverless functions, triggers, SDK usage inside functions
Realtime:
- [examples/realtime.md](examples/realtime.md) — Channel subscriptions, event filtering, cleanup
<philosophy>
Philosophy
Appwrite is an open-source backend-as-a-service providing authentication, databases (TablesDB), file storage, serverless functions, and realtime — all through a unified SDK. It can be self-hosted or used via Appwrite Cloud.
Core principles:
- Secure by default — Nothing is accessible without explicit permissions. Every row and file must have permissions set, or it is invisible to all users. This is the opposite of "open by default" — treat it as a mandatory step, not an afterthought.
- Service class architecture — All SDK interactions go through service classes (
Account,TablesDB,Storage,Functions,Teams,Realtime) instantiated from a sharedClient. This pattern is consistent across client and server SDKs. - Two SDK split —
appwrite(client) uses session-based auth for browsers.node-appwrite(server) uses API key auth for backends. They share the same API shape but different auth mechanisms. Never mix them. - Exceptions, not tuples — Appwrite throws
AppwriteExceptionon failure, not{ data, error }tuples. Always wrap calls in try/catch. - TablesDB is the future — Appwrite 1.8 introduced TablesDB (tables/rows/columns) as the modern API. The legacy Databases API (collections/documents/attributes) still works but is deprecated. New features only land in TablesDB.
- ID generation — Use
ID.unique()for server-generated unique IDs. If you pass a string directly, it becomes a custom ID (which must be globally unique within the table).
When to use Appwrite:
- Open-source, self-hostable BaaS with full control over your data
- Projects needing auth, database, storage, and functions in one platform
- Teams wanting a self-hostable BaaS with no vendor lock-in
- Applications that benefit from granular document/row-level permissions
When NOT to use:
- Complex SQL-heavy applications requiring joins, views, and stored procedures
- Server-side realtime consumers (Appwrite Realtime is client SDK only — no server SDK support)
- Offline-first apps requiring local-first sync (Appwrite has no built-in offline sync)
</philosophy>
<patterns>
Core Patterns
Pattern 1: Client SDK Setup (Browser)
Initialize the Appwrite client with endpoint and project ID. All service classes share a single Client instance.
// lib/appwrite.ts
import { Client, Account, TablesDB, Storage, Realtime } from "appwrite";
const APPWRITE_ENDPOINT = process.env.APPWRITE_ENDPOINT!;
const APPWRITE_PROJECT_ID = process.env.APPWRITE_PROJECT_ID!;
const client = new Client()
.setEndpoint(APPWRITE_ENDPOINT)
.setProject(APPWRITE_PROJECT_ID);
export const account = new Account(client);
export const tablesDB = new TablesDB(client);
export const storage = new Storage(client);
export const realtime = new Realtime(client);
Why good: Single client instance shared across services, named constants for config, named exports for each service, environment variables keep config out of code
// BAD: Hardcoded config, recreating clients
import { Client, Account } from "appwrite";
const account = new Account(
new Client()
.setEndpoint("https://cloud.appwrite.io/v1") // Hardcoded
.setProject("abc123"), // Hardcoded project ID
);
Why bad: Hardcoded endpoint and project ID, new Client per service wastes resources, no shared config
Pattern 2: Server SDK Setup (Node.js)
The server SDK uses API key authentication — never expose API keys in client code.
// lib/appwrite-server.ts
import { Client, TablesDB, Users, Storage } from "node-appwrite";
const APPWRITE_ENDPOINT = process.env.APPWRITE_ENDPOINT!;
const APPWRITE_PROJECT_ID = process.env.APPWRITE_PROJECT_ID!;
const APPWRITE_API_KEY = process.env.APPWRITE_API_KEY!;
const client = new Client()
.setEndpoint(APPWRITE_ENDPOINT)
.setProject(APPWRITE_PROJECT_ID)
.setKey(APPWRITE_API_KEY);
export const tablesDB = new TablesDB(client);
export const users = new Users(client);
export const storage = new Storage(client);
Why good: .setKey() for API key auth (server only), Users service for admin user management (not available in client SDK), named constants
When to use: API routes, serverless functions, admin scripts, webhooks. NEVER import this module from clien