Communitygithub.com

agents-inc/skills

Appwrite 后端即服务 — 身份认证、TablesDB、存储、云函数、实时订阅、权限模型、类型化客户端

skills 是什么?

该技能提供了使用 Appwrite 作为开源后端即服务(BaaS)平台的完整实践指南,涵盖身份认证、结构化数据(TablesDB)、文件存储、无服务器函数和实时订阅等功能。它强调了 Appwrite 的“默认安全”权限模型——每行数据和每个文件都必须显式设置权限,否则将无法被任何用户访问。技能详细说明了客户端(`appwrite`,使用会话认证)和服务器端(`node-appwrite`,使用 API 密钥认证)两个 SDK 的初始化与使用模式,并提供了包括邮箱/密码、OAuth、魔法链接和匿名会话在内的完整认证流程。此外,它还介绍了 TablesDB 的增删改查操作、存储文件的上传与预览、云函数的编写与部署,以及通过 `Channel` 助手和 `realtime.subscribe()` 方法实现的实时订阅。技能特别强调了几个关键注意事项:必须使用 `AppwriteException` 进行错误处理(Appwrite 不返回 `{ data, error }` 元组),必须使用 `ID.unique()` 生成自动递增的 ID,以及 TablesDB 是未来主要的数据库 API。该技能适用于 Cursor、Claude Code 或 Codex 等 AI 编码助手,帮助开发者快速、安全地在 TypeScript 项目中集成 Appwrite。

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

在你喜欢的 AI 中提问

打开一个已预加载此 Agent Skill 的新对话。

文档

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)

详细资源:

客户端与查询:

身份认证:

  • examples/auth.md — 完整的认证流程、OAuth、魔法链接、会话、团队

存储与函数:

实时:


理念

Appwrite 是一个开源的后端即服务平台,通过统一的 SDK 提供身份认证、数据库(TablesDB)、文件存储、无服务器函数和实时功能。它可以自托管或通过 Appwrite Cloud 使用。

核心原则:

  1. 默认安全 — 没有显式权限,任何内容都不可访问。每一行数据和每一个文件都必须设置权限,否则对所有用户都不可见。这与“默认开放”相反——把它当作一个强制步骤,而不是事后才考虑的事情。
  2. 服务类架构 — 所有 SDK 交互都通过从共享的 Client 实例化的服务类(Account、TablesDB、Storage、Functions、Teams、Realtime)进行。此模式在客户端和服务端 SDK 中保持一致。
  3. 两个 SDK 分离 — appwrite(客户端)为浏览器使用基于会话的认证。node-appwrite(服务器)为后端使用 API 密钥认证。它们共享相同的 API 形状,但使用不同的认证机制。切勿混用。
  4. 异常,而非元组 — Appwrite 在失败时会抛出 AppwriteException,而不是返回 { data, error } 元组。始终将调用包裹在 try/catch 中。
  5. TablesDB 是未来 — Appwrite 1.8 引入了 TablesDB(表/行/列)作为现代 API。传统的 Databases API(集合/文档/属性)仍然有效但已弃用。新功能只会登陆 TablesDB。
  6. 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 了解团队管理的更多上下文,包括在权限中设置团队范围的访问。

Individual skills in this repo

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

相关技能