/SKILL.md
Appwrite BaaS. TablesDB/Auth/Storage/Functions/Realtime. Dart/Python/TS. Use for Appwrite SDK, DB, auth, storage, fn, cli. Patterns+rules only.
npx skillsauth add sgaabdu4/appwrite-backend appwrite-backendInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
dart_appwrite, appwrite, node-appwrite, Python appwrite). Raw REST/GraphQL HTTP via fetch, requests, dio, package:http, curl, etc. is a violation unless the official SDK has no supported API and user explicitly approves.1.9.0: use dart_appwrite: 21.1.0 for Dart Functions/server code and Flutter appwrite: 23.0.0 for client apps.ID.unique() for all unique IDs — Row IDs (rowId:), file IDs, user IDs, team IDs, webhook IDs, message IDs, subscriber IDs, and entity IDs in columns. No hardcoded unique IDs, custom generators, names, timestamps, or slugs-as-IDs; they overflow column limits and leak data. Use stable natural keys only as indexed columns.string deprecated; use varchar or text/mediumtext/longtextappwrite generate — Type-safe SDK from schemacreateExecution calls for delete/sync/import/export/migrate/generate flows use async execution, then reconcile source-of-truth state with bounded polling/realtime/fetch. Do not block on backend completion; report destructive failures only after reconciliation proves the entity/account still exists.Use a repo-local ignored .env.appwrite.local to choose the correct CLI line per
project. Do not rely on whatever global Appwrite CLI config was last used.
# .env.appwrite.local (gitignored)
APPWRITE_ENDPOINT=https://<endpoint>/v1
APPWRITE_PROJECT_ID=<project_id>
APPWRITE_API_KEY=standard_...
# Use "cloud" for Appwrite Cloud, or the exact self-hosted server line.
APPWRITE_SERVER_VERSION=cloud
CLI version policy:
appwrite-cli.1.9.0: [email protected].Before Appwrite CLI work:
set -a
[ -f .env.appwrite.local ] && . ./.env.appwrite.local
set +a
case "$APPWRITE_SERVER_VERSION" in
cloud|"")
npm install -g appwrite-cli@latest
;;
1.9.0)
npm install -g [email protected]
;;
*)
echo "Unsupported APPWRITE_SERVER_VERSION=$APPWRITE_SERVER_VERSION; choose a matching CLI before continuing."
exit 1
;;
esac
appwrite --version
appwrite client \
--endpoint "$APPWRITE_ENDPOINT" \
--project-id "$APPWRITE_PROJECT_ID" \
--key "$APPWRITE_API_KEY"
appwrite client --debug
appwrite client --debug must show the expected endpoint/project and a masked
key before proceeding. If .env.appwrite.local is missing, ask for this repo's
Appwrite endpoint, project ID, API key, and server version.
Rules: appwrite.config.json = local project config. appwrite client ... =
global override (non-interactive). Clear override: appwrite client --reset.
The 17.4.0 CLI does not support newer helper flags such as --limit,
--sort-desc, or --filter; use raw --queries where needed, or parse the
plain table output for quick status checks. The latest Cloud CLI can use newer
helper flags.
Details: appwrite-cli
| Old | New | |-----|-----| | Collections | Tables | | Documents | Rows | | Attributes | Columns | | Databases | TablesDB |
Package policy:
1.9.0: Dart Functions/server dart_appwrite: 21.1.0; Flutter app appwrite: 23.0.0.import 'package:dart_appwrite/dart_appwrite.dart';
final client = Client()
.setEndpoint('https://cloud.appwrite.io/v1')
.setProject('<PROJECT_ID>')
.setKey('<API_KEY>');
final tablesDB = TablesDB(client);
from appwrite.client import Client
from appwrite.services.tables_db import TablesDB
client = Client()
client.set_endpoint('https://cloud.appwrite.io/v1')
client.set_project('<PROJECT_ID>')
client.set_key('<API_KEY>')
tables_db = TablesDB(client)
import { Client, TablesDB } from 'node-appwrite';
const client = new Client()
.setEndpoint('https://cloud.appwrite.io/v1')
.setProject('<PROJECT_ID>')
.setKey('<API_KEY>');
const tablesDB = new TablesDB(client);
// Create
await tablesDB.createRow(databaseId: 'db', tableId: 'users', rowId: ID.unique(),
data: {'name': 'Alice'});
// Read
final rows = await tablesDB.listRows(databaseId: 'db', tableId: 'users',
queries: [Query.equal('status', 'active'), Query.select(['name', 'email'])]);
// Update
await tablesDB.updateRow(databaseId: 'db', tableId: 'users', rowId: 'user_123',
data: {'status': 'inactive'});
// Upsert
await tablesDB.upsertRow(databaseId: 'db', tableId: 'settings', rowId: 'prefs',
data: {'theme': 'dark'});
// Delete
await tablesDB.deleteRow(databaseId: 'db', tableId: 'users', rowId: 'user_123');
Bulk: bulk-operations.md | Chunked ID queries: chunked-queries.md
Comparison: equal | notEqual | lessThan | lessThanEqual | greaterThan | greaterThanEqual | between | notBetween
String: startsWith | endsWith | contains | search (+ not variants)
Null: isNull | isNotNull · Logical: and([...]) | or([...])
Pagination: select | limit | cursorAfter | cursorBefore | orderAsc | orderDesc | orderRandom
Timestamp: createdAfter | createdBefore | updatedAfter | updatedBefore
Spatial: distanceEqual | distanceLessThan | distanceGreaterThan | intersects | overlaps | touches | crosses (+ not variants)
All prefixed Query.. Details: query-optimization.md
data: {
'likes': Operator.increment(1),
'tags': Operator.arrayAppend(['trending']),
'updatedAt': Operator.dateSetNow(),
}
Numeric: increment | decrement | multiply | divide
Array: arrayAppend | arrayPrepend | arrayRemove | arrayUnique | arrayIntersect | arrayDiff
Other: toggle | stringConcat | stringReplace | dateAddDays | dateSetNow
Details: atomic-operators.md
| Type | Max Chars | Indexing | Use |
|------|-----------|----------|-----|
| varchar | 16,383 | Full (if size < 768) | Queryable short strings |
| text | 16,383 | Prefix only | Descriptions, notes |
| mediumtext | 4,194,303 | Prefix only | Articles |
| longtext | 1,073,741,823 | Prefix only | Large documents |
stringdeprecated. Usevarcharfor queryable,textfor non-indexed.
Other: integer | float | boolean | datetime | email | url | ip | enum | relationship | point | line | polygon
Details: schema-management.md
| Rule | Impact |
|------|--------|
| Cursor pagination | 10-100x faster than offset |
| Pagination mixin (Dart) | ~50 lines saved per datasource |
| Query.select() | 12-18x faster for relationships |
| total: false | Eliminates COUNT scan |
| Indexes | 100x faster on large tables |
| Operators | No race conditions |
| Bulk operations | N → 1 request |
| Delta sync | Fetches only changed rows |
Details: performance.md, pagination-performance.md
appwrite generate
Gen typed helpers from schema into generated/appwrite/. Autocomplete + compile checks. Regen after schema change.
CLI flow: login -> init project -> pull -> generate -> push. Details: appwrite-cli
Email/password, OAuth (50+ providers), phone, magic link, anon, email OTP, custom token. MFA: TOTP/email/phone/recovery. SSR sessions. JWT for functions.
SSR cookie: a_session_<PROJECT_ID>. Admin client creates session. Per-request session client reads user context.
Email policies can block free, aliased, or disposable emails at signup/update.
Details: authentication.md | auth-methods.md
Upload/download/preview w/ transforms (resize, format conversion). File tokens for shareable URLs. HEIC, AVIF, WebP supported. SDKs handle chunking/parallel chunk uploads; do not hand-roll upload HTTP.
Details: storage-files.md
final sub = realtime.subscribe(['databases.db.tables.posts.rows']);
sub.stream.listen((e) => print(e.events));
Channels: account | databases.<DB>.tables.<TABLE>.rows | buckets.<BUCKET>.files | presences
Channel helpers (preferred): Channel class for type-safe subs w/ IDE autocomplete:
import { Client, Realtime, Channel, Query } from "appwrite";
const sub = await realtime.subscribe(
Channel.tablesdb('<DB>').table('<TABLE>').row(),
response => console.log(response.payload),
[Query.equal('status', ['active'])] // server-side filtering
);
Use Presences API for online/typing/active state when supported; avoid durable DB rows + cleanup cron for ephemeral status.
Details: realtime.md
Init SDK outside handler. Group by domain. Event triggers, not polling.
Dart Functions on self-hosted Appwrite 1.9.0: pin dart_appwrite: 21.1.0. On Appwrite Cloud, use latest stable SDK and runtime.
Details: functions.md | functions-advanced.md
final tx = await tablesDB.createTransaction(ttl: 300);
await tablesDB.createRow(..., transactionId: tx.$id);
await tablesDB.updateTransaction(transactionId: tx.$id, commit: true);
Details: transactions.md
await tablesDB.listRows(databaseId: 'db', tableId: 'posts',
queries: [Query.equal('author.country', 'US'), Query.select(['title', 'author.name'])]);
Types: oneToOne | oneToMany | manyToOne | manyToMany
Details: relationships.md
permissions: [
Permission.read(Role.any()),
Permission.update(Role.user(userId)),
Permission.delete(Role.team('admin')),
Permission.create(Role.label('premium')),
]
Default: deny all unless row/file perms set or inherited from table/bucket.
Use row/file perms for per-resource ACL. If all resources share rules, set table/bucket perms, leave row/file perms empty.
write = create + update + delete
Avoid: missing perms = lockout; Role.any() + write/update/delete = public mutation; Permission.read(Role.any()) on sensitive data = public leak.
Roles: any() | guests() | users() | user(id) | team(id) | team(id, role) | label(name)
Details: permissions | teams | storage-files
Default page: 25 · Bulk: 1000 rows · Query.equal(): 100 values · Nesting: 3 levels · Queries/req: 100 · Timeout: 15s
400 Bad request · 401 Unauthorized · 403 Forbidden · 404 Not found · 409 Conflict · 429 Rate limited (client SDKs only)
Catch AppwriteException. 429 -> exponential backoff.
Details: error-handling.md
| Wrong | Right | Why |
|-------|-------|-----|
| N+1 queries | Query.select(['col', 'relation.col']) | Kills extra round-trips |
| Read-modify-write | Operator.increment() | Race condition |
| Large offsets | Query.cursorAfter(id) | O(n) vs O(1) |
| Skip totals | total: false | Kills COUNT scan |
| Missing indexes | Create for queried columns | Queries scan entire table |
| SDK init inside handler | Init outside for warm reuse | Repeated setup each call |
| Hardcoded secrets | Env vars | Security risk |
| Polling | Realtime or event triggers | Wasted executions |
| Client-side filtering | Realtime queries | Server does work |
| Raw channel strings | Channel helpers | Typos, no autocomplete |
| ColumnString | ColumnVarchar or ColumnText | string deprecated |
| Hand-writing types | appwrite generate | Schema drift, no autocomplete |
| databases.listDocuments() | tablesDB.listRows() | Deprecated API |
| Raw Appwrite HTTP (fetch, requests, dio, package:http, curl) | Official SDK package | Version drift, auth mistakes, lost typed APIs |
| Custom/hardcoded unique IDs | ID.unique() | Overflow risk, info leakage, collisions |
| Full re-fetch every sync | Query.updatedAfter() + per-table timestamps | Wastes bandwidth, slow |
| Loop w/ createRow() | createRows() bulk | N requests vs 1 |
Query.select() — cuts bandwidthtotal: false — fastest queriesDetails: cost-optimization.md
Data: schema-management · query-optimization · atomic-operators · relationships · transactions · bulk-operations · chunked-queries Performance: performance · pagination-performance · cost-optimization Auth: authentication · auth-methods · permissions · teams Services: storage-files · functions · functions-advanced · realtime · messaging · webhooks · avatars · graphql · locale Tooling: appwrite-cli Platform: error-handling · limits · health · self-hosting · self-hosting-ops
Docs: https://appwrite.io/docs · API: https://appwrite.io/docs/references · SDKs: https://github.com/appwrite
tools
Use when work should span one or more detached tasks but still behave like one job with a single owner context. TaskFlow is the durable flow substrate under authoring layers like Lobster, ACPX, plugins, or plain code. Keep conditional logic in the caller; use TaskFlow for flow identity, child-task linkage, waiting state, revision-checked mutations, and user-facing emergence.
tools
# Lobster Lobster executes multi-step workflows with approval checkpoints. Use it when: - User wants a repeatable automation (triage, monitor, sync) - Actions need human approval before executing (send, post, delete) - Multiple tool calls should run as one deterministic operation ## When to use Lobster | User intent | Use Lobster? | | ------------------------------------------------------ | --------------------------
tools
# Lobster Lobster executes multi-step workflows with approval checkpoints. Use it when: - User wants a repeatable automation (triage, monitor, sync) - Actions need human approval before executing (send, post, delete) - Multiple tool calls should run as one deterministic operation ## When to use Lobster | User intent | Use Lobster? | | ------------------------------------------------------ | --------------------------
tools
A CLI tool for making authenticated requests to the X (Twitter) API. Use this skill when you need to post tweets, reply, quote, search, read posts, manage followers, send DMs, upload media, or interact with any X API v2 endpoint.