Communitygithub.com

agents-inc/skills

Appwrite Backend-as-a-Service – Authentifizierung, TablesDB, Storage, Funktionen, Echtzeit, Berechtigungsmodell, typisierter Client

Was ist skills?

Diese Skill-Anleitung bietet eine umfassende Einführung in die Nutzung von Appwrite, einer quelloffenen Backend-as-a-Service (BaaS)-Plattform. Sie deckt die Kernbereiche Authentifizierung, die strukturierte Datenbank TablesDB, Dateispeicher (Storage), serverlose Funktionen und Echtzeit-Abonnements ab. Die Anleitung legt besonderen Wert auf die obligatorische, explizite Setzung von Berechtigungen für jede Datenzeile und jede Datei, da Appwrite standardmäßig keinen Zugriff gewährt. Sie unterscheidet klar zwischen der Client-SDK (`appwrite`) für browserbasierte Sitzungsauthentifizierung und der Server-SDK (`node-appwrite`) für die API-Key-basierte Authentifizierung im Backend. Die Dokumentation enthält detaillierte Codebeispiele für die Client- und Server-Initialisierung, CRUD-Operationen auf TablesDB, das Berechtigungsmodell mit `Permission` und `Role`, Authentifizierungsabläufe, Dateiverwaltung und die Fehlerbehandlung mittels `AppwriteException`. Sie richtet sich an Entwickler, die Appwrite in TypeScript-Projekten einsetzen und legt einen starken Fokus auf Sicherheit und korrekte SDK-Nutzung.

Funktioniert mit✓Claude Code~Codex CLI✓Cursor
npx skills add https://github.com/agents-inc/skills/tree/HEAD/src/skills/api-baas-appwrite

In Ihrer bevorzugten KI fragen

Öffnet einen neuen Chat, in dem dieser Agent-Skill bereits geladen ist.

Dokumentation

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 und realtime.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:

Echtzeit:


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:

  1. 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.
  2. Service-Klassen-Architektur – Alle SDK-Interaktionen erfolgen über Service-Klassen (Account, TablesDB, Storage, Functions, Teams, Realtime), die von einer gemeinsamen Client-Instanz instanziiert werden. Dieses Muster ist sowohl im Client- als auch im Server-SDK konsistent.
  3. 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.
  4. Exceptions, keine Tupel – Appwrite wirft bei Fehlern AppwriteException, nicht { data, error }-Tupel. Wickeln Sie Aufrufe immer in try/catch ein.
  5. 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.
  6. 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

Individual skills in this repo

This repo contains 11 individual skills — each has its own dedicated page.

Verwandte Skills