Appwrite 模式
快速指南: 使用 Appwrite 作為開源 BaaS,處理認證、結構化資料 (TablesDB)、檔案儲存、無伺服器函式與即時訂閱。務必從共用的
Client實例初始化服務類別、明確設定每個資料列與檔案的權限(預設無任何存取權限),並且只在後端使用 API 金鑰認證的伺服器 SDK (node-appwrite)。
<critical_requirements>
關鍵:使用此技能前
所有程式碼必須遵循 CLAUDE.md 中的專案慣例(kebab-case、具名匯出、匯入順序、
import type、具名常數)
(你必須在每個資料列與檔案上明確設定權限——Appwrite 預設不授予任何存取權限,省略權限會使資料無法存取)
(你必須在伺服器端使用 node-appwrite 搭配 API 金鑰認證,在客戶端使用 appwrite 搭配 Session 認證——絕不要在客戶端套件中暴露 API 金鑰)
(你必須在每次 SDK 呼叫時檢查 AppwriteException——Appwrite 會拋出例外,不會回傳 { data, error } 元組)
(你必須使用 ID.unique() 來產生自動 ID——傳入原始字串會建立自訂 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、魔術連結、手機 OTP、匿名 Session)
- 對 TablesDB 表格執行 CRUD 操作(資料列、查詢、權限)
- 上傳並提供來自 Storage 儲存桶的檔案,並搭配存取控制
- 建立由事件、排程或 HTTP 觸發的無伺服器函式
- 訂閱資料列、檔案或帳戶事件的即時變更
- 管理團隊成員資格與角色型權限
涵蓋的主要模式:
- 使用型別服務類別的客戶端與伺服器 SDK 初始化
- 認證流程:註冊、登入、OAuth、魔術連結、Session 管理
- 使用
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 JOIN)
- 需要伺服器端即時通訊的應用程式(Appwrite Realtime 僅限客戶端 SDK)
詳細資源:
- 決策框架與反模式,請參閱 reference.md
客戶端與查詢:
- examples/core.md — 客戶端設定、TablesDB CRUD、錯誤處理、權限
認證:
- examples/auth.md — 完整的認證流程、OAuth、魔術連結、Session、團隊
儲存與函式:
- examples/storage.md — 檔案上傳、下載、預覽、儲存桶權限
- examples/functions.md — 無伺服器函式、觸發條件、在函式內使用 SDK
即時通訊:
- examples/realtime.md — Channel 訂閱、事件過濾、清理
理念
Appwrite 是一個開源後端即服務平台,透過統一的 SDK 提供認證、資料庫 (TablesDB)、檔案儲存、無伺服器函式與即時通訊。它可以自託管或使用 Appwrite Cloud。
核心原則:
- 預設安全 — 未經明確權限,任何內容都無法存取。每個資料列與檔案都必須設定權限,否則對所有使用者都不可見。這與「預設開放」相反——請將其視為強制步驟,而非事後補救。
- 服務類別架構 — 所有 SDK 互動都透過從共用
Client實例化的服務類別(Account、TablesDB、Storage、Functions、Teams、Realtime)進行。此模式在客戶端與伺服器 SDK 中保持一致。 - 兩種 SDK 拆分 —
appwrite(客戶端)對瀏覽器使用 Session 認證。node-appwrite(伺服器)對後端使用 API 金鑰認證。它們共享相同的 API 形狀,但認證機制不同。切勿混用。 - 例外,而非元組 — Appwrite 在失敗時會拋出
AppwriteException,而非{ data, error }元組。務必將呼叫包在 try/catch 中。 - TablesDB 是未來 — Appwrite 1.8 引入了 TablesDB(表格/資料列/欄位)作為現代 API。舊版 Databases API(集合/文件/屬性)仍然可用但已棄用。新功能只會出現在 TablesDB 中。
- ID 產生 — 使用
ID.unique()產生伺服器端獨特 ID。如果直接傳入字串,它會變成自訂 ID(必須在表格內全域唯一)。
何時使用 Appwrite:
- 開源、可自託管的 BaaS,可完全控制資料
- 需要在一個平台中整合認證、資料庫、儲存與函式的專案
- 團隊需要可自託管且無供應商鎖定的 BaaS
- 受益於細粒度文件/資料列級權限的應用程式
不應使用的情況:
- 需要聯結、檢視與預存程序的複雜 SQL 應用程式
- 需要伺服器端即時通訊的消費者應用(Appwrite Realtime 僅限客戶端 SDK——無伺服器端 SDK 支援)
- 需要本機優先同步的離線優先應用程式(Appwrite 無內建離線同步功能)
核心模式
模式 1:客戶端 SDK 設定 (瀏覽器)
使用端點與專案 ID 初始化 Appwrite 客戶端。所有服務類別共享一個 Client 實例。
// 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);
為何良好: 單一客戶端實例在所有服務間共享、設定的具名常數、每個服務的具名匯出、環境變數將設定與程式碼分離
// 錯誤:硬編碼設定、重新建立客戶端
import { Client, Account } from "appwrite";
const account = new Account(
new Client()
.setEndpoint("https://cloud.appwrite.io/v1") // 硬編碼
.setProject("abc123"), // 硬編碼專案 ID
);
為何錯誤: 硬編碼的端點與專案 ID、每個服務建立新 Client 浪費資源、無共用設定
模式 2:伺服器 SDK 設定 (Node.js)
伺服器 SDK 使用 API 金鑰認證——切勿在客戶端程式碼中暴露 API 金鑰。
// 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);
為何良好: 使用 .setKey() 進行 API 金鑰認證(僅限伺服器)、Users 服務提供管理員使用者管理(客戶端 SDK 中不可用)、具名常數
使用時機: API 路由、無伺服器函式、管理腳本、Webhook。絕不要從客戶端程式碼匯入此模組。
模式 3:錯誤處理 (AppwriteException)
Appwrite 會拋出 AppwriteException——它不會回傳 { data, error } 元組。務必使用 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 — 人類可讀的訊息
// error.code — HTTP 狀態碼 (401, 404, 等)
// error.type — 機器可讀的錯誤類型字串
throw new Error(`Appwrite 錯誤 ${error.code}: ${error.message}`);
}
throw error;
}
}
為何良好: 型別收窄的 AppwriteException 提供了 code、message、type;重新拋出未知錯誤;使用 Models 命名空間明確指定回傳型別
// 錯誤:假設回傳 { data, error } 元組
const { data, error } = await account.get(); // 錯誤——Appwrite 會拋出例外,而非回傳元組
if (error) {
/* ... */
}
為何錯誤: Appwrite 不會回傳錯誤元組——這段程式碼會靜默失敗,因為 account.get() 要不是回傳資料,就是拋出例外
模式 4:TablesDB 操作 (CRUD)
使用 TablesDB 進行所有資料庫操作。建立時需要資料列 ID,請使用 ID.unique() 進行自動產生。
建立一個資料列
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: "買雜貨",
completed: false,
},
permissions: [
Permission.read(Role.user(userId)),
Permission.update(Role.user(userId)),
Permission.delete(Role.user(userId)),
],
});
使用查詢列出資料列
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 — Todo 資料列的陣列
// result.total — 符合的總數
更新一個資料列
const updated = await tablesDB.updateRow<Todo>({
databaseId: DATABASE_ID,
tableId: TABLE_ID,
rowId: row.$id,
data: { completed: true },
});
刪除一個資料列
await tablesDB.deleteRow({
databaseId: DATABASE_ID,
tableId: TABLE_ID,
rowId: row.$id,
});
為何良好: 資料庫/表格 ID 使用具名常數、使用 ID.unique() 產生自動 ID、建立時明確設定權限、使用 Query 類別進行型別安全的篩選、使用泛型型別參數指定資料列形狀
模式 5:權限模型
Appwrite 預設不授予任何存取權限。你必須為每個資料列與檔案設定權限。
Permission + Role 語法
import { Permission, Role } from "appwrite";
// 公開讀取,擁有者完整存取
const publicReadPermissions = [
Permission.read(Role.any()),
Permission.update(Role.user(userId)),
Permission.delete(Role.user(userId)),
];
// 團隊存取
const teamPermissions = [
Permission.read(Role.team(teamId)),
Permission.update(Role.team(teamId, "admin")),
Permission.delete(Role.team(teamId, "owner")),
];
// 僅限已驗證使用者
const verifiedOnlyPermissions = [Permission.read(Role.users("verified"))];
常用 Role 輔助函式
Role.any(); // 任何人(包括訪客)
Role.guests(); // 僅未認證使用者
Role.users(); // 所有已認證使用者
Role.users("verified"); // 僅已驗證使用者
Role.user(userId); // 特定使用者(依 ID)
Role.team(teamId); // 團隊的所有成員
Role.team(teamId, "admin"); // 具有 "admin" 角色的團隊成員
Role.member(membershipId); // 特定的團隊成員資格
Role.label("premium"); // 具有 "premium" 標籤的使用者
為何良好: 每個資源都明確設定權限、Role 輔助函式是型別安全的、團隊角色可實現細粒度存取
// 錯誤:建立資料列時未設定權限
const row = await tablesDB.createRow({
databaseId: DATABASE_ID,
tableId: TABLE_ID,
rowId: ID.unique(),
data: { title: "秘密" },
// 未設定權限——此資料列對所有人都不可見!
});
為何錯誤: 沒有權限,資料列存在於資料庫中,但沒有使用者可以讀取、更新或刪除它(連建立者也不行)——這是最常見的 Appwrite 錯誤
模式 6:認證流程
使用 Account 服務進行所有認證操作。
電子郵件/密碼註冊與登入
import { ID, type Models } from "appwrite";
// 註冊:建立帳戶 + 建立 Session(兩個步驟)
const user = await account.create({
userId: ID.unique(),
email,
password,
name,
});
const session = await account.createEmailPasswordSession({ email, password });
// 登入
const session = await account.createEmailPasswordSession({ email, password });
// 登出(目前裝置)
await account.deleteSession({ sessionId: "current" });
為何良好: 兩步驟註冊(建立帳戶 + 建立 Session)、所有 SDK 呼叫使用物件參數、使用 deleteSession({ sessionId: "current" }) 僅登出目前裝置
完整認證流程(包括 OAuth、魔術連結、匿名 Session、團隊與密碼重設)請參閱 examples/auth.md。
模式 7:儲存操作
使用 Storage 服務上傳、下載與預覽檔案。
import { ID, Permission, Role } from "appwrite";
const BUCKET_ID = "user-uploads";
const PREVIEW_WIDTH = 400;
const PREVIEW_HEIGHT = 300;
// 上傳並設定權限
await storage.createFile({
bucketId: BUCKET_ID,
fileId: ID.unique(),
file,
permissions: [
Permission.read(Role.user(userId)),
Permission.update(Role.user(userId)),
],
});
// 含轉換的圖片預覽
const url = storage.getFilePreview({
…
[為簡潔起見已截斷]