Appwrite Patterns
Kurzanleitung: Nutzen Sie Appwrite als Ihr quelloffenes BaaS für Authentifizierung, strukturierte Daten (TablesDB), Dateispeicher, serverlose Funktionen und Echtzeit-Abonnements. Initialisieren Sie Service-Klassen immer von einer gemeinsamen
Client-Instanz, setzen Sie Berechtigungen explizit für jede Zeile (standardmäßig ist nichts zugänglich) und verwenden Sie das Server-SDK (node-appwrite) mit API-Key-Authentifizierung ausschließlich im Backend.
<critical_requirements>
KRITISCH: Vor der Verwendung dieser Skill
Der gesamte Code muss den Projektkonventionen in CLAUDE.md folgen (kebab-case, benannte Exporte, Import-Reihenfolge,
import type, benannte Konstanten)
(Sie MÜSSEN Berechtigungen explizit für jede Zeile und jede Datei setzen – Appwrite gewährt standardmäßig KEINEN Zugriff, sodass das Weglassen von Berechtigungen Daten unzugänglich macht)
(Sie MÜSSEN node-appwrite mit API-Key-Authentifizierung auf dem Server und appwrite mit Sitzungsauthentifizierung auf dem Client verwenden – EXPONIEREN Sie NIEMALS API-Keys in Client-Bundles)
(Sie MÜSSEN bei jedem SDK-Aufruf immer auf AppwriteException prüfen – Appwrite wirft Exceptions und gibt KEINE { data, error }-Tupel zurück)
(Sie MÜSSEN ID.unique() für automatisch generierte IDs verwenden – die Übergabe eines rohen Strings erstellt eine benutzerdefinierte ID, keine automatisch generierte)
(Sie MÜSSEN für neue Projekte die TablesDB-API verwenden – die veraltete Databases/collections/documents-API ist deprecated und erhält nur noch Sicherheitspatches)
</critical_requirements>
Auto-Erkennung: Appwrite, appwrite, node-appwrite, TablesDB, Account, Databases, Permission, Role, ID.unique, Query.equal, createEmailPasswordSession, realtime.subscribe, Channel.files, Channel.tablesdb, APPWRITE_FUNCTION
Wann verwenden:
- Einrichten des Appwrite-Client- oder Server-SDKs mit TypeScript
- Implementieren der Authentifizierung (E-Mail/Passwort, OAuth, Magic URL, Phone OTP, anonyme Sitzungen)
- CRUD-Operationen auf TablesDB-Tabellen (Zeilen, Abfragen, Berechtigungen)
- Hochladen und Bereitstellen von Dateien aus Storage-Buckets mit Zugriffskontrolle
- Erstellen serverloser Funktionen, die durch Ereignisse, Zeitpläne oder HTTP ausgelöst werden
- Abonnieren von Echtzeitänderungen an Zeilen, Dateien oder Kontoereignissen
- Verwalten von Teammitgliedschaften und rollenbasierten Berechtigungen
Abgedeckte Schlüsselmuster:
- Client- und Server-SDK-Initialisierung mit typisierten Service-Klassen
- Auth-Abläufe: Registrierung, Anmeldung, OAuth, Magic URL, Sitzungsverwaltung
- TablesDB-Operationen mit der
Query-Klasse für Filterung, Paginierung, Sortierung - Berechtigungsmodell:
Permission.read(Role.any()),Role.user(),Role.team() - Storage: Hochladen, Herunterladen, Vorschau mit Bildtransformationen, Bucket-Berechtigungen
- Serverlose Funktionen:
export default async ({ req, res, log, error }) => {} - Echtzeit-Abonnements über
Channel-Hilfsfunktionen undrealtime.subscribe()
Wann NICHT verwenden:
- Direkte Datenbankverbindungen oder SQL-Abfragen (Appwrite ist reine API, kein rohes SQL)
- Komplexe relationale Joins über viele Tabellen hinweg (Appwrite unterstützt Beziehungen, aber keine beliebigen SQL-Joins)
- Anwendungen, die serverseitige Echtzeitfunktionen erfordern (Appwrite Realtime ist nur für das Client-SDK verfügbar)
Detaillierte Ressourcen:
- Für Entscheidungsrahmen und Anti-Patterns siehe reference.md
Client & Abfragen:
- examples/core.md – Client-Setup, TablesDB CRUD, Fehlerbehandlung, Berechtigungen
Authentifizierung:
- examples/auth.md – Vollständige Auth-Abläufe, OAuth, Magic URL, Sitzungen, Teams
Storage & Funktionen:
- examples/storage.md – Datei-Upload, -Download, Vorschauen, Bucket-Berechtigungen
- examples/functions.md – Serverlose Funktionen, Trigger, SDK-Nutzung in Funktionen
Echtzeit:
- examples/realtime.md – Kanal-Abonnements, Ereignisfilterung, Bereinigung
Philosophie
Appwrite ist ein quelloffenes Backend-as-a-Service, das Authentifizierung, Datenbanken (TablesDB), Dateispeicher, serverlose Funktionen und Echtzeitfunktionen über ein einheitliches SDK bereitstellt. Es kann selbst gehostet oder über Appwrite Cloud genutzt werden.
Grundprinzipien:
- Standardmäßig sicher – Nichts ist ohne explizite Berechtigungen zugänglich. Jede Zeile und jede Datei muss Berechtigungen haben, sonst ist sie für alle Benutzer unsichtbar. Dies ist das Gegenteil von „standardmäßig offen“ – behandeln Sie es als zwingenden Schritt, nicht als nachträglichen Einfall.
- Service-Klassen-Architektur – Alle SDK-Interaktionen erfolgen über Service-Klassen (
Account,TablesDB,Storage,Functions,Teams,Realtime), die von einer gemeinsamenClient-Instanz instanziiert werden. Dieses Muster ist sowohl im Client- als auch im Server-SDK konsistent. - Zwei SDKs –
appwrite(Client) verwendet sitzungsbasierte Authentifizierung für Browser.node-appwrite(Server) verwendet API-Key-Authentifizierung für Backends. Sie teilen dieselbe API-Struktur, aber unterschiedliche Authentifizierungsmechanismen. Mischen Sie sie niemals. - Exceptions, keine Tupel – Appwrite wirft bei Fehlern
AppwriteException, nicht{ data, error }-Tupel. Wickeln Sie Aufrufe immer in try/catch ein. - TablesDB ist die Zukunft – Appwrite 1.8 führte TablesDB (Tabellen/Zeilen/Spalten) als moderne API ein. Die veraltete Databases-API (Sammlungen/Dokumente/Attribute) funktioniert noch, ist aber deprecated. Neue Funktionen landen nur in TablesDB.
- ID-Generierung – Verwenden Sie
ID.unique()für servergenerierte eindeutige IDs. Wenn Sie einen String direkt übergeben, wird daraus eine benutzerdefinierte ID (die innerhalb der Tabelle global eindeutig sein muss).
Wann Appwrite verwenden:
- Quelloffenes, selbst hostbares BaaS mit voller Kontrolle über Ihre Daten
- Projekte, die Auth, Datenbank, Storage und Funktionen auf einer Plattform benötigen
- Teams, die ein selbst hostbares BaaS ohne Vendor-Lock-in wünschen
- Anwendungen, die von granularen Berechtigungen auf Dokument-/Zeilenebene profitieren
Wann NICHT verwenden:
- Komplexe SQL-lastige Anwendungen, die Joins, Views und gespeicherte Prozeduren erfordern
- Serverseitige Echtzeitkonsumenten (Appwrite Realtime ist nur für das Client-SDK – kein Server-SDK-Support)
- Offline-First-Apps, die lokale Synchronisation erfordern (Appwrite hat keine integrierte Offline-Synchronisation)
Kernmuster
Muster 1: Client SDK einrichten (Browser)
Initialisieren Sie den Appwrite-Client mit Endpunkt und Projekt-ID. Alle Service-Klassen teilen sich eine einzige Client-Instanz.
// 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);
Warum gut: Einzige Client-Instanz, die von allen Diensten gemeinsam genutzt wird; benannte Konstanten für die Konfiguration; benannte Exporte für jeden Dienst; Umgebungsvariablen halten die Konfiguration aus dem Code heraus
// SCHLECHT: Hartcodierte Konfiguration, Clients werden neu erstellt
import { Client, Account } from "appwrite";
const account = new Account(
new Client()
.setEndpoint("https://cloud.appwrite.io/v1") // Hartcodiert
.setProject("abc123"), // Hartcodierte Projekt-ID
);
Warum schlecht: Hartcodierte Endpunkt- und Projekt-ID; neuer Client pro Dienst verschwendet Ressourcen; keine gemeinsame Konfiguration
Muster 2: Server SDK einrichten (Node.js)
Das Server-SDK verwendet API-Key-Authentifizierung – exponieren Sie API-Keys niemals im 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);
Warum gut: .setKey() für API-Key-Authentifizierung (nur Server), Users-Service für die Benutzerverwaltung (nicht im Client-SDK verfügbar), benannte Konstanten
Wann verwenden: API-Routen, serverlose Funktionen, Admin-Skripte, Webhooks. Importieren Sie dieses Modul NIEMALS im clientseitigen Code.
Muster 3: Fehlerbehandlung (AppwriteException)
Appwrite wirft AppwriteException – es gibt KEINE { data, error }-Tupel zurück. Verwenden Sie immer try/catch.
import { AppwriteException, type Models } from "appwrite";
async function getUser(): Promise<Models.User<Models.Preferences>> {
try {
return await account.get();
} catch (error) {
if (error instanceof AppwriteException) {
// error.message – lesbare Nachricht
// error.code – HTTP-Statuscode (401, 404, etc.)
// error.type – maschinenlesbarer Fehlertyp-String
throw new Error(`Appwrite-Fehler ${error.code}: ${error.message}`);
}
throw error;
}
}
Warum gut: Typ-verengtes AppwriteException bietet code, message, type; unbekannte Fehler werden erneut geworfen; expliziter Rückgabetyp mittels Models-Namespace
// SCHLECHT: Annahme eines { data, error }-Tupel-Rückgabewerts
const { data, error } = await account.get(); // FALSCH – Appwrite wirft, gibt keine Tupel zurück
if (error) {
/* ... */
}
Warum schlecht: Appwrite gibt KEINE Fehler-Tupel zurück – dieser Code wird stillschweigend fehlschlagen, da account.get() entweder Daten zurückgibt oder eine Exception wirft
Muster 4: TablesDB-Operationen (CRUD)
Verwenden Sie TablesDB für alle Datenbankoperationen. Zeilen-IDs sind für Create erforderlich – verwenden Sie ID.unique() für die automatische Generierung.
Eine Zeile erstellen
import { ID, Permission, Role } from "appwrite";
interface Todo {
title: string;
completed: boolean;
}
const DATABASE_ID = "main";
const TABLE_ID = "todos";
const row = await tablesDB.createRow<Todo>({
databaseId: DATABASE_ID,
tableId: TABLE_ID,
rowId: ID.unique(),
data: {
title: "Lebensmittel einkaufen",
completed: false,
},
permissions: [
Permission.read(Role.user(userId)),
Permission.update(Role.user(userId)),
Permission.delete(Role.user(userId)),
],
});
Zeilen mit Abfragen auflisten
import { Query } from "appwrite";
const PAGE_SIZE = 25;
const result = await tablesDB.listRows<Todo>({
databaseId: DATABASE_ID,
tableId: TABLE_ID,
queries: [
Query.equal("completed", false),
Query.orderDesc("$createdAt"),
Query.limit(PAGE_SIZE),
],
});
// result.rows – Array von Todo-Zeilen
// result.total – Gesamtzahl der Treffer
Eine Zeile aktualisieren
const updated = await tablesDB.updateRow<Todo>({
databaseId: DATABASE_ID,
tableId: TABLE_ID,
rowId: row.$id,
data: { completed: true },
});
Eine Zeile löschen
await tablesDB.deleteRow({
databaseId: DATABASE_ID,
tableId: TABLE_ID,
rowId: row.$id,
});
Warum gut: Benannte Konstanten für Datenbank-/Tabellen-IDs, ID.unique() für automatisch generierte IDs, Berechtigungen explizit beim Erstellen gesetzt, Query-Klasse für typsichere Filterung, generischer Typparameter für Zeilenform
Muster 5: Berechtigungsmodell
Appwrite gewährt standardmäßig KEINEN Zugriff. Sie müssen Berechtigungen für jede Zeile und jede Datei setzen.
Permission + Role Syntax
import { Permission, Role } from "appwrite";
// Öffentlich lesbar, Besitzer hat vollen Zugriff
const publicReadPermissions = [
Permission.read(Role.any()),
Permission.update(Role.user(userId)),
Permission.delete(Role.user(userId)),
];
// Team-Zugriff
const teamPermissions = [
Permission.read(Role.team(teamId)),
Permission.update(Role.team(teamId, "admin")),
Permission.delete(Role.team(teamId, "owner")),
];
// Nur verifizierte Benutzer
const verifiedOnlyPermissions = [Permission.read(Role.users("verified"))];
Häufige Role-Helfer
Role.any(); // Jeder (auch Gäste)
Role.guests(); // Nur nicht authentifizierte Benutzer
Role.users(); // Alle authentifizierten Benutzer
Role.users("verified"); // Nur verifizierte Benutzer
Role.user(userId); // Bestimmter Benutzer nach ID
Role.team(teamId); // Alle Mitglieder eines Teams
Role.team(teamId, "admin"); // Teammitglieder mit der Rolle "admin"
Role.member(membershipId); // Bestimmte Teammitgliedschaft
Role.label("premium"); // Benutzer mit dem Label "premium"
Warum gut: Explizite Berechtigungen für jede Ressource, Rollen-Helfer sind typsicher, Team-Rollen ermöglichen granularen Zugriff
// SCHLECHT: Erstellen einer Zeile ohne Berechtigungen
const row = await tablesDB.createRow({
databaseId: DATABASE_ID,
tableId: TABLE_ID,
rowId: ID.unique(),
data: { title: "Geheimnis" },
// KEINE Berechtigungen gesetzt – diese Zeile ist für JEDEN unsichtbar!
});
Warum schlecht: Ohne Berechtigungen existiert die Zeile in der Datenbank, aber KEIN Benutzer kann sie lesen, aktualisieren oder löschen (nicht einmal der Ersteller) – dies ist der häufigste Appwrite-Fehler
Muster 6: Authentifizierungsabläufe
Verwenden Sie den Account-Service für alle Auth-Operationen.
Registrierung und Anmeldung mit E-Mail/Passwort
import { ID, type Models } from "appwrite";
// Registrierung: Konto erstellen + Sitzung erstellen (zwei Schritte)
const user = await account.create({
userId: ID.unique(),
email,
password,
name,
});
const session = await account.createEmailPasswordSession({ email, password });
// Anmeldung
const session = await account.createEmailPasswordSession({ email, password });
// Abmeldung (aktuelles Gerät)
await account.deleteSession({ sessionId: "current" });
Warum gut: Zweistufige Registrierung (Konto erstellen + Sitzung erstellen), Objektparameter für alle SDK-Aufrufe, deleteSession({ sessionId: "current" }) nur für das aktuelle Gerät
Vollständige Auth-Abläufe inklusive OAuth, Magic URL, anonymer Sitzungen, Teams und Passwort-Reset finden Sie unter examples/auth.md.
Muster 7: Storage-Operationen
Laden Sie Dateien mit dem Storage-Service hoch, herunter und als Vorschau anzuzeigen.
import { ID, Permission, Role } from "appwrite";
const BUCKET_ID = "user-uploads";
const PREVIEW_WIDTH = 400;
const PREVIEW_HEIGHT = 300;
// Hochladen mit Berechtigungen
await storage.createFile({
bucketId: BUCKET_ID,
fileId: ID.unique(),
file,
permissions: [
Permission.read(Role.user(userId)),
Permission.update(Role.user(userId)),
],
});
// Bildvorschau mit Transformationen
const url = storage.getFilePreview({
bucketId: BUCKET_ID,
fileId: fileId,
width: PREVIEW_WIDTH,
height: PREVIEW_HEIGHT,
});
Warum gut: Explizite Bucket-ID, ID.unique() für Datei-IDs, Berechtigungen beim Hochladen gesetzt, Bildvorschau-URLs mit Abmessungen
Vollständige Storage-Beispiele finden Sie unter examples/storage.md.
Muster 8: Serverlose Funktionen
Appwrite-Funktionen werden in Node.js ausgeführt und erhalten req und res als Parameter.
// functions/hello-world/src/index.ts
import { Client, Account } from "appwrite";
export default async ({ req, res, log, error }: any) => {
const client = new Client()
.setEndpoint(process.env.APPWRITE_ENDPOINT!)
.setProject(process.env.APPWRITE_PROJECT_ID!)
.setKey(process.env.APPWRITE_API_KEY!);
const account = new Account(client);
try {
const user = await account.get();
return res.json({ user });
} catch (error) {
return res.json({ error: "Nicht autorisiert" }, 401);
}
};
Warum gut: Nutzt Umgebungsvariablen für die Konfiguration, erstellt eine neue Client-Instanz innerhalb der Funktion, verwendet res.json() für strukturierte Antworten, behandelt AppwriteException mit try/catch
Weitere Funktionen-Beispiele finden Sie unter examples/functions.md.
Muster 9: Echtzeit-Abonnements
Mit dem Realtime-Service können Sie Änderungen an Zeilen, Dateien und Kontoereignissen abonnieren.
import { Channel } from "appwrite";
const DATABASE_ID = "main";
const TABLE_ID = "todos";
// Alle Änderungen an einer Tabelle abonnieren
const unsubscribe = realtime.subscribe({
channels: [Channel.tablesdb(DATABASE_ID, TABLE_ID)],
onEvent: (event) => {
console.log("Ereignis empfangen:", event);
// event.payload – die geänderte Zeile
// event.events – Array von Ereignis-Strings (z. B. ["databases.*.tables.*.row.*.create"])
// event.channels – Array von abonnierten Kanälen
},
});
// Später: Abbestellen
unsubscribe();
Warum gut: Verwendet Channel-Helfer für typsichere Kanal-Strings, subscribe gibt eine unsubscribe-Funktion zurück, Ereignis-Payload enthält die geänderten Daten
Weitere Echtzeit-Beispiele finden Sie unter examples/realtime.md.
Referenzen
- reference.md – Entscheidungsrahmen und Anti-Patterns
- examples/core.md – Client-Setup, TablesDB CRUD, Fehlerbehandlung, Berechtigungen
- examples/auth.md – Vollständige Auth-Abläufe, OAuth, Magic URL, Sitzungen, Teams
- examples/storage.md – Datei-Upload, -Download, Vorschauen, Bucket-Berechtigungen
- examples/functions.md – Serverlose Funktionen, Trigger, SDK-Nutzung in Funktionen
- examples/realtime.md – Kanal-Abonnements, Ereignisfilterung, Bereinigung