Appwrite 模式
快速指南: 使用 Appwrite 作为你的开源 BaaS,用于身份认证、结构化数据(TablesDB)、文件存储、无服务器函数和实时订阅。始终从共享的
Client实例初始化服务类,在每一行数据上显式设置权限(默认情况下任何内容都不可访问),并且仅在后端使用服务器 SDK(node-appwrite)和 API 密钥认证。
<critical_requirements>
关键:使用此技能前必须了解
所有代码必须遵循 CLAUDE.md 中的项目约定(kebab-case、命名导出、导入顺序、
import type、命名常量)
(您必须在每一行数据和每一个文件上显式设置权限——Appwrite 默认不授予任何访问权限,因此省略权限将导致数据无法访问)
(您必须在服务器上使用带 API 密钥认证的 node-appwrite,在客户端上使用带会话认证的 appwrite——切勿在客户端代码包中暴露 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、匿名会话)
- 对 TablesDB 表执行 CRUD 操作(行、查询、权限)
- 上传和提供来自存储桶的文件,并带有访问控制
- 构建由事件、定时任务或 HTTP 触发的无服务器函数
- 订阅行、文件或账户事件的实时更改
- 管理团队会员资格和基于角色的权限
涵盖的关键模式:
- 使用类型化服务类的客户端和服务端 SDK 初始化
- 认证流程:注册、登录、OAuth、魔法链接、会话管理
- 使用
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、魔法链接、会话、团队
存储与函数:
- examples/storage.md — 文件上传、下载、预览、存储桶权限
- examples/functions.md — 无服务器函数、触发器、函数内的 SDK 使用
实时:
- examples/realtime.md — 通道订阅、事件过滤、清理
理念
Appwrite 是一个开源的后端即服务平台,通过统一的 SDK 提供身份认证、数据库(TablesDB)、文件存储、无服务器函数和实时功能。它可以自托管或通过 Appwrite Cloud 使用。
核心原则:
- 默认安全 — 没有显式权限,任何内容都不可访问。每一行数据和每一个文件都必须设置权限,否则对所有用户都不可见。这与“默认开放”相反——把它当作一个强制步骤,而不是事后才考虑的事情。
- 服务类架构 — 所有 SDK 交互都通过从共享的
Client实例化的服务类(Account、TablesDB、Storage、Functions、Teams、Realtime)进行。此模式在客户端和服务端 SDK 中保持一致。 - 两个 SDK 分离 —
appwrite(客户端)为浏览器使用基于会话的认证。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 路由、无服务器函数、管理脚本、Webhooks。切勿从客户端代码导入此模块。
模式 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";
// 注册:创建账户 + 创建会话(两步)
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" });
为何优秀: 两步注册(创建账户 + 创建会话),所有 SDK 调用使用对象参数,使用 deleteSession({ sessionId: "current" }) 仅登出当前设备
请参见 examples/auth.md 了解完整的认证流程,包括 OAuth、魔法链接、匿名会话、团队和密码重置。
模式 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({
bucketId: BUCKET_ID,
fileId: fileId,
width: PREVIEW_WIDTH,
height: PREVIEW_HEIGHT,
});
// 文件下载
await storage.getFileDownload({
bucketId: BUCKET_ID,
fileId: fileId,
});
请参见 examples/storage.md 了解完整的存储使用模式,包括存储桶级别的权限和图像转换。
模式 8:无服务器函数
Appwrite 无服务器函数通过标准处理程序接收事件。
// functions/my-function/src/main.ts
export default async ({ req, res, log, error }: any) => {
log("函数已触发");
try {
const { name } = JSON.parse(req.body || "{}");
return res.json({
message: `你好, ${name || "世界"}!`,
timestamp: new Date().toISOString(),
});
} catch (err) {
error("处理请求时出错:" + err);
return res.text("内部错误", 500);
}
};
为何优秀: 简单的处理程序签名,log/error 用于带时间戳的日志,res.json()/res.text() 用于 HTTP 响应,可以使用 node-appwrite 在函数内部访问 Appwrite。
安全提示: 在函数中,通过函数执行的执行上下文(
req.variables)访问环境变量,而不是process.env。
请参见 examples/functions.md 了解函数触发(事件、计划、HTTP)、SDK 使用和错误处理。
模式 9:实时订阅
使用客户端 SDK 中的 Realtime 服务订阅实时更改。
import { Channel } from "appwrite";
// 订阅 TablesDB 更改
const unsubscribe = realtime.subscribe(
Channel.tablesDB({
databaseId: "main",
tableId: "todos",
rowId: "some-row-id", // 可选:特定行
}),
(response) => {
console.log("事件类型:", response.events);
console.log("行数据:", response.payload);
},
);
// 稍后清理
unsubscribe();
// 订阅文件更改
const unsubscribeFiles = realtime.subscribe(
Channel.files({
bucketId: "user-uploads",
}),
(response) => {
console.log("文件事件:", response.events);
},
);
为何优秀: Channel 助手减少了错误的通道字符串构造,自动清理以避免内存泄漏,类型化的 events 和 payload 属性
重要提示: Appwrite Realtime 仅适用于客户端 SDK。服务器 SDK (
node-appwrite) 没有Realtime服务。
请参见 examples/realtime.md 了解通道格式化、事件过滤和清理模式的详细信息。
模式 10:团队和成员管理
使用 Teams 服务管理团队和成员角色。
import { ID, type Models } from "appwrite";
// 创建团队
const team = await teams.create({
teamId: ID.unique(),
name: "开发团队",
roles: ["admin", "member"], // 可选:自定义角色
});
// 列出用户的团队
const userTeams = await teams.list();
// 通过成员添加用户(他们必须接受)
const membership = await teams.createMembership({
teamId: team.$id,
roles: ["member"],
email: "[email protected]",
url: "https://example.com/accept", // 成员接受邀请后将重定向到此 URL
});
// 更新成员角色
await teams.updateMembershipRoles({
teamId: team.$id,
membershipId: membershipId,
roles: ["admin"],
});
为何优秀: 用于精细访问控制的自定义团队角色,createMembership 与基于权限的行和文件集成,使基于团队的资源访问变得容易。
请参见 examples/auth.md 了解团队管理的更多上下文,包括在权限中设置团队范围的访问。