Claude Skill

api-baas-appwrite

Appwrite backend-as-a-service — Auth, TablesDB, Storage, Functions, Realtime, permissions model, typed client

LLM Mart · 0 points · 0 views 0 listing impressions 0 install-command copies
Virus-scanned Reviewed automatically before listing.

Full trust report

Download agents-inc-skills-dist_plugins_api-baas-appwrite_skills_api-baas-appwrite-3a51ef5.zip · 23 KB
Part of agents-inc/skills — 130 skills

Install

skills CLI npx skills add https://github.com/agents-inc/skills/tree/main/dist/plugins/api-baas-appwrite/skills/api-baas-appwrite
Claude Code claude plugin marketplace add https://llmmart.ai/marketplace.json && claude plugin install agents-inc-skills@llmmart
Git git clone https://github.com/agents-inc/skills.git

The skills CLI installs just this skill, for any of its supported agents. Claude Code installs the whole agents-inc/skills collection as a plugin from our marketplace. Git is the plain clone.

Skill manifest

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 shared Client instance, 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 Query class 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 Channel helpers and realtime.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

Client & Queries:

Authentication:

Storage & Functions:

Realtime:




<decision_framework>

Decision Framework

Client SDK vs Server SDK

Where is the code running?
+-  Browser / Client-side -> `appwrite` package, session auth
+-  Server / API route -> `node-appwrite` package, API key auth
+-  Serverless function -> `node-appwrite` package, dynamic key from `req.headers['x-appwrite-key']`
    +-- NEVER expose API keys in client bundles

TablesDB vs Legacy Databases

Which API should I use?
+-  New project -> TablesDB (tables, rows, columns)
+-  Existing project with collections -> Legacy Databases still works (deprecated, security patches only)
+-  Migrating -> Adopt TablesDB for new tables, migrate existing gradually

Auth Method Selection

What auth flow does the user need?
+-  Email + Password -> account.create({ ... }) + account.createEmailPasswordSession({ ... })
+-  Social login (GitHub, Google, etc.) -> account.createOAuth2Session({ provider: OAuthProvider.Github, ... })
+-  Passwordless email -> account.createMagicURLToken({ ... })
+-  Phone + SMS -> account.createPhoneToken({ ... })
+-  Guest / anonymous -> account.createAnonymousSession()
+-  Existing JWT -> client.setJWT()

Permissions Strategy

Who should access this resource?
+-  Public (anyone) -> Permission.read(Role.any())
+-  Owner only -> Permission.read(Role.user(userId))
+-  Team members -> Permission.read(Role.team(teamId))
+-  Team role (e.g. admins) -> Permission.read(Role.team(teamId, "admin"))
+-  Verified users -> Permission.read(Role.users("verified"))
+-  Labeled users -> Permission.read(Role.label("premium"))

Storage: Public vs Private

Who should see the files?
+-  Anyone (public assets) -> Set Permission.read(Role.any()) on the file
+-  Owner only (private docs) -> Set Permission.read(Role.user(userId))
+-  Team access -> Set Permission.read(Role.team(teamId))
+-  Server-only -> Use server SDK with API key (bypasses all permissions)

</decision_framework>


<red_flags>

RED FLAGS

High Priority Issues:

  • No permissions on rows/files — Appwrite grants ZERO access by default. A row without permissions is completely invisible. This is the number one mistake new Appwrite developers make.
  • API key in client bundle — The node-appwrite server SDK with API key auth must NEVER be imported in browser code. API keys bypass all permissions.
  • Assuming { data, error } return pattern — Appwrite throws AppwriteException, not error tuples. Using destructuring like const { data, error } = await account.get() will fail silently.
  • Using legacy Databases API for new projects — Databases (collections/documents) is deprecated as of Appwrite 1.8. Use TablesDB (tables/rows) for all new work.
  • Using positional parameters — As of appwrite@19.0.0 / node-appwrite@18.0.0 (September 2025), all SDK methods use object parameters. Positional arguments are deprecated. Use account.create({ userId, email, password }) not account.create(userId, email, password).
  • Using string literals for OAuth providers — Use the OAuthProvider enum: OAuthProvider.Github not "github".

Medium Priority Issues:

  • Forgetting ID.unique() — Passing a raw string as a row/file ID creates a custom ID. If you want auto-generated IDs, you must explicitly call ID.unique().
  • Not unsubscribing from Realtime — Each realtime.subscribe() adds to a shared WebSocket. Failing to subscription.close() leaks connections and causes duplicate events.
  • Using Permission.write() when you mean specific actions — Permission.write() is an alias for create + update + delete combined. Use Permission.update() and Permission.delete() separately for granular control.
  • Calling account.create() without createEmailPasswordSession() — create() registers the user but does NOT create a session. The user is not logged in until you call a session method.
  • Server SDK Realtime — Appwrite Realtime is NOT available through server SDKs. Subscriptions only work with client SDKs.

Common Mistakes:

  • OAuth: no code runs after createOAuth2Session() — This method triggers a browser redirect. Any code after the call will not execute.
  • Not setting both success AND failure URLs for OAuth — If you omit the failure URL, failed auth has no redirect target.
  • Using Role.users() when you mean Role.user(userId) — Role.users() grants access to ALL authenticated users. Role.user(userId) grants access to ONE specific user.
  • Expecting $createdAt / $updatedAt in queries without $ prefix — System fields in Appwrite use $ prefix: $id, $createdAt, $updatedAt, $permissions.

Gotchas & Edge Cases:

  • Row security vs table-level permissions — By default, only table-level permissions apply. To use row-level (document-level) permissions, you must enable "Row Security" in the table settings. When enabled, BOTH table-level AND row-level permissions must pass.
  • Permission.read(Role.any()) includes guests — Role.any() means everyone, including unauthenticated users. Use Role.users() for authenticated-only access.
  • Realtime reconnection creates new WebSocket — Adding or removing a subscription tears down and recreates the entire WebSocket connection. Batch subscription changes where possible.
  • File preview only works for images — storage.getFilePreview() with width/height transforms only works on image files. For non-images, use getFileDownload() or getFileView().
  • account.get() throws if no session — There is no "null user" return. An unauthenticated call throws 401. Wrap in try/catch and return null for "no user" state.
  • Max 100 rows per listRows() call — The default and maximum limit is 100. For larger datasets, use cursor-based pagination with Query.cursorAfter().
  • Appwrite function cold starts — Functions have cold start latency on first invocation after idle. Design "fat functions" (one function handling multiple routes) to reduce cold starts.
  • Query.search() requires a full-text index — The search query method only works on columns that have a full-text index configured in the Appwrite console.
  • Labels are server-only — User labels (Role.label("premium")) can only be set via the server SDK users.updateLabels(). Clients cannot modify their own labels.

</red_flags>


<critical_reminders>

CRITICAL REMINDERS

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)

Failure to follow these rules will create inaccessible data, security vulnerabilities, and silent runtime failures.

</critical_reminders>

