jpskill.com
🛠️ 開発・MCP コミュニティ

api-baas-appwrite

Appwriteは、認証、データベース、ストレージ、関数、リアルタイム機能などを備えた包括的なバックエンドサービスで、権限管理や型付きクライアントも提供し、アプリケーション開発を効率化するSkill。

📜 元の英語説明(参考)

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

🇯🇵 日本人クリエイター向け解説

一言でいうと

Appwriteは、認証、データベース、ストレージ、関数、リアルタイム機能などを備えた包括的なバックエンドサービスで、権限管理や型付きクライアントも提供し、アプリケーション開発を効率化するSkill。

※ jpskill.com 編集部が日本のビジネス現場向けに補足した解説です。Skill本体の挙動とは独立した参考情報です。

⚡ おすすめ: コマンド1行でインストール(60秒)

下記のコマンドをコピーしてターミナル(Mac/Linux)または PowerShell(Windows)に貼り付けてください。 ダウンロード → 解凍 → 配置まで全自動。

🍎 Mac / 🐧 Linux
mkdir -p ~/.claude/skills && cd ~/.claude/skills && curl -L -o api-baas-appwrite.zip https://jpskill.com/download/10223.zip && unzip -o api-baas-appwrite.zip && rm api-baas-appwrite.zip
🪟 Windows (PowerShell)
$d = "$env:USERPROFILE\.claude\skills"; ni -Force -ItemType Directory $d | Out-Null; iwr https://jpskill.com/download/10223.zip -OutFile "$d\api-baas-appwrite.zip"; Expand-Archive "$d\api-baas-appwrite.zip" -DestinationPath $d -Force; ri "$d\api-baas-appwrite.zip"

完了後、Claude Code を再起動 → 普通に「動画プロンプト作って」のように話しかけるだけで自動発動します。

💾 手動でダウンロードしたい(コマンドが難しい人向け)
  1. 1. 下の青いボタンを押して api-baas-appwrite.zip をダウンロード
  2. 2. ZIPファイルをダブルクリックで解凍 → api-baas-appwrite フォルダができる
  3. 3. そのフォルダを C:\Users\あなたの名前\.claude\skills\(Win)または ~/.claude/skills/(Mac)へ移動
  4. 4. Claude Code を再起動

⚠️ ダウンロード・利用は自己責任でお願いします。当サイトは内容・動作・安全性について責任を負いません。

🎯 このSkillでできること

下記の説明文を読むと、このSkillがあなたに何をしてくれるかが分かります。Claudeにこの分野の依頼をすると、自動で発動します。

📦 インストール方法 (3ステップ)

  1. 1. 上の「ダウンロード」ボタンを押して .skill ファイルを取得
  2. 2. ファイル名の拡張子を .skill から .zip に変えて展開(macは自動展開可)
  3. 3. 展開してできたフォルダを、ホームフォルダの .claude/skills/ に置く
    • · macOS / Linux: ~/.claude/skills/
    • · Windows: %USERPROFILE%\.claude\skills\

Claude Code を再起動すれば完了。「このSkillを使って…」と話しかけなくても、関連する依頼で自動的に呼び出されます。

詳しい使い方ガイドを見る →
最終更新
2026-05-18
取得日時
2026-05-18
同梱ファイル
1

📖 Skill本文(日本語訳)

※ 原文(英語/中国語)を Gemini で日本語化したものです。Claude 自身は原文を読みます。誤訳がある場合は原文をご確認ください。

Appwrite パターン

クイックガイド: Appwrite を認証、構造化データ (TablesDB)、ファイルストレージ、サーバーレス関数、リアルタイムサブスクリプションのためのオープンソース BaaS として使用します。常に共有の Client インスタンスからサービスクラスを初期化し、すべての行に明示的に権限を設定してください (デフォルトでは何もアクセスできません)。また、バックエンドでのみ API キー認証でサーバー SDK (node-appwrite) を使用してください。


<critical_requirements>

重要: この Skill を使用する前に

すべてのコードは CLAUDE.md のプロジェクト規約に従う必要があります (kebab-case、名前付きエクスポート、インポート順序、import type、名前付き定数)

(すべての行とファイルに明示的に権限を設定する必要があります — Appwrite はデフォルトではアクセスを許可しないため、権限を省略するとデータにアクセスできなくなります)

(サーバーでは API キー認証で node-appwrite を、クライアントではセッション認証で appwrite を使用する必要があります — クライアントバンドルで API キーを公開しないでください)

