Appwrite backend
Appwrite BaaS skill for AI agents. Covers TablesDB, Auth, Storage, Functions, Messaging, and Realtime in Dart, Python, and TypeScript.
npx -y skills add sgaabdu4/appwrite-backend --skill appwrite-backendAssembled from the repository path, not quoted from the project. Check it against their README if it does not work.
One thing to look at
- 2 stars2 stars. Stars are a popularity signal and not a quality one, but at this level it is likely that nobody has read this closely except its author, and you would be relying on your own review.
What its author says it does
Copied from the file, not written here
Appwrite backend development and operations. Use for Appwrite SDK work; any Appwrite CLI command or failure must route through the CLI safety branch.
The file declares its own license as MIT. That is the author’s claim about this one file, and it is not the same thing as the license GitHub reports for the repository, which is listed with the other numbers below.
SKILL.md
16.2 KB, as published. Nobody here has run it
Appwrite Development
Route
| Trigger | Load before action |
|---|---|
| Any Appwrite CLI/wrapper command, deployment, schema sync, function-variable operation, or CLI failure | appwrite-cli.md |
| Production schema/data/ACL/function cutover | production-migrations.md + CLI reference when CLI participates |
| TablesDB transaction or cross-service consistency | transactions.md + permissions.md |
| Self-hosted backup, restore, or data-loss incident | self-hosting-ops.md |
Critical Rules
- Use official SDK packages only — Dart/Flutter/TypeScript/Python must use sdk-routing. Raw REST/GraphQL HTTP via
fetch,requests,dio,package:http,curl, etc. is a violation unless the SDK lacks the endpoint or an isolated, testedClient.callworks around SDK model parsing. - Pin SDKs by target — Cloud: latest stable SDK. Self-hosted
1.9.x:dart_appwrite25.1.0, Flutterappwrite25.2.0,node-appwrite26.2.0, webappwrite26.1.0, Pythonappwrite21.0.0, CLI 22.4.0. - Use TablesDB API — Collections API deprecated 1.8.0
- Allocate Appwrite IDs once with
ID.unique()— Appwrite resource IDs and surrogate entity IDs use the official SDK helper. Retryable create: callID.unique()before the first attempt → persist the returned ID in the durable draft/intent → reuse that exact ID for every retry/reconciliation. CallingID.unique()again on retry creates a second resource and breaks idempotency. Stable business/natural identity remains in indexed columns; never derive resource IDs from names, timestamps, slugs, hashes, or custom generators. - Use Query.select() — Relationships return IDs only without explicit selection.
- Use cursor pagination — Offset degrades on large tables
- Use Operator for counters — Avoids race conditions
- Create indexes — Queries without scan entire tables
- Init outside handler — SDK/connections persist between warm invocations
- Group functions by domain — One per domain, not per op
- Event triggers over polling — One trigger replaces thousands of requests
- Use explicit string types —
stringdeprecated; usevarcharortext/mediumtext/longtext - Use
appwrite generate— Type-safe SDK from schema - Use Channel helpers — Type-safe realtime subs, not raw strings
- Use Realtime queries — Server-side event filtering, not client-side
- Async-start long-running Functions — Client
createExecutioncalls 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. - Guard schema pushes —
appwrite push tablesreconciles remote TablesDB resources against the complete local manifest; omission means deletion. Production push requires appwrite-cli inventory + manifest guard PASS.push all,--all, or--forcenever substitutes for this gate. - Stage production migrations — Additive expand → type-aware resumable backfill → compatible deployment → contract/read-back → consumer activation. Partial data/schema never activates downstream code. Use production-migrations.
CLI Quick Check (Top)
Any CLI/wrapper intent or failure → load appwrite-cli.md before installing, binding, probing, diagnosing, or mutating. Repository-pinned wrapper/version wins over this skill's generic pin. Never infer a command shape from another version.
Terminology (1.8.0+)
| Old | New |
|---|---|
| Collections | Tables |
| Documents | Rows |
| Attributes | Columns |
| Databases | TablesDB |
Setup
Package policy:
- Cloud: latest stable official SDK.
- Self-hosted
1.9.x: use Critical Rule 2 pins. - TypeScript/React browser:
appwrite; TypeScript server/SSR/Functions:node-appwrite. - Python:
appwrite; prefer keyword arguments for SDK calls. - Dart:
appwritefor Flutter/client apps,dart_appwritefor server/Functions; prefer named parameters.
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);
TablesDB CRUD
// 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');
Use SDK idioms:
- TypeScript uses object parameters:
tablesDB.createRow({ databaseId, tableId, rowId, data }). - Python uses keyword arguments:
tables_db.create_row(database_id='db', table_id='users', row_id=ID.unique(), data={...}). - Dart uses named parameters as shown above.
Bulk: bulk-operations.md | Chunked ID queries: chunked-queries.md
Query Reference
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
Operators (Atomic Updates)
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
Column Types
| 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
Performance
| 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
Type-Safe SDK Generation
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
Authentication
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
Storage
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
Realtime
final sub = realtime.subscribe(['tablesdb.db.tables.posts.rows']);
sub.stream.listen((e) => print(e.events));
Channels: account | tablesdb.<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
Functions
Init SDK outside handler. Group by domain. Event triggers, not polling. Functions: self-hosted uses Rule 2 Dart pin; Cloud uses latest SDK/runtime.
Details: functions.md | functions-advanced.md
Transactions
final tx = await tablesDB.createTransaction(ttl: 300);
await tablesDB.createRow(..., transactionId: tx.$id);
await tablesDB.updateTransaction(transactionId: tx.$id, commit: true);
Details: transactions.md
Relationships
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
permissions: [
Permission.read(Role.any()),
Permission.update(Role.user(userId)),
Permission.delete(Role.team('admin')),
Permission.create(Role.label('premium')),
]
Default: Server SDK/Console create = empty resource ACL; Client SDK create = creator read/update/delete. Pass explicit permissions whenever ACL correctness matters.
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
Limits
Default page: 25 · Bulk: 1000 rows · Query.equal(): 100 values · Nesting: 3 levels · Queries/req: 100 · Timeout: 15s
Error Codes
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
Anti-Patterns
| 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 |
Derived/custom resource ID or fresh ID.unique() per retry | Preallocate one ID.unique(), persist, reuse | Leakage/collision or duplicate resource |
| Full re-fetch every sync | Query.updatedAfter() + per-table timestamps | Wastes bandwidth, slow |
Loop w/ createRow() | createRows() bulk | N requests vs 1 |
Cost Optimization
Query.select()— cuts bandwidth- Cursor pagination +
total: false— fastest queries - Realtime over polling — one connection vs repeated calls
- Batch ops — 1 execution vs N
- WebP quality 80 — smallest files, universal support
- Init outside handler — fewer cold starts
- Budget cap — Organization → Billing → Budget cap
Details: cost-optimization.md
Reference Files
Data: schema-management · production-migrations · 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: sdk-routing · appwrite-cli Platform: error-handling · limits · health · self-hosting · self-hosting-ops
Resources
Docs: https://appwrite.io/docs · API: https://appwrite.io/docs/references · SDKs: https://github.com/appwrite