appwrite-dart — Claude Code skill for the Appwrite Dart SDK
Gives your agent the Appwrite Dart SDK reference: auth, database queries, file uploads, realtime and server-side admin.
npx skills add appwrite/skills --skill appwrite-dart -a claude-codeWhat appwrite-dart does
The skill covers both Appwrite packages for Dart. Flutter apps (mobile, web, desktop) pull in appwrite; server-side code uses dart_appwrite. The client is configured with setEndpoint and setProject, and on the server you add setKey with an API key read from an environment variable.
Inside are working examples: email signup and login, OAuth through createOAuth2Session, user management, table operations via TablesDB, file uploads with native file handling, realtime subscriptions, plus storage and functions administration. A section on string column types explains the trade-off: varchar lives inline in the row and can be fully indexed, while text, mediumtext and longtext are stored off-page with a prefix index only. For short indexed fields like names or slugs it points you at varchar.
One compatibility note matters. The Databases class is marked deprecated and new code should use TablesDB; the skill allows keeping Databases if the existing codebase already depends on it. Server-side calls need an API key, so admin methods won't work without one.
What it helps with
- wire up email and OAuth login in a Flutter app
- pick the right string column type for an indexed field
- migrate code off the deprecated
Databasesclass - configure a server-side client with an API key
- subscribe to live data changes
Example prompts for the agent
- Connect Appwrite to my Flutter app with email and Google sign-in
- Create an articles table with title, summary and body columns and pick the types
- Rewrite this code to use TablesDB instead of Databases
How to install appwrite-dart
1Skills CLI (Vercel)
npx skills add appwrite/skills --skill appwrite-dart -a claude-code2Manually: put the files in place
~/.claude/skills/appwrite-dart/SKILL.mdCopy the whole skill folder; for one project use .claude/skills/appwrite-dart/
1Skills CLI (Vercel)
npx skills add appwrite/skills --skill appwrite-dart -a codex2Manually: put the files in place
.agents/skills/appwrite-dart/SKILL.mdFor all projects use ~/.agents/skills/
1Skills CLI (Vercel)
npx skills add appwrite/skills --skill appwrite-dart -a cursor2Manually: put the files in place
.cursor/skills/appwrite-dart/SKILL.mdCursor also reads .agents/skills/ and .claude/skills/
1GitHub CLI
gh skill install appwrite/skills appwrite-dart2Manually: put the files in place
.github/skills/appwrite-dart/SKILL.md1Skills CLI (Vercel)
npx skills add appwrite/skills --skill appwrite-dart -a gemini-cli2Platform CLI
gemini skills install https://github.com/appwrite/skills --path skills/appwrite-dartManually: put the files in place
.devin/skills/appwrite-dart/SKILL.mdThe legacy .windsurf/skills/ path still works
Skills CLI (Vercel)
npx skills add appwrite/skills --skill appwrite-dart -a opencodeClaude.ai
Zip the folder and upload it in Settings → Capabilities → Skills
Step by step, with commands for each platform: How to install skills in Claude Code: five ways
Questions about appwrite-dart
How do I install appwrite-dart in Claude?
Run npx skills add appwrite/skills --skill appwrite-dart -a claude-code. The skill file lands at ~/.claude/skills/appwrite-dart/SKILL.md.
What does appwrite-dart do?
It hands the agent an Appwrite Dart SDK reference: client and server calls, database queries, file handling and realtime subscriptions.
Which class should I use for database work?
Use TablesDB for new code. Databases is deprecated and only worth keeping if your existing codebase already relies on it.
Source text
SKILL.md · BSD-3-Clause
---
name: appwrite-dart
description: Appwrite Dart SDK skill. Use when building Flutter apps (mobile, web, desktop) or server-side Dart applications with Appwrite. Covers client-side auth (email, OAuth), database queries, file uploads with native file handling, real-time subscriptions, and server-side admin via API keys for user management, database administration, storage, and functions.
---
# Appwrite Dart SDK
## Installation
```bash
# Flutter (client-side)
flutter pub add appwrite
# Dart (server-side)
dart pub add dart_appwrite
```
## Setting Up the Client
### Client-side (Flutter)
```dart
import 'package:appwrite/appwrite.dart';
final client = Client()
.setEndpoint('https://<REGION>.cloud.appwrite.io/v1')
.setProject('[PROJECT_ID]');
```
### Server-side (Dart)
```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']!);
```
## Code Examples
### Authentication (client-side)
```dart
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');
```
### User Management (server-side)
```dart
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]');
```
### Database Operations
> **Note:** Use `TablesDB` (not the deprecated `Databases` class) for all new code. Only use `Databases` if the existing codebase already relies on it or the user explicitly requests it.
>
> **Tip:** Prefer named parameters (e.g., `databaseId: '...'`) for all SDK method calls. Only use positional arguments if the existing codebase already uses them or the user explicitly requests it.
```dart
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 Column Types
> **Note:** The legacy `string` type is deprecated. Use explicit column types for all new columns.
| Type | Max characters | Indexing | Storage |
|------|---------------|----------|---------|
| `varchar` | 16,383 | Full index (if size ≤ 768) | Inline in row |
| `text` | 16,383 | Prefix only | Off-page |
| `mediumtext` | 4,194,303 | Prefix only | Off-page |
| `longtext` | 1,073,741,823 | Prefix only | Off-page |
- `varchar` is stored inline and counts towards the 64 KB row size limit. Prefer for short, indexed fields like names, slugs, or identifiers.
- `text`, `mediumtext`, and `longtext` are stored off-page (only a 20-byte pointer lives in the row), so they don't consume the row size budget. `size` is not required for these types.
```dart
// 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
],
);
```
### Query Methods
```dart
// 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,
);
```
### File Storage
```dart
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 Factory Methods
```dart
// 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')
```
### Teams
```dart
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-based access:** Use `Role.team('[TEAM_ID]')` for all team members or `Role.team('[TEAM_ID]', 'editor')` for a specific team role when setting permissions.
### Real-time Subscriptions (client-side)
```dart
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();
```
**Available channels:**
| Channel | Description |
|---------|-------------|
| `account` | Changes to the authenticated user's account |
| `tablesdb.[DB_ID].tables.[TABLE_ID].rows` | All rows in a table |
| `tablesdb.[DB_ID].tables.[TABLE_ID].rows.[ROW_ID]` | A specific row |
| `buckets.[BUCKET_ID].files` | All files in a bucket |
| `buckets.[BUCKET_ID].files.[FILE_ID]` | A specific file |
| `teams` | Changes to teams the user belongs to |
| `teams.[TEAM_ID]` | A specific team |
| `memberships` | The user's team memberships |
| `memberships.[MEMBERSHIP_ID]` | A specific membership |
| `functions.[FUNCTION_ID].executions` | Function execution updates |
Response fields: `events` (array), `payload` (resource), `channels` (matched), `timestamp` (ISO 8601).
### Serverless Functions (server-side)
```dart
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]');
```
#### Writing a Function Handler (Dart runtime)
```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
}
```
### Server-Side Rendering (SSR) Authentication
SSR apps using server-side Dart (Dart Frog, Shelf, etc.) use the **server SDK** (`dart_appwrite`) to handle auth. You need two clients:
- **Admin client** — uses an API key, creates sessions, bypasses rate limits (reusable singleton)
- **Session client** — uses a session cookie, acts on behalf of a user (create per-request, never share)
```dart
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);
}
```
#### Email/Password Login
```dart
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=/');
```
#### Authenticated Requests
```dart
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 Flow
```dart
// 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 security:** Always use `HttpOnly`, `Secure`, and `SameSite=Strict` to prevent XSS. The cookie name must be `a_session_<PROJECT_ID>`.
> **Forwarding user agent:** Call `sessionClient.setForwardedUserAgent(request.headers['user-agent'])` to record the end-user's browser info for debugging and security.
## Error Handling
```dart
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)
}
```
**Common error codes:**
| Code | Meaning |
|------|---------|
| `401` | Unauthorized — missing or invalid session/API key |
| `403` | Forbidden — insufficient permissions |
| `404` | Not found — resource does not exist |
| `409` | Conflict — duplicate ID or unique constraint |
| `429` | Rate limited — too many requests |
## Permissions & Roles (Critical)
Appwrite uses permission strings to control access to resources. Each permission pairs an action (`read`, `update`, `delete`, `create`, or `write` which grants create + update + delete) with a role target. By default, **no user has access** unless permissions are explicitly set at the row/file level or inherited from the table/bucket settings. Permissions are arrays of strings built with the `Permission` and `Role` helpers.
```dart
import 'package:appwrite/appwrite.dart';
// Permission and Role are included in the main package import
```
### Database Row with Permissions
```dart
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
],
);
```
### File Upload with Permissions
```dart
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]')),
],
);
```
> **When to set permissions:** Set row/file-level permissions when you need per-resource access control. If all rows in a table share the same rules, configure permissions at the table/bucket level and leave row permissions empty.
> **Common mistakes:**
> - **Forgetting permissions** — the resource becomes inaccessible to all users (including the creator)
> - **`Role.any()` with `write`/`update`/`delete`** — allows any user, including unauthenticated guests, to modify or remove the resource
> - **`Permission.read(Role.any())` on sensitive data** — makes the resource publicly readable
© appwrite · BSD-3-Clause · appwrite/skills