(すべての SDK 呼び出しで常に AppwriteException を確認する必要があります — Appwrite は例外をスローし、{ data, error } タプルを返しません)

(自動生成された ID には ID.unique() を使用する必要があります — 生の文字列を渡すと、自動生成された ID ではなくカスタム ID が作成されます)

(新しいプロジェクトには TablesDB API を使用する必要があります — レガシーの Databases/collections/documents API は非推奨であり、セキュリティパッチのみが提供されます)

</critical_requirements>


自動検出: Appwrite, appwrite, node-appwrite, TablesDB, Account, Databases, Permission, Role, ID.unique, Query.equal, createEmailPasswordSession, realtime.subscribe, Channel.files, Channel.tablesdb, APPWRITE_FUNCTION

使用する場面:

  • TypeScript で Appwrite クライアントまたはサーバー SDK をセットアップする
  • 認証 (メール/パスワード、OAuth、マジック URL、電話 OTP、匿名セッション) を実装する
  • TablesDB テーブル (行、クエリ、権限) に対して CRUD を実行する
  • アクセス制御付きでストレージバケットからファイルをアップロードおよび提供する
  • イベント、スケジュール、または HTTP によってトリガーされるサーバーレス関数を構築する
  • 行、ファイル、またはアカウントイベントのリアルタイム変更をサブスクライブする
  • チームメンバーシップとロールベースの権限を管理する

カバーされる主要なパターン:

  • 型付きサービスクラスを使用したクライアントおよびサーバー SDK の初期化
  • 認証フロー: サインアップ、サインイン、OAuth、マジック URL、セッション管理
  • フィルタリング、ページネーション、順序付けのための Query クラスを使用した TablesDB 操作
  • 権限モデル: Permission.read(Role.any())Role.user()Role.team()
  • ストレージ: アップロード、ダウンロード、画像変換によるプレビュー、バケット権限
  • サーバーレス関数: export default async ({ req, res, log, error }) => {}
  • Channel ヘルパーと realtime.subscribe() によるリアルタイムサブスクリプション

使用しない場面:

  • 直接的なデータベース接続または SQL クエリ (Appwrite は API のみで、生の SQL はありません)
  • 多数のテーブルにわたる複雑なリレーショナル結合 (Appwrite はリレーションシップをサポートしますが、任意の SQL 結合はサポートしません)
  • サーバー側のリアルタイムを必要とするアプリケーション (Appwrite Realtime はクライアント SDK のみです)

詳細なリソース:

  • 意思決定フレームワークとアンチパターンについては、reference.md を参照してください。

クライアントとクエリ:

  • examples/core.md — クライアントのセットアップ、TablesDB CRUD、エラー処理、権限

認証:

  • examples/auth.md — 完全な認証フロー、OAuth、マジック URL、セッション、チーム

ストレージと関数:

  • examples/storage.md — ファイルのアップロード、ダウンロード、プレビュー、バケット権限
  • examples/functions.md — サーバーレス関数、トリガー、関数内での SDK の使用

リアルタイム:

  • examples/realtime.md — チャネルサブスクリプション、イベントフィルタリング、クリーンアップ

<philosophy>

哲学

Appwrite は、認証、データベース (TablesDB)、ファイルストレージ、サーバーレス関数、およびリアルタイムをすべて統合された SDK を通じて提供するオープンソースの Backend-as-a-Service です。セルフホストすることも、Appwrite Cloud 経由で使用することもできます。

コア原則:

  1. デフォルトでセキュア — 明示的な権限なしには何もアクセスできません。すべての行とファイルには権限を設定する必要があります。そうしないと、すべてのユーザーに表示されません。これは「デフォルトでオープン」とは逆です — 後付けではなく、必須のステップとして扱ってください。
  2. サービスクラスアーキテクチャ — すべての SDK インタラクションは、共有の Client からインスタンス化されたサービスクラス (AccountTablesDBStorageFunctionsTeamsRealtime) を介して行われます。このパターンは、クライアントとサーバーの SDK で一貫しています。
  3. 2 つの SDK 分割appwrite (クライアント) はブラウザ用にセッションベースの認証を使用します。node-appwrite (サーバー) はバックエンド用に API キー認証を使用します。それらは同じ API 形状を共有しますが、異なる認証メカニズムを使用します。決して混ぜないでください。
  4. タプルではなく例外 — Appwrite は失敗時に { data, error } タプルではなく AppwriteException をスローします。常に try/catch で呼び出しをラップしてください。
  5. TablesDB が未来 — Appwrite 1.8 では、最新の API として TablesDB (テーブル/行/列) が導入されました。レガシーの Databases API (コレクション/ドキュメント/属性) はまだ動作しますが、非推奨です。新しい機能は TablesDB にのみ実装されます。
  6. ID 生成 — サーバーで生成された一意の ID には ID.unique() を使用します。文字列を直接渡すと、カスタム ID になります (テーブル内でグローバルに一意である必要があります)。