Files (skills)
  • examples
    • auth.md 10.2 KB
      # Appwrite Auth Examples
      
      > Full auth flows, OAuth, magic URL, sessions, and team management. See [SKILL.md](../SKILL.md) for core concepts.
      
      ---
      
      ## Pattern 1: Email/Password Authentication
      
      ### Good Example — Sign Up (Create Account + Session)
      
      ```typescript
      import { ID, AppwriteException, type Models } from "appwrite";
      
      async function signUp(
        email: string,
        password: string,
        name: string,
      ): Promise<{ user: Models.User<Models.Preferences>; session: Models.Session }> {
        try {
          // Step 1: Create the user account
          const user = await account.create({
            userId: ID.unique(),
            email,
            password,
            name,
          });
      
          // Step 2: Create a session (account.create does NOT log the user in)
          const session = await account.createEmailPasswordSession({
            email,
            password,
          });
      
          return { user, session };
        } catch (error) {
          if (error instanceof AppwriteException) {
            throw new Error(`Sign up failed: ${error.message}`);
          }
          throw error;
        }
      }
      ```
      
      **Why good:** Two-step process (create account + create session), object parameters for all SDK calls, `ID.unique()` for user ID, typed return with `Models` namespace, `AppwriteException` handling
      
      ### Good Example — Sign In
      
      ```typescript
      async function signIn(
        email: string,
        password: string,
      ): Promise<Models.Session> {
        try {
          return await account.createEmailPasswordSession({ email, password });
        } catch (error) {
          if (error instanceof AppwriteException) {
            // Don't expose whether the email exists — generic message
            throw new Error("Invalid email or password");
          }
          throw error;
        }
      }
      ```
      
      **Why good:** Generic error message prevents user enumeration, returns session for immediate use
      
      ### Bad Example — Forgetting to Create Session After Account Creation
      
      ```typescript
      // BAD: User account created but NOT logged in
      async function signUp(email: string, password: string) {
        const user = await account.create({ userId: ID.unique(), email, password });
        return user; // User exists but has no active session!
      }
      ```
      
      **Why bad:** `account.create()` only registers the user — it does NOT create a session. The user will appear "not logged in" until `createEmailPasswordSession()` is called.
      
      ---
      
      ## Pattern 2: OAuth (Social Login)
      
      ### Good Example — GitHub OAuth
      
      ```typescript
      import { OAuthProvider } from "appwrite";
      
      function signInWithGitHub() {
        account.createOAuth2Session({
          provider: OAuthProvider.Github,
          success: `${window.location.origin}/auth/callback`,
          failure: `${window.location.origin}/auth/failure`,
          scopes: ["read:user", "user:email"],
        });
        // IMPORTANT: Browser redirects here — no code after this runs
      }
      ```
      
      ### Good Example — Google OAuth
      
      ```typescript
      import { OAuthProvider } from "appwrite";
      
      function signInWithGoogle() {
        account.createOAuth2Session({
          provider: OAuthProvider.Google,
          success: `${window.location.origin}/auth/callback`,
          failure: `${window.location.origin}/auth/failure`,
          scopes: ["openid", "email", "profile"],
        });
      }
      ```
      
      ### Good Example — OAuth Callback Handler
      
      ```typescript
      // /auth/callback page — session is automatically established after redirect
      async function handleOAuthCallback() {
        try {
          const user = await account.get();
          // User is now authenticated — redirect to app
          return user;
        } catch {
          // Session wasn't established — redirect to login
          window.location.href = "/login";
        }
      }
      ```
      
      **Why good:** Both success and failure URLs provided, scopes request specific permissions, callback page calls `account.get()` to verify session
      
      ### Bad Example — Missing Failure URL
      
      ```typescript
      // BAD: No failure redirect
      account.createOAuth2Session({
        provider: OAuthProvider.Github,
        success: `${window.location.origin}/auth/callback`,
        // Missing failure URL — where does the user go on auth failure?
      });
      ```
      
      **Why bad:** Without a failure URL, the user has no redirect target if authentication fails
      
      ---
      
      ## Pattern 3: Magic URL (Passwordless)
      
      ### Good Example — Send and Verify Magic Link
      
      ```typescript
      import { ID } from "appwrite";
      
      // Step 1: Send magic link email
      async function sendMagicLink(email: string) {
        try {
          await account.createMagicURLToken({
            userId: ID.unique(),
            email,
            url: `${window.location.origin}/auth/magic-callback`,
          });
          // Email sent — user clicks the link
        } catch (error) {
          if (error instanceof AppwriteException) {
            throw new Error(`Failed to send magic link: ${error.message}`);
          }
          throw error;
        }
      }
      
      // Step 2: Handle the callback (user clicked the link)
      async function handleMagicURLCallback(userId: string, secret: string) {
        try {
          // Exchange the token for a session
          const session = await account.updateMagicURLSession({ userId, secret });
          return session;
        } catch (error) {
          if (error instanceof AppwriteException) {
            throw new Error(`Magic link verification failed: ${error.message}`);
          }
          throw error;
        }
      }
      ```
      
      **Why good:** Two-step flow (create token + update session), object parameters for all calls, `ID.unique()` for new users (creates account if needed), callback URL for redirect, token exchange for session
      
      ---
      
      ## Pattern 4: Anonymous Sessions
      
      ### Good Example — Guest Session with Account Upgrade
      
      ```typescript
      // Create an anonymous (guest) session
      async function createGuestSession() {
        try {
          return await account.createAnonymousSession();
        } catch (error) {
          if (error instanceof AppwriteException) {
            throw new Error(`Guest session failed: ${error.message}`);
          }
          throw error;
        }
      }
      
      // Later: Convert anonymous account to email/password account
      async function upgradeGuestAccount(email: string, password: string) {
        try {
          // This converts the anonymous user to a permanent account
          await account.updateEmail({ email, password });
          return await account.get();
        } catch (error) {
          if (error instanceof AppwriteException) {
            throw new Error(`Account upgrade failed: ${error.message}`);
          }
          throw error;
        }
      }
      ```
      
      **Why good:** Anonymous sessions let users try the app without registration, `updateEmail` converts the anonymous account in-place (data is preserved)
      
      ---
      
      ## Pattern 5: Session Management
      
      ### Good Example — Get Current User Safely
      
      ```typescript
      import type { Models } from "appwrite";
      
      async function getCurrentUser(): Promise<Models.User<Models.Preferences> | null> {
        try {
          return await account.get();
        } catch {
          // No active session — user is not logged in
          return null;
        }
      }
      ```
      
      **Why good:** `account.get()` throws 401 if no session exists — returning `null` provides a clean "no user" signal without propagating the exception
      
      ### Good Example — Sign Out
      
      ```typescript
      // Sign out current device only
      async function signOut() {
        try {
          await account.deleteSession({ sessionId: "current" });
        } catch (error) {
          if (error instanceof AppwriteException) {
            // Session may already be expired — safe to ignore
            console.warn(`Sign out warning: ${error.message}`);
          }
        }
      }
      
      // Sign out ALL devices
      async function signOutEverywhere() {
        await account.deleteSessions();
      }
      ```
      
      **Why good:** `deleteSession({ sessionId: "current" })` only ends the current session, `deleteSessions()` for security-critical sign-out-everywhere, catches already-expired sessions gracefully
      
      ### Good Example — List Active Sessions
      
      ```typescript
      async function getActiveSessions(): Promise<Models.Session[]> {
        const result = await account.listSessions();
        return result.sessions;
      }
      ```
      
      ---
      
      ## Pattern 6: Email Verification
      
      ### Good Example — Send and Confirm Verification
      
      ```typescript
      // Step 1: Send verification email
      async function sendVerificationEmail() {
        await account.createEmailVerification({
          url: `${window.location.origin}/auth/verify`,
        });
      }
      
      // Step 2: Handle verification callback
      async function confirmVerification(userId: string, secret: string) {
        await account.updateEmailVerification({ userId, secret });
      }
      ```
      
      **Why good:** Object parameters for all calls, redirect URL for verification callback, two-step flow (create + confirm). Note: the method is `updateEmailVerification` (not `updateVerification`).
      
      ---
      
      ## Pattern 7: Teams and Memberships
      
      ### Good Example — Create Team and Invite Members
      
      ```typescript
      import { Teams, ID, type Models } from "appwrite";
      
      const teams = new Teams(client);
      
      // Create a team
      async function createTeam(
        name: string,
      ): Promise<Models.Team<Models.Preferences>> {
        return await teams.create({ teamId: ID.unique(), name });
      }
      
      // Invite a member by email with roles
      async function inviteTeamMember(
        teamId: string,
        email: string,
        roles: string[],
      ) {
        return await teams.createMembership({
          teamId,
          roles,
          email,
          url: `${window.location.origin}/teams/accept`,
        });
      }
      
      // List team members
      async function listTeamMembers(teamId: string) {
        return await teams.listMemberships({ teamId });
      }
      
      // Update member roles
      async function updateMemberRoles(
        teamId: string,
        membershipId: string,
        roles: string[],
      ) {
        return await teams.updateMembership({ teamId, membershipId, roles });
      }
      ```
      
      **Why good:** Object parameters for all calls, `ID.unique()` for team ID, email-based invitation with redirect URL, roles as string array (any custom role names), separate methods for CRUD
      
      **When to use:** Multi-user collaboration features (workspaces, projects, organizations). Teams integrate with the permissions system via `Role.team(teamId)` and `Role.team(teamId, "role")`.
      
      ---
      
      ## Pattern 8: Password Reset
      
      ### Good Example — Request and Complete Password Reset
      
      ```typescript
      // Step 1: Request password reset
      async function requestPasswordReset(email: string) {
        await account.createRecovery({
          email,
          url: `${window.location.origin}/auth/reset-password`,
        });
      }
      
      // Step 2: Complete password reset (on the reset page)
      async function completePasswordReset(
        userId: string,
        secret: string,
        newPassword: string,
      ) {
        await account.updateRecovery({ userId, secret, password: newPassword });
      }
      ```
      
      **Why good:** Object parameters for all calls, two-step flow (create recovery + update recovery), redirect URL for reset page, `secret` from the email link used to verify the request
      
      ---
      
      _For database patterns, see [core.md](core.md). For storage patterns, see [storage.md](storage.md). For functions, see [functions.md](functions.md)._
      
    • core.md 10.2 KB
      # Appwrite Core Examples
      
      > Client setup, TablesDB CRUD, error handling, and permissions patterns. See [SKILL.md](../SKILL.md) for core concepts.
      
      **Auth patterns:** See [auth.md](auth.md). **Storage patterns:** See [storage.md](storage.md). **Functions:** See [functions.md](functions.md). **Realtime:** See [realtime.md](realtime.md).
      
      ---
      
      ## Pattern 1: Client Setup — Browser
      
      ### Good Example — Shared Client with Service Exports
      
      ```typescript
      // 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);
      export { ID, Permission, Role, Query } from "appwrite";
      ```
      
      **Why good:** Single `Client` instance shared across all services, named constants for configuration, re-exports SDK utilities for convenient import, environment variables keep secrets out of source
      
      ### Bad Example — Recreating Clients Per Service
      
      ```typescript
      // BAD: Each file creates its own Client
      import { Client, Account } from "appwrite";
      
      export const account = new Account(
        new Client()
          .setEndpoint("https://cloud.appwrite.io/v1") // Hardcoded
          .setProject("abc123"), // Hardcoded project ID
      );
      
      // Another file
      import { Client, TablesDB } from "appwrite";
      
      export const db = new TablesDB(
        new Client()
          .setEndpoint("https://cloud.appwrite.io/v1") // Duplicated
          .setProject("abc123"), // Duplicated
      );
      ```
      
      **Why bad:** Multiple `Client` instances waste memory and prevent shared session state, hardcoded credentials, duplicated configuration
      
      ---
      
      ## Pattern 2: Server SDK Setup
      
      ### Good Example — API Key Authentication
      
      ```typescript
      // 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()` authenticates with API key (admin access), `Users` service for admin operations (not available in client SDK), named exports for tree-shaking
      
      **When to use:** API routes, admin scripts, webhooks, cron jobs. NEVER import this file from client-side code.
      
      ### Good Example — JWT-Scoped Server Client
      
      ```typescript
      // For server routes that need to act as a specific user
      import { Client, TablesDB } from "node-appwrite";
      
      export function createUserScopedClient(jwt: string) {
        const client = new Client()
          .setEndpoint(process.env.APPWRITE_ENDPOINT!)
          .setProject(process.env.APPWRITE_PROJECT_ID!)
          .setJWT(jwt);
      
        return {
          tablesDB: new TablesDB(client),
          storage: new Storage(client),
        };
      }
      ```
      
      **Why good:** `.setJWT()` scopes operations to a specific user's permissions, factory function creates per-request clients, no API key needed (uses user's session)
      
      **When to use:** Server-side rendering where you need to fetch data as the logged-in user, or API routes that forward the user's JWT.
      
      ---
      
      ## Pattern 3: Error Handling
      
      ### Good Example — Typed AppwriteException
      
      ```typescript
      import { AppwriteException } from "appwrite";
      
      const HTTP_UNAUTHORIZED = 401;
      const HTTP_NOT_FOUND = 404;
      const HTTP_CONFLICT = 409;
      
      async function safeGetRow<T>(
        databaseId: string,
        tableId: string,
        rowId: string,
      ) {
        try {
          return await tablesDB.getRow<T>({ databaseId, tableId, rowId });
        } catch (error) {
          if (error instanceof AppwriteException) {
            switch (error.code) {
              case HTTP_NOT_FOUND:
                return null; // Row doesn't exist
              case HTTP_UNAUTHORIZED:
                throw new Error("Not authorized to access this row");
              default:
                throw new Error(`Appwrite error [${error.code}]: ${error.message}`);
            }
          }
          throw error; // Re-throw non-Appwrite errors
        }
      }
      ```
      
      **Why good:** Named constants for HTTP codes, typed `AppwriteException` with `code`/`message`/`type`, 404 returns null instead of throwing, unknown errors re-thrown
      
      ### Bad Example — Assuming Tuple Returns
      
      ```typescript
      // BAD: Appwrite does NOT return { data, error } tuples
      async function getUser() {
        const { data, error } = await account.get(); // WRONG
        if (error) {
          console.log(error);
        }
        return data;
      }
      ```
      
      **Why bad:** Appwrite SDK methods return data directly or throw `AppwriteException`, destructuring as `{ data, error }` will not work and silently produces `undefined`
      
      ---
      
      ## Pattern 4: TablesDB — Typed CRUD
      
      ### Good Example — Full CRUD with TypeScript Generics
      
      ```typescript
      import { ID, Query, Permission, Role, type Models } from "appwrite";
      
      // Define your row shape (extends Appwrite's base row model)
      interface Todo {
        title: string;
        completed: boolean;
        priority: number;
      }
      
      const DATABASE_ID = "main";
      const TABLE_ID = "todos";
      const PAGE_SIZE = 25;
      
      // CREATE
      async function createTodo(
        userId: string,
        data: Pick<Todo, "title" | "priority">,
      ) {
        return await tablesDB.createRow<Todo>({
          databaseId: DATABASE_ID,
          tableId: TABLE_ID,
          rowId: ID.unique(),
          data: {
            ...data,
            completed: false,
          },
          permissions: [
            Permission.read(Role.user(userId)),
            Permission.update(Role.user(userId)),
            Permission.delete(Role.user(userId)),
          ],
        });
      }
      
      // READ (single)
      async function getTodo(rowId: string) {
        return await tablesDB.getRow<Todo>({
          databaseId: DATABASE_ID,
          tableId: TABLE_ID,
          rowId,
        });
      }
      
      // READ (list with filters)
      async function listTodos(page: number, completed?: boolean) {
        const queries: string[] = [
          Query.orderDesc("$createdAt"),
          Query.limit(PAGE_SIZE),
          Query.offset(page * PAGE_SIZE),
        ];
      
        if (completed !== undefined) {
          queries.push(Query.equal("completed", completed));
        }
      
        return await tablesDB.listRows<Todo>({
          databaseId: DATABASE_ID,
          tableId: TABLE_ID,
          queries,
        });
      }
      
      // UPDATE
      async function updateTodo(rowId: string, data: Partial<Todo>) {
        return await tablesDB.updateRow<Todo>({
          databaseId: DATABASE_ID,
          tableId: TABLE_ID,
          rowId,
          data,
        });
      }
      
      // DELETE
      async function deleteTodo(rowId: string) {
        await tablesDB.deleteRow({
          databaseId: DATABASE_ID,
          tableId: TABLE_ID,
          rowId,
        });
      }
      ```
      
      **Why good:** Generic type `<Todo>` on all methods for type-safe results, `ID.unique()` for auto-generated IDs, named constants for database/table/page size, permissions set on create, composable query array, `Partial<Todo>` for updates
      
      ### Bad Example — Missing Permissions and No Types
      
      ```typescript
      // BAD: No permissions, no types, hardcoded IDs
      async function createTodo(title: string) {
        return await tablesDB.createRow({
          databaseId: "main",
          tableId: "todos",
          rowId: "my-todo", // Custom ID — not unique!
          data: { title },
          // NO permissions — row will be invisible!
        });
      }
      ```
      
      **Why bad:** No generic type parameter loses type safety, hardcoded string for `rowId` creates a custom ID (not auto-generated, will conflict on second call), no permissions means the row is inaccessible to everyone
      
      ---
      
      ## Pattern 5: Cursor-Based Pagination
      
      ### Good Example — Efficient Pagination for Large Datasets
      
      ```typescript
      const PAGE_SIZE = 25;
      
      async function loadNextPage<T>(
        databaseId: string,
        tableId: string,
        lastRowId: string | null,
      ) {
        const queries: string[] = [
          Query.orderDesc("$createdAt"),
          Query.limit(PAGE_SIZE),
        ];
      
        if (lastRowId) {
          queries.push(Query.cursorAfter(lastRowId));
        }
      
        const result = await tablesDB.listRows<T>({
          databaseId,
          tableId,
          queries,
        });
      
        return {
          rows: result.rows,
          hasMore: result.rows.length === PAGE_SIZE,
          lastId: result.rows.at(-1)?.$id ?? null,
        };
      }
      ```
      
      **Why good:** `Query.cursorAfter()` for efficient cursor pagination (better than offset for large datasets), tracks whether more pages exist, returns last ID for next page call
      
      **When to use:** Prefer cursor-based pagination (`cursorAfter`/`cursorBefore`) over offset-based (`Query.offset()`) for datasets with more than a few hundred rows. Offset pagination degrades as the offset increases.
      
      ---
      
      ## Pattern 6: Permissions — Common Patterns
      
      ### Good Example — Owner-Only Access
      
      ```typescript
      // Only the creator can read, update, and delete
      function ownerPermissions(userId: string) {
        return [
          Permission.read(Role.user(userId)),
          Permission.update(Role.user(userId)),
          Permission.delete(Role.user(userId)),
        ];
      }
      ```
      
      ### Good Example — Public Read, Owner Write
      
      ```typescript
      // Anyone can read, only owner can modify
      function publicReadOwnerWrite(userId: string) {
        return [
          Permission.read(Role.any()),
          Permission.update(Role.user(userId)),
          Permission.delete(Role.user(userId)),
        ];
      }
      ```
      
      ### Good Example — Team Collaboration
      
      ```typescript
      // All team members can read, admins can edit, owners can delete
      function teamPermissions(teamId: string, ownerId: string) {
        return [
          Permission.read(Role.team(teamId)),
          Permission.update(Role.team(teamId, "admin")),
          Permission.delete(Role.user(ownerId)),
        ];
      }
      ```
      
      ### Good Example — Updating Permissions on Existing Row
      
      ```typescript
      // Grant access to a new team member
      async function shareWithUser(
        databaseId: string,
        tableId: string,
        rowId: string,
        targetUserId: string,
      ) {
        const row = await tablesDB.getRow({ databaseId, tableId, rowId });
        const currentPermissions = row.$permissions;
      
        await tablesDB.updateRow({
          databaseId,
          tableId,
          rowId,
          permissions: [
            ...currentPermissions,
            Permission.read(Role.user(targetUserId)),
          ],
        });
      }
      ```
      
      **Why good:** Preserves existing permissions, adds new user read access, reads current state before modifying
      
      ---
      
      _For auth patterns, see [auth.md](auth.md). For storage, see [storage.md](storage.md). For functions, see [functions.md](functions.md). For realtime, see [realtime.md](realtime.md)._
      
    • functions.md 8.4 KB
      # Appwrite Functions Examples
      
      > Serverless functions, triggers, SDK usage inside functions, and common patterns. See [SKILL.md](../SKILL.md) for core concepts.
      
      ---
      
      ## Pattern 1: Basic Function
      
      ### Good Example — HTTP Handler with Appwrite SDK
      
      ```typescript
      // functions/get-todos/src/main.ts
      import { Client, TablesDB, Query } from "node-appwrite";
      
      const DATABASE_ID = "main";
      const TABLE_ID = "todos";
      const PAGE_SIZE = 25;
      
      export default async ({ req, res, log, error }) => {
        // Initialize Appwrite client with the dynamic API key
        const client = new Client()
          .setEndpoint(process.env.APPWRITE_FUNCTION_API_ENDPOINT!)
          .setProject(process.env.APPWRITE_FUNCTION_PROJECT_ID!)
          .setKey(req.headers["x-appwrite-key"]);
      
        const tablesDB = new TablesDB(client);
      
        try {
          const result = await tablesDB.listRows({
            databaseId: DATABASE_ID,
            tableId: TABLE_ID,
            queries: [Query.orderDesc("$createdAt"), Query.limit(PAGE_SIZE)],
          });
      
          return res.json({ rows: result.rows, total: result.total });
        } catch (err) {
          error(`Failed to list todos: ${err.message}`);
          return res.json({ error: "Internal error" }, 500);
        }
      };
      ```
      
      **Why good:** Uses `req.headers["x-appwrite-key"]` for dynamic API key (auto-injected by Appwrite), `node-appwrite` server SDK, named constants, `log()`/`error()` for developer-only logging, JSON response with status code
      
      ### Bad Example — Hardcoded Credentials in Function
      
      ```typescript
      // BAD: Hardcoded API key
      export default async ({ req, res }) => {
        const client = new Client()
          .setEndpoint("https://cloud.appwrite.io/v1") // Hardcoded
          .setProject("abc123") // Hardcoded
          .setKey("secret-api-key-here"); // Hardcoded — exposed in source
      
        // ...
      };
      ```
      
      **Why bad:** Hardcoded credentials exposed in source control, should use `process.env` or `req.headers["x-appwrite-key"]` for the dynamic key
      
      ---
      
      ## Pattern 2: User-Scoped Function
      
      ### Good Example — Acting as the Calling User
      
      ```typescript
      // functions/create-post/src/main.ts
      import { Client, TablesDB, ID, Permission, Role } from "node-appwrite";
      
      const DATABASE_ID = "main";
      const TABLE_ID = "posts";
      
      export default async ({ req, res, log, error }) => {
        // Check if the user is authenticated
        const jwt = req.headers["x-appwrite-user-jwt"];
        if (!jwt) {
          return res.json({ error: "Authentication required" }, 401);
        }
      
        // Use JWT for user-scoped access (respects permissions)
        const client = new Client()
          .setEndpoint(process.env.APPWRITE_FUNCTION_API_ENDPOINT!)
          .setProject(process.env.APPWRITE_FUNCTION_PROJECT_ID!)
          .setJWT(jwt);
      
        const tablesDB = new TablesDB(client);
      
        try {
          const { title, content } = req.bodyJson;
          const userId = req.headers["x-appwrite-user-id"];
      
          const row = await tablesDB.createRow({
            databaseId: DATABASE_ID,
            tableId: TABLE_ID,
            rowId: ID.unique(),
            data: { title, content, authorId: userId },
            permissions: [
              Permission.read(Role.any()),
              Permission.update(Role.user(userId)),
              Permission.delete(Role.user(userId)),
            ],
          });
      
          return res.json({ post: row }, 201);
        } catch (err) {
          error(`Create post failed: ${err.message}`);
          return res.json({ error: "Failed to create post" }, 500);
        }
      };
      ```
      
      **Why good:** JWT-based auth scopes operations to the calling user's permissions, user ID from `x-appwrite-user-id` header, `req.bodyJson` for parsed body, permissions set on creation, 201 status for created resource
      
      ---
      
      ## Pattern 3: Admin Function (Bypasses Permissions)
      
      ### Good Example — Cleanup Script with API Key
      
      ```typescript
      // functions/cleanup-stale/src/main.ts
      import { Client, TablesDB, Query } from "node-appwrite";
      
      const DATABASE_ID = "main";
      const TABLE_ID = "temp-files";
      const STALE_DAYS = 30;
      
      export default async ({ req, res, log, error }) => {
        // Admin client — uses API key, bypasses all permissions
        const client = new Client()
          .setEndpoint(process.env.APPWRITE_FUNCTION_API_ENDPOINT!)
          .setProject(process.env.APPWRITE_FUNCTION_PROJECT_ID!)
          .setKey(req.headers["x-appwrite-key"]);
      
        const tablesDB = new TablesDB(client);
      
        const cutoffDate = new Date();
        cutoffDate.setDate(cutoffDate.getDate() - STALE_DAYS);
      
        try {
          const staleRows = await tablesDB.listRows({
            databaseId: DATABASE_ID,
            tableId: TABLE_ID,
            queries: [
              Query.lessThan("$createdAt", cutoffDate.toISOString()),
              Query.limit(100),
            ],
          });
      
          let deletedCount = 0;
          for (const row of staleRows.rows) {
            await tablesDB.deleteRow({
              databaseId: DATABASE_ID,
              tableId: TABLE_ID,
              rowId: row.$id,
            });
            deletedCount++;
          }
      
          log(`Cleaned up ${deletedCount} stale rows`);
          return res.json({ deleted: deletedCount });
        } catch (err) {
          error(`Cleanup failed: ${err.message}`);
          return res.json({ error: "Cleanup failed" }, 500);
        }
      };
      ```
      
      **Why good:** API key from `x-appwrite-key` for admin access, named constant for stale threshold, date-based query filtering, counts deleted rows, developer-only logging
      
      **When to use:** Scheduled cleanup, data migrations, admin operations, background processing. Functions using the dynamic API key have full admin access — they bypass all permissions.
      
      ---
      
      ## Pattern 4: Event-Triggered Function
      
      ### Good Example — Handle Row Creation Event
      
      ```typescript
      // functions/on-user-created/src/main.ts
      import { Client, TablesDB, ID, Permission, Role } from "node-appwrite";
      
      const DATABASE_ID = "main";
      const PROFILES_TABLE = "profiles";
      
      export default async ({ req, res, log, error }) => {
        // Check trigger type
        const trigger = req.headers["x-appwrite-trigger"];
      
        if (trigger !== "event") {
          return res.json({ error: "Not an event trigger" }, 400);
        }
      
        const client = new Client()
          .setEndpoint(process.env.APPWRITE_FUNCTION_API_ENDPOINT!)
          .setProject(process.env.APPWRITE_FUNCTION_PROJECT_ID!)
          .setKey(req.headers["x-appwrite-key"]);
      
        const tablesDB = new TablesDB(client);
      
        try {
          // req.bodyJson contains the event payload (the created user)
          const user = req.bodyJson;
      
          // Create a profile row for the new user
          await tablesDB.createRow({
            databaseId: DATABASE_ID,
            tableId: PROFILES_TABLE,
            rowId: ID.unique(),
            data: {
              userId: user.$id,
              displayName: user.name || "Anonymous",
              bio: "",
            },
            permissions: [
              Permission.read(Role.any()),
              Permission.update(Role.user(user.$id)),
            ],
          });
      
          log(`Profile created for user ${user.$id}`);
          return res.json({ success: true });
        } catch (err) {
          error(`Profile creation failed: ${err.message}`);
          return res.json({ error: "Failed to create profile" }, 500);
        }
      };
      ```
      
      **Why good:** Checks `x-appwrite-trigger` header to verify event source, event payload in `req.bodyJson`, creates related data automatically on user creation, appropriate permissions on the new profile
      
      **When to use:** Configure event triggers in the Appwrite console (e.g., `users.*.create` triggers this function when any user is created). Useful for creating default data, sending welcome emails, or syncing to external services.
      
      ---
      
      ## Pattern 5: Scheduled Function (Cron)
      
      ### Good Example — Daily Report
      
      ```typescript
      // functions/daily-report/src/main.ts
      import { Client, TablesDB, Query } from "node-appwrite";
      
      const DATABASE_ID = "main";
      const ORDERS_TABLE = "orders";
      const HOURS_IN_DAY = 24;
      
      export default async ({ req, res, log }) => {
        const client = new Client()
          .setEndpoint(process.env.APPWRITE_FUNCTION_API_ENDPOINT!)
          .setProject(process.env.APPWRITE_FUNCTION_PROJECT_ID!)
          .setKey(req.headers["x-appwrite-key"]);
      
        const tablesDB = new TablesDB(client);
      
        const yesterday = new Date();
        yesterday.setHours(yesterday.getHours() - HOURS_IN_DAY);
      
        const recentOrders = await tablesDB.listRows({
          databaseId: DATABASE_ID,
          tableId: ORDERS_TABLE,
          queries: [Query.greaterThan("$createdAt", yesterday.toISOString())],
        });
      
        log(`Orders in last 24h: ${recentOrders.total}`);
      
        // Send report to external service, store summary, etc.
      
        return res.json({
          period: "24h",
          orderCount: recentOrders.total,
        });
      };
      ```
      
      **Why good:** Named constant for time period, date arithmetic for range query, scheduled via Appwrite console cron expression (e.g., `0 9 * * *` for 9 AM daily)
      
      ---
      
      _For auth patterns, see [auth.md](auth.md). For database patterns, see [core.md](core.md). For storage, see [storage.md](storage.md)._
      
    • realtime.md 5.8 KB
      # Appwrite Realtime Examples
      
      > Channel subscriptions, event filtering, cleanup patterns. See [SKILL.md](../SKILL.md) for core concepts.
      
      ---
      
      ## Pattern 1: Subscribe to Table Changes
      
      ### Good Example — Listen for Row Events
      
      ```typescript
      import { Realtime, Channel } from "appwrite";
      
      const DATABASE_ID = "main";
      const MESSAGES_TABLE = "messages";
      
      // Subscribe to all changes on a table
      const subscription = await realtime.subscribe(
        Channel.tablesdb(DATABASE_ID).table(MESSAGES_TABLE),
        (response) => {
          const events = response.events;
      
          if (events.includes("tablesdb.*.tables.*.rows.*.create")) {
            handleNewMessage(response.payload);
          }
      
          if (events.includes("tablesdb.*.tables.*.rows.*.update")) {
            handleUpdatedMessage(response.payload);
          }
      
          if (events.includes("tablesdb.*.tables.*.rows.*.delete")) {
            handleDeletedMessage(response.payload);
          }
        },
      );
      
      function handleNewMessage(payload: unknown) {
        // Add new message to UI
      }
      
      function handleUpdatedMessage(payload: unknown) {
        // Update existing message in UI
      }
      
      function handleDeletedMessage(payload: unknown) {
        // Remove message from UI
      }
      
      // IMPORTANT: Always clean up when done
      await subscription.close();
      ```
      
      **Why good:** `Channel` helper for type-safe channel construction, event string matching to filter by operation type, separate handlers per event, explicit cleanup with `subscription.close()`
      
      ### Bad Example — Not Unsubscribing
      
      ```typescript
      // BAD: Subscription leaks — never closed
      realtime.subscribe(
        Channel.tablesdb(DATABASE_ID).table(MESSAGES_TABLE),
        (response) => {
          console.log(response);
        },
      );
      // No reference saved — cannot unsubscribe
      // WebSocket stays open indefinitely
      ```
      
      **Why bad:** No reference to the subscription means you cannot close it, leaked subscriptions cause duplicate events and memory issues, each new subscribe without closing the old one tears down and recreates the WebSocket
      
      ---
      
      ## Pattern 2: Subscribe to a Specific Row
      
      ### Good Example — Single Row Updates
      
      ```typescript
      const DATABASE_ID = "main";
      const ORDERS_TABLE = "orders";
      
      async function watchOrder(orderId: string, onUpdate: (order: unknown) => void) {
        const subscription = await realtime.subscribe(
          Channel.tablesdb(DATABASE_ID).table(ORDERS_TABLE).row(orderId),
          (response) => {
            onUpdate(response.payload);
          },
        );
      
        // Return cleanup function
        return () => subscription.close();
      }
      
      // Usage
      const cleanup = await watchOrder("order-123", (order) => {
        console.log("Order updated:", order);
      });
      
      // When done watching
      await cleanup();
      ```
      
      **Why good:** Subscribes to a single row (not entire table), callback pattern decouples realtime from UI, returns cleanup function for lifecycle management
      
      **When to use:** Order status tracking, live document editing, real-time form collaboration. More efficient than table-wide subscriptions when you only care about one row.
      
      ---
      
      ## Pattern 3: Subscribe to File Events
      
      ### Good Example — Watch for Uploads
      
      ```typescript
      const subscription = await realtime.subscribe(Channel.files(), (response) => {
        if (response.events.includes("buckets.*.files.*.create")) {
          console.log("New file uploaded:", response.payload);
        }
      });
      ```
      
      **Why good:** `Channel.files()` subscribes to all file events across all buckets, event filtering narrows to create events only
      
      ---
      
      ## Pattern 4: Subscribe to Account Events
      
      ### Good Example — User Session Changes
      
      ```typescript
      const subscription = await realtime.subscribe(Channel.account(), (response) => {
        const events = response.events;
      
        if (events.some((e) => e.includes("sessions.*.create"))) {
          console.log("New session created");
        }
      
        if (events.some((e) => e.includes("sessions.*.delete"))) {
          console.log("Session ended — user may have signed out");
          // Redirect to login or refresh auth state
        }
      });
      ```
      
      **Why good:** `Channel.account()` tracks session and account changes, useful for detecting sign-out from another tab/device
      
      ---
      
      ## Pattern 5: Multiple Channel Subscriptions
      
      ### Good Example — Subscribe to Multiple Channels
      
      ```typescript
      const DATABASE_ID = "main";
      const MESSAGES_TABLE = "messages";
      const USERS_TABLE = "users";
      
      const subscription = await realtime.subscribe(
        [
          Channel.tablesdb(DATABASE_ID).table(MESSAGES_TABLE),
          Channel.tablesdb(DATABASE_ID).table(USERS_TABLE),
          Channel.account(),
        ],
        (response) => {
          // Check which channel the event came from
          if (response.events.some((e) => e.includes("messages"))) {
            handleMessageEvent(response);
          } else if (response.events.some((e) => e.includes("users"))) {
            handleUserEvent(response);
          } else {
            handleAccountEvent(response);
          }
        },
      );
      ```
      
      **Why good:** Single subscription for multiple channels (single WebSocket), event string inspection to route to correct handler, more efficient than multiple separate subscriptions (avoids WebSocket reconnection)
      
      ---
      
      ## Pattern 6: Realtime with Query Filtering
      
      ### Good Example — Server-Side Event Filtering
      
      ```typescript
      import { Query } from "appwrite";
      
      const DATABASE_ID = "main";
      const MESSAGES_TABLE = "messages";
      const ROOM_ID = "room-123";
      
      // Subscribe with query filters — events are filtered server-side
      const subscription = await realtime.subscribe(
        Channel.tablesdb(DATABASE_ID).table(MESSAGES_TABLE),
        (response) => {
          // Only receives events matching the query
          console.log("Message in room:", response.payload);
        },
        [Query.equal("roomId", ROOM_ID)],
      );
      ```
      
      **Why good:** Server-side filtering reduces unnecessary events to the client, `Query.equal` narrows to a specific room, less client-side processing needed
      
      **When to use:** Chat rooms, filtered dashboards, multi-tenant apps where you only need events for a subset of rows.
      
      ---
      
      _For database patterns, see [core.md](core.md). For auth patterns, see [auth.md](auth.md). For storage, see [storage.md](storage.md)._
      
    • storage.md 5.7 KB
      # Appwrite Storage Examples
      
      > File upload, download, preview with image transforms, and bucket permissions. See [SKILL.md](../SKILL.md) for core concepts.
      
      ---
      
      ## Pattern 1: File Upload
      
      ### Good Example — Upload with Permissions and Validation
      
      ```typescript
      import { ID, Permission, Role, AppwriteException } from "appwrite";
      
      const BUCKET_ID = "user-uploads";
      const MAX_FILE_SIZE_MB = 10;
      const MAX_FILE_SIZE_BYTES = MAX_FILE_SIZE_MB * 1024 * 1024;
      
      async function uploadFile(file: File, userId: string) {
        if (file.size > MAX_FILE_SIZE_BYTES) {
          throw new Error(`File exceeds ${MAX_FILE_SIZE_MB}MB limit`);
        }
      
        try {
          return await storage.createFile({
            bucketId: BUCKET_ID,
            fileId: ID.unique(),
            file,
            permissions: [
              Permission.read(Role.user(userId)),
              Permission.update(Role.user(userId)),
              Permission.delete(Role.user(userId)),
            ],
          });
        } catch (error) {
          if (error instanceof AppwriteException) {
            throw new Error(`Upload failed: ${error.message}`);
          }
          throw error;
        }
      }
      ```
      
      **Why good:** Client-side size validation before upload, `ID.unique()` for auto-generated file ID, explicit permissions (without them the file is inaccessible), named constants for bucket and limits
      
      ### Good Example — Avatar Upload (Upsert Pattern)
      
      ```typescript
      const AVATAR_BUCKET = "avatars";
      
      async function uploadAvatar(userId: string, file: File) {
        // Use a deterministic file ID so the same user always overwrites their avatar
        const fileId = `avatar-${userId}`;
      
        try {
          // Try to delete existing avatar first
          await storage.deleteFile({ bucketId: AVATAR_BUCKET, fileId });
        } catch {
          // File doesn't exist yet — that's fine
        }
      
        return await storage.createFile({
          bucketId: AVATAR_BUCKET,
          fileId,
          file,
          permissions: [Permission.read(Role.any())], // Public avatar
        });
      }
      ```
      
      **Why good:** Deterministic file ID ensures one avatar per user, delete-then-create pattern for upsert (Appwrite `createFile` does not have an upsert option), public read permission for avatars
      
      ### Bad Example — No Permissions on Upload
      
      ```typescript
      // BAD: File uploaded but inaccessible
      await storage.createFile({ bucketId: BUCKET_ID, fileId: ID.unique(), file });
      // No permissions — nobody can download this file!
      ```
      
      **Why bad:** Without permissions, the file exists in the bucket but is invisible and inaccessible to all users
      
      ---
      
      ## Pattern 2: File Preview (Images)
      
      ### Good Example — Image Transforms
      
      ```typescript
      const IMAGES_BUCKET = "product-images";
      const THUMBNAIL_WIDTH = 200;
      const THUMBNAIL_HEIGHT = 200;
      const FULL_WIDTH = 1200;
      const OPTIMIZED_WIDTH = 800;
      const OPTIMIZED_HEIGHT = 600;
      const OPTIMIZED_QUALITY = 80;
      
      // Thumbnail URL
      function getThumbnailUrl(fileId: string) {
        return storage.getFilePreview({
          bucketId: IMAGES_BUCKET,
          fileId,
          width: THUMBNAIL_WIDTH,
          height: THUMBNAIL_HEIGHT,
        });
      }
      
      // Full-size URL
      function getFullImageUrl(fileId: string) {
        return storage.getFilePreview({
          bucketId: IMAGES_BUCKET,
          fileId,
          width: FULL_WIDTH,
        });
      }
      
      // Preview with quality control
      function getOptimizedPreview(fileId: string) {
        return storage.getFilePreview({
          bucketId: IMAGES_BUCKET,
          fileId,
          width: OPTIMIZED_WIDTH,
          height: OPTIMIZED_HEIGHT,
          quality: OPTIMIZED_QUALITY,
        });
      }
      ```
      
      **Why good:** Named constants for dimensions, `getFilePreview` handles server-side resizing (saves bandwidth), quality parameter for compression control
      
      **When to use:** `getFilePreview()` only works on image files (JPEG, PNG, GIF, WebP). For non-image files, use `getFileDownload()` or `getFileView()`.
      
      ---
      
      ## Pattern 3: File Download and View
      
      ### Good Example — Download vs View
      
      ```typescript
      const DOCUMENTS_BUCKET = "documents";
      
      // Download — returns URL with Content-Disposition: attachment header
      // Browser will prompt a file save dialog
      function getDownloadUrl(fileId: string) {
        return storage.getFileDownload({ bucketId: DOCUMENTS_BUCKET, fileId });
      }
      
      // View — returns URL without attachment header
      // Browser will display the file inline (e.g., PDF in browser)
      function getViewUrl(fileId: string) {
        return storage.getFileView({ bucketId: DOCUMENTS_BUCKET, fileId });
      }
      ```
      
      **Why good:** `getFileDownload` forces a download dialog, `getFileView` displays inline — choose based on UX intent
      
      ---
      
      ## Pattern 4: File Management
      
      ### Good Example — List, Delete, Update
      
      ```typescript
      import { Query } from "appwrite";
      
      const BUCKET_ID = "user-uploads";
      const FILES_PER_PAGE = 25;
      
      // List files with pagination
      async function listUserFiles(page: number) {
        return await storage.listFiles({
          bucketId: BUCKET_ID,
          queries: [
            Query.limit(FILES_PER_PAGE),
            Query.offset(page * FILES_PER_PAGE),
            Query.orderDesc("$createdAt"),
          ],
        });
      }
      
      // Delete a file
      async function deleteFile(fileId: string) {
        try {
          await storage.deleteFile({ bucketId: BUCKET_ID, fileId });
        } catch (error) {
          if (error instanceof AppwriteException) {
            throw new Error(`Delete failed: ${error.message}`);
          }
          throw error;
        }
      }
      
      // Update file permissions (e.g., share with another user)
      async function shareFile(fileId: string, targetUserId: string) {
        const file = await storage.getFile({ bucketId: BUCKET_ID, fileId });
        const currentPermissions = file.$permissions;
      
        await storage.updateFile({
          bucketId: BUCKET_ID,
          fileId,
          permissions: [
            ...currentPermissions,
            Permission.read(Role.user(targetUserId)),
          ],
        });
      }
      ```
      
      **Why good:** Query-based file listing with pagination, preserves existing permissions when sharing, `getFile` to read current permissions before updating
      
      ---
      
      _For auth patterns, see [auth.md](auth.md). For database patterns, see [core.md](core.md). For functions, see [functions.md](functions.md)._
      
  • reference.md 10.8 KB
    # Appwrite Reference
    
    > CLI commands, Query class methods, Permission/Role syntax, and quick lookup tables. See [SKILL.md](SKILL.md) for core concepts and [examples/](examples/) for code examples.
    
    ---
    
    ## Appwrite CLI Commands
    
    ### Project Setup
    
    ```bash
    # Install CLI globally
    npm install -g appwrite-cli
    
    # Login to Appwrite
    appwrite login
    
    # Initialize project in current directory
    appwrite init project
    
    # Initialize functions
    appwrite init function
    ```
    
    ### Type Generation
    
    ```bash
    # Generate TypeScript types from your project schema
    appwrite types
    
    # Output: generates interfaces for all tables and their columns
    ```
    
    ### Functions
    
    ```bash
    # Create a new function
    appwrite init function
    
    # Deploy functions
    appwrite deploy function
    
    # Execute a function
    appwrite functions createExecution --functionId <FUNCTION_ID> --body '{"key":"value"}'
    ```
    
    ---
    
    ## Environment Variables
    
    ```bash
    # .env.local
    APPWRITE_ENDPOINT=https://<REGION>.cloud.appwrite.io/v1
    APPWRITE_PROJECT_ID=your-project-id
    APPWRITE_API_KEY=your-api-key          # SERVER ONLY — never expose to client
    ```
    
    ---
    
    ## Query Class Reference
    
    | Method                             | Example                                | Description                         |
    | ---------------------------------- | -------------------------------------- | ----------------------------------- |
    | `Query.equal(col, val)`            | `Query.equal("status", "active")`      | Exact match (also accepts arrays)   |
    | `Query.notEqual(col, val)`         | `Query.notEqual("status", "archived")` | Exclude value                       |
    | `Query.greaterThan(col, val)`      | `Query.greaterThan("age", 18)`         | Greater than                        |
    | `Query.greaterThanEqual(col, val)` | `Query.greaterThanEqual("age", 18)`    | Greater than or equal               |
    | `Query.lessThan(col, val)`         | `Query.lessThan("price", 100)`         | Less than                           |
    | `Query.lessThanEqual(col, val)`    | `Query.lessThanEqual("price", 100)`    | Less than or equal                  |
    | `Query.between(col, start, end)`   | `Query.between("age", 18, 65)`         | Inclusive range                     |
    | `Query.isNull(col)`                | `Query.isNull("deletedAt")`            | Column is null                      |
    | `Query.isNotNull(col)`             | `Query.isNotNull("email")`             | Column is not null                  |
    | `Query.startsWith(col, str)`       | `Query.startsWith("name", "Al")`       | String prefix match                 |
    | `Query.endsWith(col, str)`         | `Query.endsWith("email", ".com")`      | String suffix match                 |
    | `Query.contains(col, val)`         | `Query.contains("tags", ["urgent"])`   | Array/substring contains            |
    | `Query.search(col, keywords)`      | `Query.search("title", "appwrite")`    | Full-text search (requires index)   |
    | `Query.orderAsc(col)`              | `Query.orderAsc("createdAt")`          | Sort ascending                      |
    | `Query.orderDesc(col)`             | `Query.orderDesc("$createdAt")`        | Sort descending                     |
    | `Query.limit(n)`                   | `Query.limit(25)`                      | Max rows returned (max 100)         |
    | `Query.offset(n)`                  | `Query.offset(25)`                     | Skip rows (offset-based pagination) |
    | `Query.cursorAfter(id)`            | `Query.cursorAfter("row_abc")`         | Cursor-based pagination (after ID)  |
    | `Query.cursorBefore(id)`           | `Query.cursorBefore("row_xyz")`        | Cursor-based pagination (before ID) |
    | `Query.select(fields)`             | `Query.select(["title", "status"])`    | Return only specific columns        |
    | `Query.and(queries)`               | `Query.and([q1, q2])`                  | All conditions must match           |
    | `Query.or(queries)`                | `Query.or([q1, q2])`                   | Any condition can match             |
    
    ---
    
    ## Permission + Role Quick Reference
    
    ### Permission Methods
    
    | Method                    | Description                        |
    | ------------------------- | ---------------------------------- |
    | `Permission.read(role)`   | Can read the resource              |
    | `Permission.create(role)` | Can create child resources         |
    | `Permission.update(role)` | Can modify the resource            |
    | `Permission.delete(role)` | Can remove the resource            |
    | `Permission.write(role)`  | Alias for create + update + delete |
    
    ### Role Methods
    
    | Method                          | Description                        |
    | ------------------------------- | ---------------------------------- |
    | `Role.any()`                    | Anyone (including unauthenticated) |
    | `Role.guests()`                 | Unauthenticated users only         |
    | `Role.users()`                  | All authenticated users            |
    | `Role.users("verified")`        | Verified authenticated users       |
    | `Role.user(userId)`             | Specific user                      |
    | `Role.user(userId, "verified")` | Specific verified user             |
    | `Role.team(teamId)`             | All team members                   |
    | `Role.team(teamId, "admin")`    | Team members with role             |
    | `Role.member(membershipId)`     | Specific team membership           |
    | `Role.label("premium")`         | Users with label (server-set only) |
    
    ---
    
    ## Realtime Channel Helpers
    
    | Channel                                            | Subscribe To                       |
    | -------------------------------------------------- | ---------------------------------- |
    | `Channel.account()`                                | Current user's account changes     |
    | `Channel.files()`                                  | All file events across all buckets |
    | `Channel.tablesdb(dbId).table(tableId)`            | All row events in a table          |
    | `Channel.tablesdb(dbId).table(tableId).row(rowId)` | Specific row events                |
    
    ### Event String Patterns
    
    | Pattern                             | Description   |
    | ----------------------------------- | ------------- |
    | `tablesdb.*.tables.*.rows.*.create` | Row created   |
    | `tablesdb.*.tables.*.rows.*.update` | Row updated   |
    | `tablesdb.*.tables.*.rows.*.delete` | Row deleted   |
    | `buckets.*.files.*.create`          | File uploaded |
    | `buckets.*.files.*.update`          | File updated  |
    | `buckets.*.files.*.delete`          | File deleted  |
    
    ---
    
    ## Storage Methods Quick Reference
    
    | Method                                                               | Description                           |
    | -------------------------------------------------------------------- | ------------------------------------- |
    | `storage.createFile({ bucketId, fileId, file, permissions? })`       | Upload a file                         |
    | `storage.getFile({ bucketId, fileId })`                              | Get file metadata                     |
    | `storage.listFiles({ bucketId, queries? })`                          | List files in bucket                  |
    | `storage.updateFile({ bucketId, fileId, name?, permissions? })`      | Update file metadata/permissions      |
    | `storage.deleteFile({ bucketId, fileId })`                           | Delete a file                         |
    | `storage.getFilePreview({ bucketId, fileId, width?, height?, ... })` | Image preview with transforms         |
    | `storage.getFileDownload({ bucketId, fileId })`                      | Download URL (attachment header)      |
    | `storage.getFileView({ bucketId, fileId })`                          | View URL (inline, no download header) |
    
    ---
    
    ## Auth Methods Quick Reference
    
    | Method                                                                   | Description                             |
    | ------------------------------------------------------------------------ | --------------------------------------- |
    | `account.create({ userId, email, password, name? })`                     | Create new user account                 |
    | `account.createEmailPasswordSession({ email, password })`                | Sign in with email/password             |
    | `account.createOAuth2Session({ provider, success?, failure?, scopes? })` | OAuth redirect (`OAuthProvider` enum)   |
    | `account.createMagicURLToken({ userId, email, url?, phrase? })`          | Send magic link email                   |
    | `account.createPhoneToken({ userId, phone })`                            | Send SMS OTP                            |
    | `account.createAnonymousSession()`                                       | Guest session                           |
    | `account.get()`                                                          | Get current user (throws if no session) |
    | `account.updatePrefs({ prefs })`                                         | Update user preferences                 |
    | `account.updateName({ name })`                                           | Update user display name                |
    | `account.updatePassword({ password, oldPassword? })`                     | Change password                         |
    | `account.createEmailVerification({ url })`                               | Send verification email                 |
    | `account.updateEmailVerification({ userId, secret })`                    | Confirm email verification              |
    | `account.deleteSession({ sessionId: "current" })`                        | Sign out current device                 |
    | `account.deleteSessions()`                                               | Sign out all devices                    |
    
    ---
    
    ## System Fields on Rows
    
    All rows include these system-managed fields (prefixed with `$`):
    
    | Field                        | Type     | Description                    |
    | ---------------------------- | -------- | ------------------------------ |
    | `$id`                        | string   | Unique row identifier          |
    | `$createdAt`                 | string   | ISO 8601 creation timestamp    |
    | `$updatedAt`                 | string   | ISO 8601 last update timestamp |
    | `$permissions`               | string[] | Row-level permission strings   |
    | `$databaseId`                | string   | Parent database ID             |
    | `$collectionId` / `$tableId` | string   | Parent table ID                |
    
    Use `$createdAt` and `$updatedAt` in queries: `Query.orderDesc("$createdAt")`
    
    ---
    
    ## Function Context Object
    
    ```typescript
    export default async ({ req, res, log, error }) => {
      req.method; // GET, POST, PUT, DELETE, etc.
      req.path; // URL path
      req.query; // Parsed query parameters
      req.bodyText; // Raw text body
      req.bodyJson; // Parsed JSON body
      req.headers; // All headers (lowercase keys)
      req.headers["x-appwrite-key"]; // Dynamic API key (admin access)
      req.headers["x-appwrite-user-jwt"]; // User JWT (user-scoped access)
    
      res.text("ok"); // Plain text response
      res.json({ key: "value" }); // JSON response
      res.empty(); // 204 No Content
      res.redirect("https://..."); // 301 Redirect
    
      log("debug info"); // Developer-only logging
      error("something went wrong"); // Developer-only error logging
    };
    ```
    
  • SKILL.md 23.2 KB
    ---
    name: api-baas-appwrite
    description: Appwrite backend-as-a-service — Auth, TablesDB, Storage, Functions, Realtime, permissions model, typed client
    ---
    
    # 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 shared `Client` instance, 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 `Query` class 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 `Channel` helpers and `realtime.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:**
    
    1. **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.
    2. **Service class architecture** — All SDK interactions go through service classes (`Account`, `TablesDB`, `Storage`, `Functions`, `Teams`, `Realtime`) instantiated from a shared `Client`. This pattern is consistent across client and server SDKs.
    3. **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.
    4. **Exceptions, not tuples** — Appwrite throws `AppwriteException` on failure, not `{ data, error }` tuples. Always wrap calls in try/catch.
    5. **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.
    6. **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.
    
    ```typescript
    // 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
    
    ```typescript
    // 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.
    
    ```typescript
    // 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 client-side code.
    
    ---
    
    ### Pattern 3: Error Handling (AppwriteException)
    
    Appwrite throws `AppwriteException` — it does NOT return `{ data, error }` tuples. Always use try/catch.
    
    ```typescript
    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 — human-readable message
          // error.code — HTTP status code (401, 404, etc.)
          // error.type — machine-readable error type string
          throw new Error(`Appwrite error ${error.code}: ${error.message}`);
        }
        throw error;
      }
    }
    ```
    
    **Why good:** Type-narrowed `AppwriteException` provides `code`, `message`, `type`; re-throws unknown errors; explicit return type using `Models` namespace
    
    ```typescript
    // BAD: Assuming { data, error } tuple returns
    const { data, error } = await account.get(); // WRONG — Appwrite throws, not returns tuples
    if (error) {
      /* ... */
    }
    ```
    
    **Why bad:** Appwrite does NOT return error tuples — this code will fail silently because `account.get()` either returns data or throws
    
    ---
    
    ### Pattern 4: TablesDB Operations (CRUD)
    
    Use `TablesDB` for all database operations. Row IDs are required for create — use `ID.unique()` for auto-generation.
    
    #### Create a Row
    
    ```typescript
    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: "Buy groceries",
        completed: false,
      },
      permissions: [
        Permission.read(Role.user(userId)),
        Permission.update(Role.user(userId)),
        Permission.delete(Role.user(userId)),
      ],
    });
    ```
    
    #### List Rows with Queries
    
    ```typescript
    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 of Todo rows
    // result.total — total matching count
    ```
    
    #### Update a Row
    
    ```typescript
    const updated = await tablesDB.updateRow<Todo>({
      databaseId: DATABASE_ID,
      tableId: TABLE_ID,
      rowId: row.$id,
      data: { completed: true },
    });
    ```
    
    #### Delete a Row
    
    ```typescript
    await tablesDB.deleteRow({
      databaseId: DATABASE_ID,
      tableId: TABLE_ID,
      rowId: row.$id,
    });
    ```
    
    **Why good:** Named constants for database/table IDs, `ID.unique()` for auto-generated IDs, permissions set explicitly on create, `Query` class for type-safe filtering, generic type parameter for row shape
    
    ---
    
    ### Pattern 5: Permissions Model
    
    Appwrite grants NO access by default. You must set permissions on every row and file.
    
    #### Permission + Role Syntax
    
    ```typescript
    import { Permission, Role } from "appwrite";
    
    // Public read, owner full access
    const publicReadPermissions = [
      Permission.read(Role.any()),
      Permission.update(Role.user(userId)),
      Permission.delete(Role.user(userId)),
    ];
    
    // Team access
    const teamPermissions = [
      Permission.read(Role.team(teamId)),
      Permission.update(Role.team(teamId, "admin")),
      Permission.delete(Role.team(teamId, "owner")),
    ];
    
    // Verified users only
    const verifiedOnlyPermissions = [Permission.read(Role.users("verified"))];
    ```
    
    #### Common Role Helpers
    
    ```typescript
    Role.any(); // Anyone (including guests)
    Role.guests(); // Unauthenticated users only
    Role.users(); // All authenticated users
    Role.users("verified"); // Only verified users
    Role.user(userId); // Specific user by ID
    Role.team(teamId); // All members of a team
    Role.team(teamId, "admin"); // Team members with "admin" role
    Role.member(membershipId); // Specific team membership
    Role.label("premium"); // Users with "premium" label
    ```
    
    **Why good:** Explicit permissions on every resource, role helpers are type-safe, team roles enable granular access
    
    ```typescript
    // BAD: Creating a row without permissions
    const row = await tablesDB.createRow({
      databaseId: DATABASE_ID,
      tableId: TABLE_ID,
      rowId: ID.unique(),
      data: { title: "Secret" },
      // NO permissions set — this row is INVISIBLE to everyone!
    });
    ```
    
    **Why bad:** Without permissions, the row exists in the database but NO user can read, update, or delete it (not even the creator) — this is the most common Appwrite mistake
    
    ---
    
    ### Pattern 6: Authentication Flows
    
    Use the `Account` service for all auth operations.
    
    #### Email/Password Sign Up and Sign In
    
    ```typescript
    import { ID, type Models } from "appwrite";
    
    // Sign up: create account + create session (two steps)
    const user = await account.create({
      userId: ID.unique(),
      email,
      password,
      name,
    });
    const session = await account.createEmailPasswordSession({ email, password });
    
    // Sign in
    const session = await account.createEmailPasswordSession({ email, password });
    
    // Sign out (current device)
    await account.deleteSession({ sessionId: "current" });
    ```
    
    **Why good:** Two-step sign up (create account + create session), object parameters for all SDK calls, `deleteSession({ sessionId: "current" })` for current device only
    
    See [examples/auth.md](examples/auth.md) for full auth flows including OAuth, magic URL, anonymous sessions, teams, and password reset.
    
    ---
    
    ### Pattern 7: Storage Operations
    
    Upload, download, and preview files with the `Storage` service.
    
    ```typescript
    import { ID, Permission, Role } from "appwrite";
    
    const BUCKET_ID = "user-uploads";
    const PREVIEW_WIDTH = 400;
    const PREVIEW_HEIGHT = 300;
    
    // Upload with permissions
    await storage.createFile({
      bucketId: BUCKET_ID,
      fileId: ID.unique(),
      file,
      permissions: [
        Permission.read(Role.user(userId)),
        Permission.update(Role.user(userId)),
      ],
    });
    
    // Image preview with transforms
    const url = storage.getFilePreview({
      bucketId: BUCKET_ID,
      fileId,
      width: PREVIEW_WIDTH,
      height: PREVIEW_HEIGHT,
    });
    
    // Download URL
    const downloadUrl = storage.getFileDownload({ bucketId: BUCKET_ID, fileId });
    ```
    
    **Why good:** Object parameters for all SDK calls, named constants, `ID.unique()` for file IDs, permissions set on upload
    
    See [examples/storage.md](examples/storage.md) for full upload, preview, download, and file management patterns.
    
    ---
    
    ### Pattern 8: Realtime Subscriptions
    
    Subscribe to changes using the `Realtime` service with `Channel` helpers.
    
    ```typescript
    import { Realtime, Channel } from "appwrite";
    
    const DATABASE_ID = "main";
    const TABLE_ID = "messages";
    
    // Subscribe to all changes on a table
    const subscription = await realtime.subscribe(
      Channel.tablesdb(DATABASE_ID).table(TABLE_ID),
      (response) => {
        if (response.events.includes("tablesdb.*.tables.*.rows.*.create")) {
          console.log("New row:", response.payload);
        }
        if (response.events.includes("tablesdb.*.tables.*.rows.*.update")) {
          console.log("Updated row:", response.payload);
        }
        if (response.events.includes("tablesdb.*.tables.*.rows.*.delete")) {
          console.log("Deleted row:", response.payload);
        }
      },
    );
    
    // IMPORTANT: Always unsubscribe when done
    await subscription.close();
    ```
    
    #### Subscribe to Files
    
    ```typescript
    const fileSubscription = await realtime.subscribe(
      Channel.files(),
      (response) => {
        if (response.events.includes("buckets.*.files.*.create")) {
          console.log("New file uploaded:", response.payload);
        }
      },
    );
    ```
    
    #### Subscribe to Account Changes
    
    ```typescript
    const accountSub = await realtime.subscribe(Channel.account(), (response) => {
      console.log("Account event:", response.events);
    });
    ```
    
    **Why good:** `Channel` helpers provide type-safe channel construction, event string matching for filtering, explicit cleanup with `subscription.close()`, separate subscriptions per concern
    
    **When to use:** Chat apps, live dashboards, collaborative editing, notification feeds. Avoid for high-frequency data streams.
    
    </patterns>
    
    ---
    
    <decision_framework>
    
    ## Decision Framework
    
    ### Client SDK vs Server SDK
    
    ```
    Where is the code running?
    +-  Browser / Client-side -> `appwrite` package, session auth
    +-  Server / API route -> `node-appwrite` package, API key auth
    +-  Serverless function -> `node-appwrite` package, dynamic key from `req.headers['x-appwrite-key']`
        +-- NEVER expose API keys in client bundles
    ```
    
    ### TablesDB vs Legacy Databases
    
    ```
    Which API should I use?
    +-  New project -> TablesDB (tables, rows, columns)
    +-  Existing project with collections -> Legacy Databases still works (deprecated, security patches only)
    +-  Migrating -> Adopt TablesDB for new tables, migrate existing gradually
    ```
    
    ### Auth Method Selection
    
    ```
    What auth flow does the user need?
    +-  Email + Password -> account.create({ ... }) + account.createEmailPasswordSession({ ... })
    +-  Social login (GitHub, Google, etc.) -> account.createOAuth2Session({ provider: OAuthProvider.Github, ... })
    +-  Passwordless email -> account.createMagicURLToken({ ... })
    +-  Phone + SMS -> account.createPhoneToken({ ... })
    +-  Guest / anonymous -> account.createAnonymousSession()
    +-  Existing JWT -> client.setJWT()
    ```
    
    ### Permissions Strategy
    
    ```
    Who should access this resource?
    +-  Public (anyone) -> Permission.read(Role.any())
    +-  Owner only -> Permission.read(Role.user(userId))
    +-  Team members -> Permission.read(Role.team(teamId))
    +-  Team role (e.g. admins) -> Permission.read(Role.team(teamId, "admin"))
    +-  Verified users -> Permission.read(Role.users("verified"))
    +-  Labeled users -> Permission.read(Role.label("premium"))
    ```
    
    ### Storage: Public vs Private
    
    ```
    Who should see the files?
    +-  Anyone (public assets) -> Set Permission.read(Role.any()) on the file
    +-  Owner only (private docs) -> Set Permission.read(Role.user(userId))
    +-  Team access -> Set Permission.read(Role.team(teamId))
    +-  Server-only -> Use server SDK with API key (bypasses all permissions)
    ```
    
    </decision_framework>
    
    ---
    
    <red_flags>
    
    ## RED FLAGS
    
    **High Priority Issues:**
    
    - **No permissions on rows/files** — Appwrite grants ZERO access by default. A row without permissions is completely invisible. This is the number one mistake new Appwrite developers make.
    - **API key in client bundle** — The `node-appwrite` server SDK with API key auth must NEVER be imported in browser code. API keys bypass all permissions.
    - **Assuming `{ data, error }` return pattern** — Appwrite throws `AppwriteException`, not error tuples. Using destructuring like `const { data, error } = await account.get()` will fail silently.
    - **Using legacy Databases API for new projects** — `Databases` (collections/documents) is deprecated as of Appwrite 1.8. Use `TablesDB` (tables/rows) for all new work.
    - **Using positional parameters** — As of `appwrite@19.0.0` / `node-appwrite@18.0.0` (September 2025), all SDK methods use object parameters. Positional arguments are deprecated. Use `account.create({ userId, email, password })` not `account.create(userId, email, password)`.
    - **Using string literals for OAuth providers** — Use the `OAuthProvider` enum: `OAuthProvider.Github` not `"github"`.
    
    **Medium Priority Issues:**
    
    - **Forgetting `ID.unique()`** — Passing a raw string as a row/file ID creates a custom ID. If you want auto-generated IDs, you must explicitly call `ID.unique()`.
    - **Not unsubscribing from Realtime** — Each `realtime.subscribe()` adds to a shared WebSocket. Failing to `subscription.close()` leaks connections and causes duplicate events.
    - **Using `Permission.write()` when you mean specific actions** — `Permission.write()` is an alias for create + update + delete combined. Use `Permission.update()` and `Permission.delete()` separately for granular control.
    - **Calling `account.create()` without `createEmailPasswordSession()`** — `create()` registers the user but does NOT create a session. The user is not logged in until you call a session method.
    - **Server SDK Realtime** — Appwrite Realtime is NOT available through server SDKs. Subscriptions only work with client SDKs.
    
    **Common Mistakes:**
    
    - **OAuth: no code runs after `createOAuth2Session()`** — This method triggers a browser redirect. Any code after the call will not execute.
    - **Not setting both success AND failure URLs for OAuth** — If you omit the failure URL, failed auth has no redirect target.
    - **Using `Role.users()` when you mean `Role.user(userId)`** — `Role.users()` grants access to ALL authenticated users. `Role.user(userId)` grants access to ONE specific user.
    - **Expecting `$createdAt` / `$updatedAt` in queries without `$` prefix** — System fields in Appwrite use `$` prefix: `$id`, `$createdAt`, `$updatedAt`, `$permissions`.
    
    **Gotchas & Edge Cases:**
    
    - **Row security vs table-level permissions** — By default, only table-level permissions apply. To use row-level (document-level) permissions, you must enable "Row Security" in the table settings. When enabled, BOTH table-level AND row-level permissions must pass.
    - **`Permission.read(Role.any())` includes guests** — `Role.any()` means everyone, including unauthenticated users. Use `Role.users()` for authenticated-only access.
    - **Realtime reconnection creates new WebSocket** — Adding or removing a subscription tears down and recreates the entire WebSocket connection. Batch subscription changes where possible.
    - **File preview only works for images** — `storage.getFilePreview()` with width/height transforms only works on image files. For non-images, use `getFileDownload()` or `getFileView()`.
    - **`account.get()` throws if no session** — There is no "null user" return. An unauthenticated call throws 401. Wrap in try/catch and return `null` for "no user" state.
    - **Max 100 rows per `listRows()` call** — The default and maximum limit is 100. For larger datasets, use cursor-based pagination with `Query.cursorAfter()`.
    - **Appwrite function cold starts** — Functions have cold start latency on first invocation after idle. Design "fat functions" (one function handling multiple routes) to reduce cold starts.
    - **`Query.search()` requires a full-text index** — The `search` query method only works on columns that have a full-text index configured in the Appwrite console.
    - **Labels are server-only** — User labels (`Role.label("premium")`) can only be set via the server SDK `users.updateLabels()`. Clients cannot modify their own labels.
    
    </red_flags>
    
    ---
    
    <critical_reminders>
    
    ## CRITICAL REMINDERS
    
    > **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)**
    
    **Failure to follow these rules will create inaccessible data, security vulnerabilities, and silent runtime failures.**
    
    </critical_reminders>
    

Comments (0)

Sign in to join the conversation.

No comments yet.

Reviews (0)

No reviews yet.

Related