关于 appwrite-dart
appwrite-dart 是面向 Flutter 客户端和 Dart 服务端的 Appwrite SDK。它提供身份验证、TablesDB、文件存储、团队、实时订阅及函数等 API,并支持权限与角色控制。
主要功能
- 01连接 Appwrite 项目
- 02处理身份验证与用户管理
- 03操作 TablesDB 和查询
- 04上传文件并设置权限
- 05管理团队与角色
- 06执行函数并订阅实时事件
适用场景
- 01在 Flutter 应用中接入 Appwrite 服务
- 02通过 Dart 服务端管理用户和函数
- 03为表行及文件配置访问权限
- 04订阅账户、表格或团队变更
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ appwrite/ skills --skill appwrite-dart安装后在 Claude Code 中输入/appwrite-dart确认已加载。
详细介绍
Appwrite Dart SDK
安装
# Flutter (client-side)
flutter pub add appwrite
# Dart (server-side)
dart pub add dart_appwrite
设置客户端
客户端(Flutter)
import 'package:appwrite/appwrite.dart';
final client = Client()
.setEndpoint('https://<REGION>.cloud.appwrite.io/v1')
.setProject('[PROJECT_ID]');
服务端(Dart)
import 'package:dart_appwrite/dart_appwrite.dart';
final client = Client()
.setEndpoint('https://<REGION>.cloud.appwrite.io/v1')
.setProject(Platform.environment['APPWRITE_PROJECT_ID']!)
.setKey(Platform.environment['APPWRITE_API_KEY']!);
代码示例
身份验证(客户端)
final account = Account(client);
// Signup
await account.create(userId: ID.unique(), email: '[email protected]', password: 'password123', name: 'User Name');
// Login
final session = await account.createEmailPasswordSession(email: '[email protected]', password: 'password123');
// OAuth login
await account.createOAuth2Session(provider: OAuthProvider.google);
// Get current user
final user = await account.get();
// Logout
await account.deleteSession(sessionId: 'current');
用户管理(服务端)
final users = Users(client);
// Create user
final user = await users.create(userId: ID.unique(), email: '[email protected]', password: 'password123', name: 'User Name');
// List users
final list = await users.list(queries: [Query.limit(25)]);
// Get user
final fetched = await users.get(userId: '[USER_ID]');
// Delete user
await users.delete(userId: '[USER_ID]');
数据库操作
注意: 所有新代码都应使用
TablesDB(而不是已弃用的Databases类)。仅当现有代码库已依赖Databases,或用户明确要求时,才使用Databases。提示: SDK 方法调用应优先使用命名参数(例如
databaseId: '...')。仅当现有代码库已使用位置参数,或用户明确要求时,才使用位置参数。
final tablesDB = TablesDB(client);
// Create database (server-side only)
final db = await tablesDB.create(databaseId: ID.unique(), name: 'My Database');
// Create table (server-side only)
final col = await tablesDB.createTable(databaseId: '[DATABASE_ID]', tableId: ID.unique(), name: 'My Table');
// Create row
final doc = await tablesDB.createRow(
databaseId: '[DATABASE_ID]',
tableId: '[TABLE_ID]',
rowId: ID.unique(),
data: {'title': 'Hello', 'done': false},
);
// Query rows
final results = await tablesDB.listRows(
databaseId: '[DATABASE_ID]',
tableId: '[TABLE_ID]',
queries: [Query.equal('done', false), Query.limit(10)],
);
// Get row
final row = await tablesDB.getRow(databaseId: '[DATABASE_ID]', tableId: '[TABLE_ID]', rowId: '[ROW_ID]');
// Update row
await tablesDB.updateRow(
databaseId: '[DATABASE_ID]',
tableId: '[TABLE_ID]',
rowId: '[ROW_ID]',
data: {'done': true},
);
// Delete row
await tablesDB.deleteRow(
databaseId: '[DATABASE_ID]',
tableId: '[TABLE_ID]',
rowId: '[ROW_ID]',
);
字符串列类型
注意: 旧版
string类型已弃用。所有新列都应使用明确的列类型。
| 类型 | 最大字符数 | 索引 | 存储 |
|---|---|---|---|
varchar | 16,383 | 完整索引(若大小 ≤ 768) | 行内 |
text | 16,383 | 仅前缀 | 行外 |
mediumtext | 4,194,303 | 仅前缀 | 行外 |
longtext | 1,073,741,823 | 仅前缀 | 行外 |
varchar存储在行内,并计入 64 KB 的行大小限制。短小且需要索引的字段(如名称、slug 或标识符)优先使用此类型。text、mediumtext和longtext存储在行外(行内仅保留一个 20 字节的指针),因此不会占用行大小配额。这些类型不需要设置size。
// Create table with explicit string column types
await tablesDB.createTable(
databaseId: '[DATABASE_ID]',
tableId: ID.unique(),
name: 'articles',
columns: [
{'key': 'title', 'type': 'varchar', 'size': 255, 'required': true}, // inline, fully indexable
{'key': 'summary', 'type': 'text', 'required': false}, // off-page, prefix index only
{'key': 'body', 'type': 'mediumtext', 'required': false}, // up to ~4 M chars
{'key': 'raw_data', 'type': 'longtext', 'required': false}, // up to ~1 B chars
],
);
查询方法
// Filtering
Query.equal('field', 'value') // == (or pass list for IN)
Query.notEqual('field', 'value') // !=
Query.lessThan('field', 100) // <
Query.lessThanEqual('field', 100) // <=
Query.greaterThan('field', 100) // >
Query.greaterThanEqual('field', 100) // >=
Query.between('field', 1, 100) // 1 <= field <= 100
Query.isNull('field') // is null
Query.isNotNull('field') // is not null
Query.startsWith('field', 'prefix') // starts with
Query.endsWith('field', 'suffix') // ends with
Query.contains('field', 'sub') // contains
Query.search('field', 'keywords') // full-text search (requires index)
// Sorting
Query.orderAsc('field')
Query.orderDesc('field')
// Pagination
Query.limit(25) // max rows (default 25, max 100)
Query.offset(0) // skip N rows
Query.cursorAfter('[ROW_ID]') // cursor pagination (preferred)
Query.cursorBefore('[ROW_ID]')
// Selection & Logic
Query.select(['field1', 'field2']) // return only specified fields
Query.or([Query.equal('a', 1), Query.equal('b', 2)]) // OR
Query.and([Query.greaterThan('age', 18), Query.lessThan('age', 65)]) // AND (default)
Query.relationship('comments', [Query.equal('approved', true), Query.limit(25)]) // related documents
// Aggregations (optional alias)
Query.count('*', 'total')
Query.sum('amount', 'revenue')
Query.avg('score', 'avgScore')
Query.min('price', 'minPrice')
Query.max('price', 'maxPrice')
// Grouping
Query.groupBy(['status', 'year'])
Query.having([Query.greaterThan('total', 1)])
Query.distinct()
// Joins (table id, left key, right key, operator, alias)
Query.join('orders', r'$id', 'customerId')
Query.leftJoin('orders', r'$id', 'customerId', '=', 'ord')
Query.leftJoin('orders', 'ord', [
Query.on(r'$id', 'customerId'),
Query.equal('ord.status', 'paid'),
])
// Fluent builder. Pass build() into list methods.
final queries = Query.builder()
.equal('status', 'published')
.leftJoin('orders', r'$id', 'customerId', '=', 'ord')
.count('*', 'total')
.groupBy(['status'])
.limit(25)
.build();
await tablesDB.listRows(
databaseId: '[DATABASE_ID]',
tableId: '[TABLE_ID]',
queries: queries,
);
文件存储
final storage = Storage(client);
// Upload file
final file = await storage.createFile(
bucketId: '[BUCKET_ID]',
fileId: ID.unique(),
file: InputFile.fromPath(path: '/path/to/file.png', filename: 'file.png'),
);
// Get file preview
final preview = storage.getFilePreview(bucketId: '[BUCKET_ID]', fileId: '[FILE_ID]', width: 300, height: 300);
// List files
final files = await storage.listFiles(bucketId: '[BUCKET_ID]');
// Delete file
await storage.deleteFile(bucketId: '[BUCKET_ID]', fileId: '[FILE_ID]');
InputFile 工厂方法
// Client-side (Flutter)
InputFile.fromPath(path: '/path/to/file.png', filename: 'file.png') // from path
InputFile.fromBytes(bytes: uint8List, filename: 'file.png') // from Uint8List
// Server-side (Dart)
InputFile.fromPath(path: '/path/to/file.png', filename: 'file.png')
InputFile.fromBytes(bytes: uint8List, filename: 'file.png')
团队
final teams = Teams(client);
// Create team
final team = await teams.create(teamId: ID.unique(), name: 'Engineering');
// List teams
final list = await teams.list();
// Create membership (invite user by email)
final membership = await teams.createMembership(
teamId: '[TEAM_ID]',
roles: ['editor'],
email: '[email protected]',
);
// List memberships
final members = await teams.listMemberships(teamId: '[TEAM_ID]');
// Update membership roles
await teams.updateMembership(teamId: '[TEAM_ID]', membershipId: '[MEMBERSHIP_ID]', roles: ['admin']);
// Delete team
await teams.delete(teamId: '[TEAM_ID]');
基于角色的访问: 设置权限时,使用
Role.team('[TEAM_ID]')表示所有团队成员,或使用Role.team('[TEAM_ID]', 'editor')指定特定团队角色。
实时订阅(客户端)
final realtime = Realtime(client);
// Subscribe to row changes
final subscription = realtime.subscribe([
Channel.tablesdb('[DATABASE_ID]').table('[TABLE_ID]').row(),
]);
subscription.stream.listen((response) {
print(response.events); // e.g. ['tablesdb.*.tables.*.rows.*.create']
print(response.payload); // the affected resource
});
// Subscribe to multiple channels
final multi = realtime.subscribe([
Channel.tablesdb('[DATABASE_ID]').table('[TABLE_ID]').row(),
Channel.bucket('[BUCKET_ID]').file(),
]);
// Cleanup
subscription.close();
可用频道:
| 频道 | 描述 |
|---|---|
account | 已验证用户账户的变更 |
tablesdb.[DB_ID].tables.[TABLE_ID].rows | 表中的所有行 |
tablesdb.[DB_ID].tables.[TABLE_ID].rows.[ROW_ID] | 特定行 |
buckets.[BUCKET_ID].files | 存储桶中的所有文件 |
buckets.[BUCKET_ID].files.[FILE_ID] | 特定文件 |
teams | 用户所属团队的变更 |
teams.[TEAM_ID] | 特定团队 |
memberships | 用户的团队成员关系 |
memberships.[MEMBERSHIP_ID] | 特定成员关系 |
functions.[FUNCTION_ID].executions | 函数执行更新 |
响应字段:events(数组)、payload(资源)、channels(匹配的频道)、timestamp(ISO 8601)。
无服务器函数(服务端)
final functions = Functions(client);
// Execute function
final execution = await functions.createExecution(functionId: '[FUNCTION_ID]', body: '{"key": "value"}');
// List executions
final executions = await functions.listExecutions(functionId: '[FUNCTION_ID]');
编写函数处理程序(Dart 运行时)
// lib/main.dart — Appwrite Function entry point
Future<dynamic> main(final context) async {
// context.req.body — raw body (String)
// context.req.bodyJson — parsed JSON (Map or null)
// context.req.headers — headers (Map)
// context.req.method — HTTP method
// context.req.path — URL path
// context.req.query — query params (Map)
context.log('Processing: ${context.req.method} ${context.req.path}');
if (context.req.method == 'GET') {
return context.res.json({'message': 'Hello from Appwrite Function!'});
}
return context.res.json({'success': true}); // JSON
// return context.res.text('Hello'); // plain text
// return context.res.empty(); // 204
// return context.res.redirect('https://...'); // 302
}
服务端渲染(SSR)身份验证
使用服务端 Dart(Dart Frog、Shelf 等)的 SSR 应用会使用服务端 SDK(dart_appwrite)处理身份验证。你需要两个客户端:
- 管理员客户端 — 使用 API 密钥创建会话,并绕过速率限制(可复用的单例)
- 会话客户端 — 使用会话 Cookie 代表用户执行操作(每个请求创建一个,切勿共享)
import 'package:dart_appwrite/dart_appwrite.dart';
// Admin client (reusable)
final adminClient = Client()
.setEndpoint('https://<REGION>.cloud.appwrite.io/v1')
.setProject('[PROJECT_ID]')
.setKey(Platform.environment['APPWRITE_API_KEY']!);
// Session client (create per-request)
final sessionClient = Client()
.setEndpoint('https://<REGION>.cloud.appwrite.io/v1')
.setProject('[PROJECT_ID]');
final session = request.cookies['a_session_[PROJECT_ID]'];
if (session != null) {
sessionClient.setSession(session);
}
电子邮件/密码登录
final account = Account(adminClient);
final session = await account.createEmailPasswordSession(
email: body['email'],
password: body['password'],
);
// Cookie name must be a_session_<PROJECT_ID>
response.headers.add('Set-Cookie',
'a_session_[PROJECT_ID]=${session.secret}; '
'HttpOnly; Secure; SameSite=Strict; '
'Expires=${HttpDate.format(DateTime.parse(session.expire))}; Path=/');
已验证请求
final session = request.cookies['a_session_[PROJECT_ID]'];
if (session == null) {
return Response(statusCode: 401, body: 'Unauthorized');
}
final sessionClient = Client()
.setEndpoint('https://<REGION>.cloud.appwrite.io/v1')
.setProject('[PROJECT_ID]')
.setSession(session);
final account = Account(sessionClient);
final user = await account.get();
OAuth2 SSR 流程
// Step 1: Redirect to OAuth provider
final account = Account(adminClient);
final redirectUrl = await account.createOAuth2Token(
provider: OAuthProvider.github,
success: 'https://example.com/oauth/success',
failure: 'https://example.com/oauth/failure',
);
return Response(statusCode: 302, headers: {'Location': redirectUrl});
// Step 2: Handle callback — exchange token for session
final account = Account(adminClient);
final session = await account.createSession(
userId: request.uri.queryParameters['userId']!,
secret: request.uri.queryParameters['secret']!,
);
// Set session cookie as above
Cookie 安全: 始终使用
HttpOnly、Secure和SameSite=Strict,以防止 XSS。Cookie 名称必须为a_session_<PROJECT_ID>。
转发用户代理: 调用
sessionClient.setForwardedUserAgent(request.headers['user-agent']),记录最终用户的浏览器信息,以便调试和保障安全。
错误处理
import 'package:appwrite/appwrite.dart';
// AppwriteException is included in the main import
try {
final row = await tablesDB.getRow(databaseId: '[DATABASE_ID]', tableId: '[TABLE_ID]', rowId: '[ROW_ID]');
} on AppwriteException catch (e) {
print(e.message); // human-readable message
print(e.code); // HTTP status code (int)
print(e.type); // error type (e.g. 'document_not_found')
print(e.response); // full response body (Map)
}
常见错误代码:
| 代码 | 含义 |
|---|---|
401 | 未授权 — 缺少或无效的会话/API 密钥 |
403 | 禁止访问 — 权限不足 |
404 | 未找到 — 资源不存在 |
409 | 冲突 — ID 重复或违反唯一性约束 |
429 | 速率受限 — 请求过多 |
权限与角色(重要)
Appwrite 使用权限字符串控制对资源的访问。每项权限将一个操作(read、update、delete、create,或授予 create + update + delete 权限的 write)与一个角色目标配对。默认情况下,除非在行/文件级别明确设置权限,或从表/存储桶设置继承权限,否则没有用户可以访问。权限是由 Permission 和 Role 辅助工具构建的字符串数组。
import 'package:appwrite/appwrite.dart';
// Permission and Role are included in the main package import
带权限的数据库行
final doc = await tablesDB.createRow(
databaseId: '[DATABASE_ID]',
tableId: '[TABLE_ID]',
rowId: ID.unique(),
data: {'title': 'Hello World'},
permissions: [
Permission.read(Role.user('[USER_ID]')), // specific user can read
Permission.update(Role.user('[USER_ID]')), // specific user can update
Permission.read(Role.team('[TEAM_ID]')), // all team members can read
Permission.read(Role.any()), // anyone (including guests) can read
],
);
带权限的文件上传
final file = await storage.createFile(
bucketId: '[BUCKET_ID]',
fileId: ID.unique(),
file: InputFile.fromPath(path: '/path/to/file.png', filename: 'file.png'),
permissions: [
Permission.read(Role.any()),
Permission.update(Role.user('[USER_ID]')),
Permission.delete(Role.user('[USER_ID]')),
],
);
何时设置权限: 当需要对每个资源进行访问控制时,请设置行/文件级权限。如果表中的所有行都使用相同的规则,请在表/存储桶级别配置权限,并将行权限留空。
常见错误:
- 忘记设置权限 — 资源将对所有用户(包括创建者)不可访问
- 对
Role.any()使用write/update/delete— 允许任何用户(包括未通过身份验证的访客)修改或删除资源- 在敏感数据上使用
Permission.read(Role.any())— 使资源可被公开读取