Appwrite を使用する場面:

  • データを完全に制御できるオープンソースのセルフホスト可能な BaaS
  • 認証、データベース、ストレージ、および関数を 1 つのプラットフォームで必要とするプロジェクト
  • ベンダーロックインのないセルフホスト可能な BaaS を必要とするチーム
  • 粒度の細かいドキュメント/行レベルの権限からメリットを得られるアプリケーション

使用しない場面:

  • 結合、ビュー、およびストアドプロシージャを必要とする複雑な SQL ヘビーなアプリケーション
  • サーバー側のリアルタイムコンシューマー (Appwrite Realtime はクライアント SDK のみです — サーバー SDK のサポートはありません)
  • ローカルファーストの同期を必要とするオフラインファーストアプリ (Appwrite には組み込みのオフライン同期はありません)

</philosophy>


<patterns>

コアパターン

パターン 1: クライアント SDK のセットアップ (ブラウザ)

Appwrite クライアントをエンドポイントとプロジェクト ID で初期化します。すべてのサービスクラスは単一の Client インスタンスを共有します。

(原文がここで切り詰められています)

📜 原文 SKILL.md(Claudeが読む英語/中国語)を展開

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:


<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 splitappwrite (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.

// lib/appwrite.ts
import { Client, Account, TablesDB, Storage, Realtime } from "appwrite";

const APPWRITE_ENDPOINT = process.env.APPWRITE_ENDPOINT!;
const APPWRITE_PROJECT_ID = process.env.APPWRITE_PROJECT_ID!;

const client = new Client()
  .setEndpoint(APPWRITE_ENDPOINT)
  .setProject(APPWRITE_PROJECT_ID);

export const account = new Account(client);
export const tablesDB = new TablesDB(client);
export const storage = new Storage(client);
export const realtime = new Realtime(client);

Why good: Single client instance shared across services, named constants for config, named exports for each service, environment variables keep config out of code

// BAD: Hardcoded config, recreating clients
import { Client, Account } from "appwrite";

const account = new Account(
  new Client()
    .setEndpoint("https://cloud.appwrite.io/v1") // Hardcoded
    .setProject("abc123"), // Hardcoded project ID
);

Why bad: Hardcoded endpoint and project ID, new Client per service wastes resources, no shared config


Pattern 2: Server SDK Setup (Node.js)

The server SDK uses API key authentication — never expose API keys in client code.

// lib/appwrite-server.ts
import { Client, TablesDB, Users, Storage } from "node-appwrite";

const APPWRITE_ENDPOINT = process.env.APPWRITE_ENDPOINT!;
const APPWRITE_PROJECT_ID = process.env.APPWRITE_PROJECT_ID!;
const APPWRITE_API_KEY = process.env.APPWRITE_API_KEY!;

const client = new Client()
  .setEndpoint(APPWRITE_ENDPOINT)
  .setProject(APPWRITE_PROJECT_ID)
  .setKey(APPWRITE_API_KEY);

export const tablesDB = new TablesDB(client);
export const users = new Users(client);
export const storage = new Storage(client);

Why good: .setKey() for API key auth (server only), Users service for admin user management (not available in client SDK), named constants

When to use: API routes, serverless functions, admin scripts, webhooks. NEVER import this module from client-side code.


Pattern 3: Error Handling (AppwriteException)

Appwrite throws AppwriteException — it does NOT return { data, error } tuples. Always use try/catch.

import { AppwriteException, type Models } from "appwrite";

async function getUser(): Promise<Models.User<Models.Preferences>> {
  try {
    return await account.get();
  } catch (error) {
    if (error instanceof AppwriteException) {
      // error.message — 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

// 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

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

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

const updated = await tablesDB.updateRow<Todo>({
  databaseId: DATABASE_ID,
  tableId: TABLE_ID,
  rowId: row.$id,
  data: { completed: true },
});

Delete a Row

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

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

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

// 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

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 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.

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 for full upload, preview, download, and file management patterns.


Pattern 8: Realtime Subscriptions

Subscribe to changes using the Realtime service with Channel helpers.

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

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

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 projectsDatabases (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 actionsPermission.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 guestsRole.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 imagesstorage.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>