From 29f3a582fd50162c9063a94f39b4ac881d9f1799 Mon Sep 17 00:00:00 2001 From: root Date: Fri, 20 Feb 2026 12:27:21 +0000 Subject: [PATCH 1/3] regenerate --- .cursor-plugin/plugin.json | 11 + .mcp.json | 17 + CHANGELOG.md | 1 + LICENSE | 25 +- README.md | 84 +++- commands/deploy-function.md | 153 +++++++ commands/deploy-site.md | 138 ++++++ skills/appwrite-dart/SKILL.md | 492 ++++++++++++++++++++++ skills/appwrite-dotnet/SKILL.md | 405 ++++++++++++++++++ skills/appwrite-go/SKILL.md | 468 +++++++++++++++++++++ skills/appwrite-kotlin/SKILL.md | 542 ++++++++++++++++++++++++ skills/appwrite-php/SKILL.md | 386 +++++++++++++++++ skills/appwrite-python/SKILL.md | 409 ++++++++++++++++++ skills/appwrite-ruby/SKILL.md | 422 +++++++++++++++++++ skills/appwrite-swift/SKILL.md | 479 +++++++++++++++++++++ skills/appwrite-typescript/SKILL.md | 630 ++++++++++++++++++++++++++++ 16 files changed, 4644 insertions(+), 18 deletions(-) create mode 100644 .cursor-plugin/plugin.json create mode 100644 .mcp.json create mode 100644 CHANGELOG.md create mode 100644 commands/deploy-function.md create mode 100644 commands/deploy-site.md create mode 100644 skills/appwrite-dart/SKILL.md create mode 100644 skills/appwrite-dotnet/SKILL.md create mode 100644 skills/appwrite-go/SKILL.md create mode 100644 skills/appwrite-kotlin/SKILL.md create mode 100644 skills/appwrite-php/SKILL.md create mode 100644 skills/appwrite-python/SKILL.md create mode 100644 skills/appwrite-ruby/SKILL.md create mode 100644 skills/appwrite-swift/SKILL.md create mode 100644 skills/appwrite-typescript/SKILL.md diff --git a/.cursor-plugin/plugin.json b/.cursor-plugin/plugin.json new file mode 100644 index 0000000..1a3c579 --- /dev/null +++ b/.cursor-plugin/plugin.json @@ -0,0 +1,11 @@ +{ + "name": "appwrite-plugin", + "version": "0.1.0", + "description": "Appwrite is an open-source backend as a service server that abstracts and simplifies complex and repetitive development tasks behind a very simple to use REST API. Appwrite aims to help you develop your apps faster and in a more secure way. Use the CursorPlugin SDK to integrate your app with the Appwrite server to easily start interacting with all of Appwrite backend APIs and tools. For full API documentation and tutorials go to [https://appwrite.io/docs](https://appwrite.io/docs)", + "author": { + "name": "Appwrite", + "email": "team@appwrite.io" + }, + "repository": "https://github.com/appwrite/cursor-plugin", + "logo": "https://github.com/appwrite/appwrite/raw/main/public/images/github.png" +} diff --git a/.mcp.json b/.mcp.json new file mode 100644 index 0000000..9342308 --- /dev/null +++ b/.mcp.json @@ -0,0 +1,17 @@ +{ + "mcpServers": { + "appwrite-api": { + "command": "uvx", + "args": ["mcp-server-appwrite", "--users"], + "env": { + "APPWRITE_API_KEY": "your-api-key", + "APPWRITE_PROJECT_ID": "your-project-id", + "APPWRITE_ENDPOINT": "https://.cloud.appwrite.io/v1" + } + }, + "appwrite-docs": { + "command": "npx", + "args": ["mcp-remote", "https://mcp-for-docs.appwrite.io"] + } + } +} \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..420e6f2 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1 @@ +# Change Log diff --git a/LICENSE b/LICENSE index bc7497a..6b6252a 100644 --- a/LICENSE +++ b/LICENSE @@ -1,21 +1,12 @@ -MIT License +Copyright (c) 2026 Appwrite (https://appwrite.io) and individual contributors. +All rights reserved. -Copyright (c) 2026 Appwrite +Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: + 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. + 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. + 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/README.md b/README.md index 5140665..96b0aed 100644 --- a/README.md +++ b/README.md @@ -1 +1,83 @@ -# cursor-plugin \ No newline at end of file +# Appwrite CursorPlugin + +![License](https://img.shields.io/github/license/appwrite/cursor-plugin.svg?style=flat-square) +![Version](https://img.shields.io/badge/api%20version-1.8.1-blue.svg?style=flat-square) +[![Twitter Account](https://img.shields.io/twitter/follow/appwrite?color=00acee&label=twitter&style=flat-square)](https://twitter.com/appwrite) +[![Discord](https://img.shields.io/discord/564160730845151244?label=discord&style=flat-square)](https://appwrite.io/discord) + +**This SDK is compatible with Appwrite server version 1.8.x. For older versions, please check [previous releases](https://github.com/appwrite/cursor-plugin.git/releases).** + +Appwrite is an open-source backend as a service server that abstracts and simplifies complex and repetitive development tasks behind a very simple to use REST API. Appwrite aims to help you develop your apps faster and in a more secure way. Use the CursorPlugin SDK to integrate your app with the Appwrite server to easily start interacting with all of Appwrite backend APIs and tools. For full API documentation and tutorials go to [https://appwrite.io/docs](https://appwrite.io/docs) + +## Installation + +Install this plugin in Cursor by adding the plugin directory to your project or Cursor plugins folder. + +## Structure + +``` +.cursor-plugin/ +└── plugin.json # Plugin manifest +skills/ # Agent skills (per language) +├── appwrite-typescript/ +│ └── SKILL.md +├── appwrite-dart/ +│ └── SKILL.md +├── appwrite-kotlin/ +│ └── SKILL.md +├── appwrite-swift/ +│ └── SKILL.md +├── appwrite-php/ +│ └── SKILL.md +├── appwrite-python/ +│ └── SKILL.md +├── appwrite-ruby/ +│ └── SKILL.md +├── appwrite-go/ +│ └── SKILL.md +├── appwrite-dotnet/ +│ └── SKILL.md +commands/ # Agent-executable commands +├── deploy-site.md +└── deploy-function.md +.mcp.json # MCP server definitions +``` + +## Skills + +This plugin includes Appwrite SDK skills for the following languages: + +- **Typescript** — `skills/appwrite-typescript/SKILL.md` +- **Dart** — `skills/appwrite-dart/SKILL.md` +- **Kotlin** — `skills/appwrite-kotlin/SKILL.md` +- **Swift** — `skills/appwrite-swift/SKILL.md` +- **Php** — `skills/appwrite-php/SKILL.md` +- **Python** — `skills/appwrite-python/SKILL.md` +- **Ruby** — `skills/appwrite-ruby/SKILL.md` +- **Go** — `skills/appwrite-go/SKILL.md` +- **Dotnet** — `skills/appwrite-dotnet/SKILL.md` + +Each skill provides comprehensive SDK usage examples including authentication, database operations, file storage, real-time subscriptions, and more. + +## MCP Servers + +This plugin comes with two MCP (Model Context Protocol) servers pre-configured in `.mcp.json`: + +- **Appwrite API MCP** — Lets AI agents access the Appwrite API to manage users, databases, storage, and more. +- **Appwrite Docs MCP** — Access Appwrite documentation inline for quick reference while coding. + +## Commands + +Agent-executable commands for deploying directly from your editor: + +- **deploy-site** — Deploy an Appwrite site using the CLI. Covers init, push, CI/CD non-interactive mode, site management, variables, and logs. +- **deploy-function** — Deploy an Appwrite function using the CLI. Covers init, push, CI/CD non-interactive mode, function management, executions, variables, and local development. + + +## Contribution + +This library is auto-generated by Appwrite custom [SDK Generator](https://github.com/appwrite/sdk-generator). To learn more about how you can help us improve this SDK, please check the [contribution guide](https://github.com/appwrite/sdk-generator/blob/master/CONTRIBUTING.md) before sending a pull-request. + +## License + +Please see the [BSD-3-Clause license](https://raw.githubusercontent.com/appwrite/appwrite/master/LICENSE) file for more information. diff --git a/commands/deploy-function.md b/commands/deploy-function.md new file mode 100644 index 0000000..3cd7411 --- /dev/null +++ b/commands/deploy-function.md @@ -0,0 +1,153 @@ +--- +name: deploy-function +description: Deploy a function using the Appwrite CLI +--- + +# Deploy a function using the Appwrite CLI + +## Prerequisites + +- Appwrite CLI installed (`npm install -g appwrite-cli`) +- Authenticated with `appwrite login` +- Project initialized with `appwrite init` + +## Quick Deploy + +```sh +appwrite push functions +``` + +## Full Workflow + +### 1. Initialize a new function + +```sh +appwrite init functions +``` + +This scaffolds a new function with a starter template and adds it to `appwrite.config.json`. + +### 2. Pull existing functions from Console + +```sh +appwrite pull functions +``` + +### 3. Configuration + +Functions are configured in `appwrite.config.json`: + +```json +{ + "projectId": "", + "endpoint": "https://.cloud.appwrite.io/v1", + "functions": [ + { + "$id": "", + "name": "userAuth", + "enabled": true, + "live": true, + "logging": true, + "runtime": "node-18.0", + "deployment": "", + "vars": [], + "events": [], + "schedule": "", + "timeout": 15, + "entrypoint": "userAuth.js", + "commands": "npm install", + "version": "v3", + "path": "functions/userAuth" + } + ] +} +``` + +### 4. Deploy + +```sh +appwrite push functions +``` + +## CI/CD (Non-Interactive) + +Set up headless mode first: + +```sh +appwrite client \ + --endpoint https://.cloud.appwrite.io/v1 \ + --project-id \ + --key +``` + +Then deploy non-interactively: + +```sh +# Push all functions +appwrite push functions --all --force + +# Push a specific function +appwrite push functions --function-id --force + +# Push all resources at once (functions, tables, buckets, teams, topics) +appwrite push all --all --force +``` + +## Function Management Commands + +| Command | Description | +|---------|-------------| +| `appwrite functions list` | List all functions in the project | +| `appwrite functions create` | Create a new function | +| `appwrite functions get --function-id ` | Get a function by ID | +| `appwrite functions update --function-id ` | Update a function | +| `appwrite functions delete --function-id ` | Delete a function | +| `appwrite functions list-runtimes` | List all active runtimes | + +## Deployment Commands + +| Command | Description | +|---------|-------------| +| `appwrite functions list-deployments --function-id ` | List all deployments | +| `appwrite functions create-deployment --function-id ` | Upload a new deployment | +| `appwrite functions get-deployment --function-id --deployment-id ` | Get a deployment | +| `appwrite functions update-deployment --function-id --deployment-id ` | Set active deployment | +| `appwrite functions delete-deployment --function-id --deployment-id ` | Delete a deployment | +| `appwrite functions download-deployment --function-id --deployment-id ` | Download deployment contents | + +## Execution Commands + +| Command | Description | +|---------|-------------| +| `appwrite functions create-execution --function-id ` | Trigger a function execution | +| `appwrite functions list-executions --function-id ` | List execution logs | +| `appwrite functions get-execution --function-id --execution-id ` | Get an execution log | + +### Trigger with body + +```sh +appwrite functions create-execution \ + --function-id \ + --body '{"key": "value"}' +``` + +## Environment Variables + +| Command | Description | +|---------|-------------| +| `appwrite functions list-variables --function-id ` | List all variables | +| `appwrite functions create-variable --function-id --key --value ` | Create a variable | +| `appwrite functions update-variable --function-id --variable-id --key --value ` | Update a variable | +| `appwrite functions delete-variable --function-id --variable-id ` | Delete a variable | + +Variables are accessible at runtime as environment variables. + +## Local Development + +Run your function locally for quick debugging: + +```sh +appwrite run functions +``` + +This starts a local development server that watches for file changes and automatically rebuilds. diff --git a/commands/deploy-site.md b/commands/deploy-site.md new file mode 100644 index 0000000..a33d43b --- /dev/null +++ b/commands/deploy-site.md @@ -0,0 +1,138 @@ +--- +name: deploy-site +description: Deploy a site using the Appwrite CLI +--- + +# Deploy a site using the Appwrite CLI + +## Prerequisites + +- Appwrite CLI installed (`npm install -g appwrite-cli`) +- Authenticated with `appwrite login` +- Project initialized with `appwrite init` + +## Quick Deploy + +```sh +appwrite push sites +``` + +## Full Workflow + +### 1. Initialize a new site + +```sh +appwrite init sites +``` + +### 2. Pull existing sites from Console + +```sh +appwrite pull sites +``` + +### 3. Configuration + +Sites are configured in `appwrite.config.json`: + +```json +{ + "projectId": "", + "endpoint": "https://.cloud.appwrite.io/v1", + "sites": [ + { + "$id": "", + "name": "My Site", + "enabled": true, + "logging": true, + "framework": "astro", + "timeout": 30, + "installCommand": "npm install", + "buildCommand": "npm run build", + "outputDirectory": "./dist", + "specification": "s-1vcpu-512mb", + "buildRuntime": "node-22", + "adapter": "ssr", + "fallbackFile": "", + "path": "sites/my-site" + } + ] +} +``` + +### 4. Deploy + +```sh +appwrite push sites +``` + +## CI/CD (Non-Interactive) + +Set up headless mode first: + +```sh +appwrite client \ + --endpoint https://.cloud.appwrite.io/v1 \ + --project-id \ + --key +``` + +Then deploy non-interactively: + +```sh +# Push all sites +appwrite push sites --all --force + +# Push a specific site +appwrite push sites --site-id --force +``` + +## Site Management Commands + +| Command | Description | +|---------|-------------| +| `appwrite sites list` | List all sites in the project | +| `appwrite sites create` | Create a new site | +| `appwrite sites get --site-id ` | Get a site by ID | +| `appwrite sites update --site-id ` | Update a site | +| `appwrite sites delete --site-id ` | Delete a site | +| `appwrite sites list-frameworks` | List available frameworks | +| `appwrite sites list-specifications` | List allowed site specifications | + +## Deployment Commands + +| Command | Description | +|---------|-------------| +| `appwrite sites list-deployments --site-id ` | List all deployments | +| `appwrite sites create-deployment --site-id ` | Create a new deployment | +| `appwrite sites get-deployment --site-id --deployment-id ` | Get a deployment | +| `appwrite sites delete-deployment --site-id --deployment-id ` | Delete a deployment | +| `appwrite sites update-site-deployment --site-id --deployment-id ` | Set active deployment | +| `appwrite sites update-deployment-status --site-id --deployment-id ` | Cancel an ongoing build | + +## Environment Variables + +| Command | Description | +|---------|-------------| +| `appwrite sites list-variables --site-id ` | List all variables | +| `appwrite sites create-variable --site-id --key --value ` | Create a variable | +| `appwrite sites update-variable --site-id --variable-id --key --value ` | Update a variable | +| `appwrite sites delete-variable --site-id --variable-id ` | Delete a variable | + +Variables are accessible during build and runtime (server-side rendering) as environment variables. + +## Logs + +| Command | Description | +|---------|-------------| +| `appwrite sites list-logs --site-id ` | List site request logs | +| `appwrite sites get-log --site-id --log-id ` | Get a specific log | +| `appwrite sites delete-log --site-id --log-id ` | Delete a log | + +## Templates + +| Command | Description | +|---------|-------------| +| `appwrite sites list-templates` | List available site templates | +| `appwrite sites get-template --template-id ` | Get template details | +| `appwrite sites create-template-deployment --site-id ` | Deploy from a template | diff --git a/skills/appwrite-dart/SKILL.md b/skills/appwrite-dart/SKILL.md new file mode 100644 index 0000000..db37b58 --- /dev/null +++ b/skills/appwrite-dart/SKILL.md @@ -0,0 +1,492 @@ +--- +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://.cloud.appwrite.io/v1') + .setProject('[PROJECT_ID]'); +``` + +### Server-side (Dart) + +```dart +import 'package:dart_appwrite/dart_appwrite.dart'; + +final client = Client() + .setEndpoint('https://.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: 'user@example.com', password: 'password123', name: 'User Name'); + +// Login +final session = await account.createEmailPasswordSession(email: 'user@example.com', 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: 'user@example.com', 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) +``` + +### 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: 'user@example.com', +); + +// 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); + +final subscription = realtime.subscribe(['databases.[DATABASE_ID].tables.[TABLE_ID].rows']); +subscription.stream.listen((response) { + print(response.events); // e.g. ['databases.*.tables.*.rows.*.create'] + print(response.payload); // the affected resource +}); + +// Subscribe to multiple channels +final multi = realtime.subscribe([ + 'databases.[DATABASE_ID].tables.[TABLE_ID].rows', + 'buckets.[BUCKET_ID].files', +]); + +// Cleanup +subscription.close(); +``` + +**Available channels:** + +| Channel | Description | +|---------|-------------| +| `account` | Changes to the authenticated user's account | +| `databases.[DB_ID].tables.[TABLE_ID].rows` | All rows in a table | +| `databases.[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 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://.cloud.appwrite.io/v1') + .setProject('[PROJECT_ID]') + .setKey(Platform.environment['APPWRITE_API_KEY']!); + +// Session client (create per-request) +final sessionClient = Client() + .setEndpoint('https://.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_ +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://.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_`. + +> **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 document/file level or inherited from the collection/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 document/file-level permissions when you need per-resource access control. If all documents in a collection share the same rules, configure permissions at the collection/bucket level and leave document 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 + diff --git a/skills/appwrite-dotnet/SKILL.md b/skills/appwrite-dotnet/SKILL.md new file mode 100644 index 0000000..686bab0 --- /dev/null +++ b/skills/appwrite-dotnet/SKILL.md @@ -0,0 +1,405 @@ +--- +name: appwrite-dotnet +description: Appwrite .NET SDK skill. Use when building server-side C# or .NET applications with Appwrite, including ASP.NET and Blazor integrations. Covers user management, database/table CRUD, file storage, and functions via API keys. +--- + + +# Appwrite .NET SDK + +## Installation + +```bash +dotnet add package Appwrite +``` + +## Setting Up the Client + +```csharp +using Appwrite; +using Appwrite.Services; +using Appwrite.Models; + +var client = new Client() + .SetEndpoint("https://.cloud.appwrite.io/v1") + .SetProject(Environment.GetEnvironmentVariable("APPWRITE_PROJECT_ID")) + .SetKey(Environment.GetEnvironmentVariable("APPWRITE_API_KEY")); +``` + +## Code Examples + +### User Management + +```csharp +var users = new Users(client); + +// Create user +var user = await users.Create(ID.Unique(), "user@example.com", null, "password123", "User Name"); + +// List users +var list = await users.List(new List { Query.Limit(25) }); + +// Get user +var fetched = await users.Get("[USER_ID]"); + +// Delete user +await users.Delete("[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 arguments (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. + +```csharp +var tablesDB = new TablesDB(client); + +// Create database +var db = await tablesDB.Create(ID.Unique(), "My Database"); + +// Create row +var doc = await tablesDB.CreateRow("[DATABASE_ID]", "[TABLE_ID]", ID.Unique(), + new Dictionary { { "title", "Hello World" } }); + +// Query rows +var results = await tablesDB.ListRows("[DATABASE_ID]", "[TABLE_ID]", + new List { Query.Equal("title", "Hello World"), Query.Limit(10) }); + +// Get row +var row = await tablesDB.GetRow("[DATABASE_ID]", "[TABLE_ID]", "[ROW_ID]"); + +// Update row +await tablesDB.UpdateRow("[DATABASE_ID]", "[TABLE_ID]", "[ROW_ID]", + new Dictionary { { "title", "Updated" } }); + +// Delete row +await tablesDB.DeleteRow("[DATABASE_ID]", "[TABLE_ID]", "[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. + +```csharp +// Create table with explicit string column types +await tablesDB.CreateTable("[DATABASE_ID]", ID.Unique(), "articles", + new List { + new { key = "title", type = "varchar", size = 255, required = true }, // inline, fully indexable + new { key = "summary", type = "text", required = false }, // off-page, prefix index only + new { key = "body", type = "mediumtext", required = false }, // up to ~4 M chars + new { key = "raw_data", type = "longtext", required = false }, // up to ~1 B chars + }); +``` + +### Query Methods + +```csharp +// Filtering +Query.Equal("field", "value") // == (or pass array 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(new List { "field1", "field2" }) +Query.Or(new List { Query.Equal("a", 1), Query.Equal("b", 2) }) // OR +Query.And(new List { Query.GreaterThan("age", 18), Query.LessThan("age", 65) }) // AND (default) +``` + +### File Storage + +```csharp +var storage = new Storage(client); + +// Upload file +var file = await storage.CreateFile("[BUCKET_ID]", ID.Unique(), InputFile.FromPath("/path/to/file.png")); + +// List files +var files = await storage.ListFiles("[BUCKET_ID]"); + +// Delete file +await storage.DeleteFile("[BUCKET_ID]", "[FILE_ID]"); +``` + +#### InputFile Factory Methods + +```csharp +using Appwrite.Models; + +InputFile.FromPath("/path/to/file.png") // from filesystem path +InputFile.FromBytes(byteArray, "file.png", "image/png") // from byte[] +InputFile.FromStream(stream, "file.png", "image/png", size) // from Stream (size required) +``` + +### Teams + +```csharp +var teams = new Teams(client); + +// Create team +var team = await teams.Create(ID.Unique(), "Engineering"); + +// List teams +var list = await teams.List(); + +// Create membership (invite user by email) +var membership = await teams.CreateMembership( + teamId: "[TEAM_ID]", + roles: new List { "editor" }, + email: "user@example.com" +); + +// List memberships +var members = await teams.ListMemberships("[TEAM_ID]"); + +// Update membership roles +await teams.UpdateMembership("[TEAM_ID]", "[MEMBERSHIP_ID]", new List { "admin" }); + +// Delete team +await teams.Delete("[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. + +### Serverless Functions + +```csharp +var functions = new Functions(client); + +// Execute function +var execution = await functions.CreateExecution("[FUNCTION_ID]", body: "{\"key\": \"value\"}"); + +// List executions +var executions = await functions.ListExecutions("[FUNCTION_ID]"); +``` + +#### Writing a Function Handler (.NET runtime) + +```csharp +// src/Main.cs — Appwrite Function entry point +using System.Text.Json; + +public async Task Main(RuntimeContext context) +{ + // context.Req.Body — raw body (string) + // context.Req.BodyJson — parsed JSON (JsonElement) + // context.Req.Headers — headers (Dictionary) + // context.Req.Method — HTTP method + // context.Req.Path — URL path + // context.Req.Query — query params (Dictionary) + + context.Log($"Processing: {context.Req.Method} {context.Req.Path}"); + + if (context.Req.Method == "GET") + return context.Res.Json(new { message = "Hello from Appwrite Function!" }); + + return context.Res.Json(new { success = true }); // JSON + // context.Res.Text("Hello"); // plain text + // context.Res.Empty(); // 204 + // context.Res.Redirect("https://..."); // 302 +} +``` + +### Server-Side Rendering (SSR) Authentication + +SSR apps using .NET frameworks (ASP.NET, Blazor Server, etc.) use the **server SDK** 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) + +```csharp +using Appwrite; +using Appwrite.Services; + +// Admin client (reusable) +var adminClient = new Client() + .SetEndpoint("https://.cloud.appwrite.io/v1") + .SetProject("[PROJECT_ID]") + .SetKey(Environment.GetEnvironmentVariable("APPWRITE_API_KEY")); + +// Session client (create per-request) +var sessionClient = new Client() + .SetEndpoint("https://.cloud.appwrite.io/v1") + .SetProject("[PROJECT_ID]"); + +var session = Request.Cookies["a_session_[PROJECT_ID]"]; +if (session != null) +{ + sessionClient.SetSession(session); +} +``` + +#### Email/Password Login (ASP.NET Minimal API) + +```csharp +app.MapPost("/login", async (HttpContext ctx, LoginRequest body) => +{ + var account = new Account(adminClient); + var session = await account.CreateEmailPasswordSession(body.Email, body.Password); + + // Cookie name must be a_session_ + ctx.Response.Cookies.Append("a_session_[PROJECT_ID]", session.Secret, new CookieOptions + { + HttpOnly = true, + Secure = true, + SameSite = SameSiteMode.Strict, + Path = "/", + }); + + return Results.Ok(new { success = true }); +}); +``` + +#### Authenticated Requests + +```csharp +app.MapGet("/user", async (HttpContext ctx) => +{ + var session = ctx.Request.Cookies["a_session_[PROJECT_ID]"]; + if (session == null) return Results.Unauthorized(); + + var sessionClient = new Client() + .SetEndpoint("https://.cloud.appwrite.io/v1") + .SetProject("[PROJECT_ID]") + .SetSession(session); + + var account = new Account(sessionClient); + var user = await account.Get(); + return Results.Ok(user); +}); +``` + +#### OAuth2 SSR Flow + +```csharp +// Step 1: Redirect to OAuth provider +app.MapGet("/oauth", async () => +{ + var account = new Account(adminClient); + var redirectUrl = await account.CreateOAuth2Token( + provider: OAuthProvider.Github, + success: "https://example.com/oauth/success", + failure: "https://example.com/oauth/failure" + ); + return Results.Redirect(redirectUrl); +}); + +// Step 2: Handle callback — exchange token for session +app.MapGet("/oauth/success", async (HttpContext ctx, string userId, string secret) => +{ + var account = new Account(adminClient); + var session = await account.CreateSession(userId, secret); + + ctx.Response.Cookies.Append("a_session_[PROJECT_ID]", session.Secret, new CookieOptions + { + HttpOnly = true, Secure = true, SameSite = SameSiteMode.Strict, Path = "/", + }); + + return Results.Ok(new { success = true }); +}); +``` + +> **Cookie security:** Always use `HttpOnly`, `Secure`, and `SameSite = SameSiteMode.Strict` to prevent XSS. The cookie name must be `a_session_`. + +> **Forwarding user agent:** Call `sessionClient.SetForwardedUserAgent(ctx.Request.Headers["User-Agent"])` to record the end-user's browser info for debugging and security. + +## Error Handling + +```csharp +using Appwrite; + +try +{ + var row = await tablesDB.GetRow("[DATABASE_ID]", "[TABLE_ID]", "[ROW_ID]"); +} +catch (AppwriteException e) +{ + Console.WriteLine(e.Message); // human-readable message + Console.WriteLine(e.Code); // HTTP status code (int) + Console.WriteLine(e.Type); // error type (e.g. "document_not_found") + Console.WriteLine(e.Response); // full response body +} +``` + +**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 document/file level or inherited from the collection/bucket settings. Permissions are arrays of strings built with the `Permission` and `Role` helpers. + +```csharp +using Appwrite; +// Permission and Role are included in the main namespace +``` + +### Database Row with Permissions + +```csharp +var doc = await tablesDB.CreateRow("[DATABASE_ID]", "[TABLE_ID]", ID.Unique(), + new Dictionary { { "title", "Hello World" } }, + new List + { + 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 + +```csharp +var file = await storage.CreateFile("[BUCKET_ID]", ID.Unique(), + InputFile.FromPath("/path/to/file.png"), + new List + { + Permission.Read(Role.Any()), + Permission.Update(Role.User("[USER_ID]")), + Permission.Delete(Role.User("[USER_ID]")), + }); +``` + +> **When to set permissions:** Set document/file-level permissions when you need per-resource access control. If all documents in a collection share the same rules, configure permissions at the collection/bucket level and leave document 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 + diff --git a/skills/appwrite-go/SKILL.md b/skills/appwrite-go/SKILL.md new file mode 100644 index 0000000..4b92b81 --- /dev/null +++ b/skills/appwrite-go/SKILL.md @@ -0,0 +1,468 @@ +--- +name: appwrite-go +description: Appwrite Go SDK skill. Use when building server-side Go applications with Appwrite. Covers user management, database/table CRUD, file storage, and functions via API keys. Uses per-service packages and functional options pattern. +--- + + +# Appwrite Go SDK + +## Installation + +```bash +go get github.com/appwrite/sdk-for-go +``` + +## Setting Up the Client + +```go +import ( + "os" + + "github.com/appwrite/sdk-for-go/client" + "github.com/appwrite/sdk-for-go/id" + "github.com/appwrite/sdk-for-go/users" + "github.com/appwrite/sdk-for-go/tablesdb" + "github.com/appwrite/sdk-for-go/storage" +) + +clt := client.New( + client.WithEndpoint("https://.cloud.appwrite.io/v1"), + client.WithProject(os.Getenv("APPWRITE_PROJECT_ID")), + client.WithKey(os.Getenv("APPWRITE_API_KEY")), +) +``` + +## Code Examples + +### User Management + +```go +service := users.New(clt) + +// Create user +user, err := service.Create( + id.Unique(), + "user@example.com", + "password123", + users.WithCreateName("User Name"), +) + +// List users +list, err := service.List() + +// Get user +fetched, err := service.Get("[USER_ID]") + +// Delete user +_, err = service.Delete("[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 explicit functional option parameters (e.g., `tablesdb.WithUpdateRowData(...)`) over bare positional arguments where available. Only use positional-only style if the existing codebase already uses it or the user explicitly requests it. + +```go +service := tablesdb.New(clt) + +// Create database +db, err := service.Create(id.Unique(), "My Database") + +// Create row +doc, err := service.CreateRow( + "[DATABASE_ID]", + "[TABLE_ID]", + id.Unique(), + map[string]interface{}{"title": "Hello World"}, +) + +// List rows +results, err := service.ListRows("[DATABASE_ID]", "[TABLE_ID]") + +// Get row +row, err := service.GetRow("[DATABASE_ID]", "[TABLE_ID]", "[ROW_ID]") + +// Update row +_, err = service.UpdateRow( + "[DATABASE_ID]", + "[TABLE_ID]", + "[ROW_ID]", + tablesdb.WithUpdateRowData(map[string]interface{}{"title": "Updated"}), +) + +// Delete row +_, err = service.DeleteRow("[DATABASE_ID]", "[TABLE_ID]", "[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. + +```go +// Create table with explicit string column types +_, err = service.CreateTable( + "[DATABASE_ID]", + id.Unique(), + "articles", + tablesdb.WithCreateTableColumns([]map[string]interface{}{ + {"key": "title", "type": "varchar", "size": 255, "required": true}, + {"key": "summary", "type": "text", "required": false}, + {"key": "body", "type": "mediumtext", "required": false}, + {"key": "raw_data", "type": "longtext", "required": false}, + }), +) +``` + +### Query Methods + +```go +import "github.com/appwrite/sdk-for-go/query" + +// Filtering +query.Equal("field", "value") // == (or pass slice 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([]string{"field1", "field2"}) +query.Or([]string{query.Equal("a", 1), query.Equal("b", 2)}) // OR +query.And([]string{query.GreaterThan("age", 18), query.LessThan("age", 65)}) // AND (default) +``` + +### File Storage + +```go +import "github.com/appwrite/sdk-for-go/file" + +service := storage.New(clt) + +// Upload file +f, err := service.CreateFile( + "[BUCKET_ID]", + "[FILE_ID]", + file.NewInputFile("/path/to/file.png", "file.png"), +) + +// List files +files, err := service.ListFiles("[BUCKET_ID]") + +// Delete file +_, err = service.DeleteFile("[BUCKET_ID]", "[FILE_ID]") +``` + +#### InputFile Factory Methods + +```go +import "github.com/appwrite/sdk-for-go/file" + +file.NewInputFile("/path/to/file.png", "file.png") // from filesystem path +file.NewInputFileFromReader(reader, "file.png", size) // from io.Reader (size required) +file.NewInputFileFromBytes(data, "file.png") // from []byte +``` + +### Teams + +```go +import "github.com/appwrite/sdk-for-go/teams" + +svc := teams.New(clt) + +// Create team +team, err := svc.Create(id.Unique(), "Engineering") + +// List teams +list, err := svc.List() + +// Create membership (invite user by email) +membership, err := svc.CreateMembership( + "[TEAM_ID]", + []string{"editor"}, + teams.WithCreateMembershipEmail("user@example.com"), +) + +// List memberships +members, err := svc.ListMemberships("[TEAM_ID]") + +// Update membership roles +_, err = svc.UpdateMembership("[TEAM_ID]", "[MEMBERSHIP_ID]", []string{"admin"}) + +// Delete team +_, err = svc.Delete("[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. + +### Serverless Functions + +```go +import "github.com/appwrite/sdk-for-go/functions" + +svc := functions.New(clt) + +// Execute function +execution, err := svc.CreateExecution( + "[FUNCTION_ID]", + functions.WithCreateExecutionBody(`{"key": "value"}`), +) + +// List executions +executions, err := svc.ListExecutions("[FUNCTION_ID]") +``` + +#### Writing a Function Handler (Go runtime) + +```go +// src/main.go — Appwrite Function entry point +package handler + +import ( + "github.com/open-runtimes/types-for-go/v4/openruntimes" +) + +func Main(context openruntimes.Context) openruntimes.Response { + // context.Req.Body — raw body (string) + // context.Req.BodyJson — parsed JSON (map[string]interface{}) + // context.Req.Headers — headers (map[string]string) + // context.Req.Method — HTTP method + // context.Req.Path — URL path + // context.Req.Query — query params (map[string]string) + + context.Log("Processing: " + context.Req.Method + " " + context.Req.Path) + + if context.Req.Method == "GET" { + return context.Res.Json(map[string]interface{}{"message": "Hello!"}) + } + + return context.Res.Json(map[string]interface{}{"success": true}) + // context.Res.Text("Hello") // plain text + // context.Res.Empty() // 204 + // context.Res.Redirect("https://...") // 302 +} +``` + +### Server-Side Rendering (SSR) Authentication + +SSR apps using Go frameworks (net/http, Gin, Echo, Chi, etc.) use the **server SDK** 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) + +```go +import ( + "github.com/appwrite/sdk-for-go/client" + "github.com/appwrite/sdk-for-go/account" +) + +// Admin client (reusable) +adminClient := client.New( + client.WithEndpoint("https://.cloud.appwrite.io/v1"), + client.WithProject(os.Getenv("APPWRITE_PROJECT_ID")), + client.WithKey(os.Getenv("APPWRITE_API_KEY")), +) + +// Session client (create per-request) +sessionClient := client.New( + client.WithEndpoint("https://.cloud.appwrite.io/v1"), + client.WithProject(os.Getenv("APPWRITE_PROJECT_ID")), +) + +cookie, err := r.Cookie("a_session_[PROJECT_ID]") +if err == nil { + sessionClient.SetSession(cookie.Value) +} +``` + +#### Email/Password Login + +```go +http.HandleFunc("/login", func(w http.ResponseWriter, r *http.Request) { + svc := account.New(adminClient) + session, err := svc.CreateEmailPasswordSession(r.FormValue("email"), r.FormValue("password")) + if err != nil { + http.Error(w, err.Error(), http.StatusBadRequest) + return + } + + // Cookie name must be a_session_ + http.SetCookie(w, &http.Cookie{ + Name: "a_session_[PROJECT_ID]", + Value: session.Secret, + HttpOnly: true, + Secure: true, + SameSite: http.SameSiteStrictMode, + Path: "/", + }) + + w.Header().Set("Content-Type", "application/json") + w.Write([]byte(`{"success": true}`)) +}) +``` + +#### Authenticated Requests + +```go +http.HandleFunc("/user", func(w http.ResponseWriter, r *http.Request) { + cookie, err := r.Cookie("a_session_[PROJECT_ID]") + if err != nil { + http.Error(w, "Unauthorized", http.StatusUnauthorized) + return + } + + sessionClient := client.New( + client.WithEndpoint("https://.cloud.appwrite.io/v1"), + client.WithProject(os.Getenv("APPWRITE_PROJECT_ID")), + client.WithSession(cookie.Value), + ) + + svc := account.New(sessionClient) + user, err := svc.Get() + // Marshal user to JSON and write response +}) +``` + +#### OAuth2 SSR Flow + +```go +// Step 1: Redirect to OAuth provider +http.HandleFunc("/oauth", func(w http.ResponseWriter, r *http.Request) { + svc := account.New(adminClient) + redirectURL, err := svc.CreateOAuth2Token( + "github", + account.WithCreateOAuth2TokenSuccess("https://example.com/oauth/success"), + account.WithCreateOAuth2TokenFailure("https://example.com/oauth/failure"), + ) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + http.Redirect(w, r, redirectURL, http.StatusFound) +}) + +// Step 2: Handle callback — exchange token for session +http.HandleFunc("/oauth/success", func(w http.ResponseWriter, r *http.Request) { + svc := account.New(adminClient) + session, err := svc.CreateSession(r.URL.Query().Get("userId"), r.URL.Query().Get("secret")) + if err != nil { + http.Error(w, err.Error(), http.StatusBadRequest) + return + } + + http.SetCookie(w, &http.Cookie{ + Name: "a_session_[PROJECT_ID]", Value: session.Secret, + HttpOnly: true, Secure: true, SameSite: http.SameSiteStrictMode, Path: "/", + }) + w.Write([]byte(`{"success": true}`)) +}) +``` + +> **Cookie security:** Always use `HttpOnly`, `Secure`, and `SameSiteStrictMode` to prevent XSS. The cookie name must be `a_session_`. + +> **Forwarding user agent:** Call `sessionClient.SetForwardedUserAgent(r.Header.Get("User-Agent"))` to record the end-user's browser info for debugging and security. + +## Error Handling + +```go +import "github.com/appwrite/sdk-for-go/apperr" + +doc, err := service.GetRow("[DATABASE_ID]", "[TABLE_ID]", "[ROW_ID]") +if err != nil { + var appErr *apperr.AppwriteException + if errors.As(err, &appErr) { + fmt.Println(appErr.Message) // human-readable message + fmt.Println(appErr.Code) // HTTP status code (int) + fmt.Println(appErr.Type) // error type (e.g. "document_not_found") + } +} +``` + +**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 document/file level or inherited from the collection/bucket settings. Permissions are arrays of strings built with the `permission` and `role` helpers. + +```go +import ( + "github.com/appwrite/sdk-for-go/permission" + "github.com/appwrite/sdk-for-go/role" +) +``` + +### Database Row with Permissions + +```go +doc, err := service.CreateRow( + "[DATABASE_ID]", + "[TABLE_ID]", + "[ROW_ID]", + map[string]interface{}{"title": "Hello World"}, + tablesdb.WithCreateRowPermissions([]string{ + 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 + +```go +f, err := service.CreateFile( + "[BUCKET_ID]", + "[FILE_ID]", + file.NewInputFile("/path/to/file.png", "file.png"), + storage.WithCreateFilePermissions([]string{ + permission.Read(role.Any()), + permission.Update(role.User("[USER_ID]")), + permission.Delete(role.User("[USER_ID]")), + }), +) +``` + +> **When to set permissions:** Set document/file-level permissions when you need per-resource access control. If all documents in a collection share the same rules, configure permissions at the collection/bucket level and leave document 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 + diff --git a/skills/appwrite-kotlin/SKILL.md b/skills/appwrite-kotlin/SKILL.md new file mode 100644 index 0000000..59a94bf --- /dev/null +++ b/skills/appwrite-kotlin/SKILL.md @@ -0,0 +1,542 @@ +--- +name: appwrite-kotlin +description: Appwrite Kotlin SDK skill. Use when building native Android apps or server-side Kotlin/JVM backends with Appwrite. Covers client-side auth (email, OAuth with Activity integration), database queries, file uploads, real-time subscriptions with coroutine support, and server-side admin via API keys for user management, database administration, storage, and functions. +--- + + +# Appwrite Kotlin SDK + +## Installation + +```kotlin +// build.gradle.kts — Android +implementation("io.appwrite:sdk-for-android:+") + +// build.gradle.kts — Server (Kotlin JVM) +implementation("io.appwrite:sdk-for-kotlin:+") +``` + +## Setting Up the Client + +### Client-side (Android) + +```kotlin +import io.appwrite.Client +import io.appwrite.ID +import io.appwrite.Query +import io.appwrite.enums.OAuthProvider +import io.appwrite.services.Account +import io.appwrite.services.Realtime +import io.appwrite.services.TablesDB +import io.appwrite.services.Storage +import io.appwrite.models.InputFile + +val client = Client(context) + .setEndpoint("https://.cloud.appwrite.io/v1") + .setProject("[PROJECT_ID]") +``` + +### Server-side (Kotlin JVM) + +```kotlin +import io.appwrite.Client +import io.appwrite.ID +import io.appwrite.Query +import io.appwrite.services.Users +import io.appwrite.services.TablesDB +import io.appwrite.services.Storage +import io.appwrite.services.Functions + +val client = Client() + .setEndpoint("https://.cloud.appwrite.io/v1") + .setProject(System.getenv("APPWRITE_PROJECT_ID")) + .setKey(System.getenv("APPWRITE_API_KEY")) +``` + +## Code Examples + +### Authentication (client-side) + +```kotlin +val account = Account(client) + +// Signup +account.create( + userId = ID.unique(), + email = "user@example.com", + password = "password123", + name = "User Name" +) + +// Login +val session = account.createEmailPasswordSession( + email = "user@example.com", + password = "password123" +) + +// OAuth +account.createOAuth2Session(activity = activity, provider = OAuthProvider.GOOGLE) + +// Get current user +val user = account.get() + +// Logout +account.deleteSession(sessionId = "current") +``` + +### User Management (server-side) + +```kotlin +val users = Users(client) + +// Create user +val user = users.create( + userId = ID.unique(), + email = "user@example.com", + password = "password123", + name = "User Name" +) + +// List users +val list = users.list() + +// Get user +val fetched = users.get(userId = "[USER_ID]") + +// Delete user +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 arguments (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. + +```kotlin +val tablesDB = TablesDB(client) + +// Create database (server-side only) +val db = tablesDB.create(databaseId = ID.unique(), name = "My Database") + +// Create row +val doc = tablesDB.createRow( + databaseId = "[DATABASE_ID]", + tableId = "[TABLE_ID]", + rowId = ID.unique(), + data = mapOf("title" to "Hello", "done" to false) +) + +// Query rows +val results = tablesDB.listRows( + databaseId = "[DATABASE_ID]", + tableId = "[TABLE_ID]", + queries = listOf(Query.equal("done", false), Query.limit(10)) +) + +// Get row +val row = tablesDB.getRow(databaseId = "[DATABASE_ID]", tableId = "[TABLE_ID]", rowId = "[ROW_ID]") + +// Update row +tablesDB.updateRow( + databaseId = "[DATABASE_ID]", + tableId = "[TABLE_ID]", + rowId = "[ROW_ID]", + data = mapOf("done" to true) +) + +// Delete row +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. + +```kotlin +// Create table with explicit string column types +tablesDB.createTable( + databaseId = "[DATABASE_ID]", + tableId = ID.unique(), + name = "articles", + columns = listOf( + mapOf("key" to "title", "type" to "varchar", "size" to 255, "required" to true), + mapOf("key" to "summary", "type" to "text", "required" to false), + mapOf("key" to "body", "type" to "mediumtext", "required" to false), + mapOf("key" to "raw_data", "type" to "longtext", "required" to false), + ) +) +``` + +### Query Methods + +```kotlin +// 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(listOf("field1", "field2")) +Query.or(listOf(Query.equal("a", 1), Query.equal("b", 2))) // OR +Query.and(listOf(Query.greaterThan("age", 18), Query.lessThan("age", 65))) // AND (default) +``` + +### File Storage + +```kotlin +val storage = Storage(client) + +// Upload file +val file = storage.createFile( + bucketId = "[BUCKET_ID]", + fileId = ID.unique(), + file = InputFile.fromPath("/path/to/file.png") +) + +// Get file preview +val preview = storage.getFilePreview( + bucketId = "[BUCKET_ID]", + fileId = "[FILE_ID]", + width = 300, + height = 300 +) + +// List files +val files = storage.listFiles(bucketId = "[BUCKET_ID]") + +// Delete file +storage.deleteFile(bucketId = "[BUCKET_ID]", fileId = "[FILE_ID]") +``` + +#### InputFile Factory Methods + +```kotlin +import io.appwrite.models.InputFile + +InputFile.fromPath("/path/to/file.png") // from filesystem path +InputFile.fromBytes(byteArray, "file.png") // from ByteArray +``` + +### Teams + +```kotlin +val teams = Teams(client) + +// Create team +val team = teams.create(teamId = ID.unique(), name = "Engineering") + +// List teams +val list = teams.list() + +// Create membership (invite user by email) +val membership = teams.createMembership( + teamId = "[TEAM_ID]", + roles = listOf("editor"), + email = "user@example.com" +) + +// List memberships +val members = teams.listMemberships(teamId = "[TEAM_ID]") + +// Update membership roles +teams.updateMembership(teamId = "[TEAM_ID]", membershipId = "[MEMBERSHIP_ID]", roles = listOf("admin")) + +// Delete team +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) + +```kotlin +val realtime = Realtime(client) + +val subscription = realtime.subscribe("databases.[DATABASE_ID].tables.[TABLE_ID].rows") { response -> + println(response.events) // e.g. ["databases.*.tables.*.rows.*.create"] + println(response.payload) // the affected resource +} + +// Subscribe to multiple channels +val multi = realtime.subscribe( + "databases.[DATABASE_ID].tables.[TABLE_ID].rows", + "buckets.[BUCKET_ID].files" +) { response -> /* ... */ } + +// Cleanup +subscription.close() +``` + +**Available channels:** + +| Channel | Description | +|---------|-------------| +| `account` | Changes to the authenticated user's account | +| `databases.[DB_ID].tables.[TABLE_ID].rows` | All rows in a table | +| `databases.[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 | +| `functions.[FUNCTION_ID].executions` | Function execution updates | + +Response fields: `events` (array), `payload` (resource), `channels` (matched), `timestamp` (ISO 8601). + +### Serverless Functions (server-side) + +```kotlin +val functions = Functions(client) + +// Execute function +val execution = functions.createExecution( + functionId = "[FUNCTION_ID]", + body = """{"key": "value"}""" +) + +// List executions +val executions = functions.listExecutions(functionId = "[FUNCTION_ID]") +``` + +#### Writing a Function Handler (Kotlin runtime) + +```kotlin +// src/Main.kt — Appwrite Function entry point +import io.openruntimes.kotlin.RuntimeContext +import io.openruntimes.kotlin.RuntimeOutput + +fun main(context: RuntimeContext): RuntimeOutput { + // context.req.body — raw body (String) + // context.req.bodyJson — parsed JSON (Map) + // 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(mapOf("message" to "Hello from Appwrite Function!")) + } + + return context.res.json(mapOf("success" to true)) // JSON + // context.res.text("Hello") // plain text + // context.res.empty() // 204 + // context.res.redirect("https://...") // 302 +} +``` + +### Server-Side Rendering (SSR) Authentication + +SSR apps using Kotlin server frameworks (Ktor, Spring Boot, etc.) use the **server SDK** 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) + +```kotlin +import io.appwrite.Client +import io.appwrite.services.Account +import io.appwrite.enums.OAuthProvider + +// Admin client (reusable) +val adminClient = Client() + .setEndpoint("https://.cloud.appwrite.io/v1") + .setProject("[PROJECT_ID]") + .setKey(System.getenv("APPWRITE_API_KEY")) + +// Session client (create per-request) +val sessionClient = Client() + .setEndpoint("https://.cloud.appwrite.io/v1") + .setProject("[PROJECT_ID]") + +val session = call.request.cookies["a_session_[PROJECT_ID]"] +if (session != null) { + sessionClient.setSession(session) +} +``` + +#### Email/Password Login (Ktor) + +```kotlin +post("/login") { + val body = call.receive() + val account = Account(adminClient) + val session = account.createEmailPasswordSession( + email = body.email, + password = body.password, + ) + + // Cookie name must be a_session_ + call.response.cookies.append(Cookie( + name = "a_session_[PROJECT_ID]", + value = session.secret, + httpOnly = true, + secure = true, + extensions = mapOf("SameSite" to "Strict"), + path = "/", + )) + call.respond(mapOf("success" to true)) +} +``` + +#### Authenticated Requests + +```kotlin +get("/user") { + val session = call.request.cookies["a_session_[PROJECT_ID]"] + ?: return@get call.respond(HttpStatusCode.Unauthorized) + + val sessionClient = Client() + .setEndpoint("https://.cloud.appwrite.io/v1") + .setProject("[PROJECT_ID]") + .setSession(session) + + val account = Account(sessionClient) + val user = account.get() + call.respond(user) +} +``` + +#### OAuth2 SSR Flow + +```kotlin +// Step 1: Redirect to OAuth provider +get("/oauth") { + val account = Account(adminClient) + val redirectUrl = account.createOAuth2Token( + provider = OAuthProvider.GITHUB, + success = "https://example.com/oauth/success", + failure = "https://example.com/oauth/failure", + ) + call.respondRedirect(redirectUrl) +} + +// Step 2: Handle callback — exchange token for session +get("/oauth/success") { + val account = Account(adminClient) + val session = account.createSession( + userId = call.parameters["userId"]!!, + secret = call.parameters["secret"]!!, + ) + + call.response.cookies.append(Cookie( + name = "a_session_[PROJECT_ID]", value = session.secret, + httpOnly = true, secure = true, + extensions = mapOf("SameSite" to "Strict"), path = "/", + )) + call.respond(mapOf("success" to true)) +} +``` + +> **Cookie security:** Always use `httpOnly`, `secure`, and `SameSite=Strict` to prevent XSS. The cookie name must be `a_session_`. + +> **Forwarding user agent:** Call `sessionClient.setForwardedUserAgent(call.request.headers["User-Agent"])` to record the end-user's browser info for debugging and security. + +## Error Handling + +```kotlin +import io.appwrite.AppwriteException + +try { + val row = tablesDB.getRow(databaseId = "[DATABASE_ID]", tableId = "[TABLE_ID]", rowId = "[ROW_ID]") +} catch (e: AppwriteException) { + println(e.message) // human-readable message + println(e.code) // HTTP status code (Int) + println(e.type) // error type (e.g. "document_not_found") + println(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 document/file level or inherited from the collection/bucket settings. Permissions are arrays of strings built with the `Permission` and `Role` helpers. + +```kotlin +import io.appwrite.Permission +import io.appwrite.Role +``` + +### Database Row with Permissions + +```kotlin +val doc = tablesDB.createRow( + databaseId = "[DATABASE_ID]", + tableId = "[TABLE_ID]", + rowId = ID.unique(), + data = mapOf("title" to "Hello World"), + permissions = listOf( + 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 + +```kotlin +val file = storage.createFile( + bucketId = "[BUCKET_ID]", + fileId = ID.unique(), + file = InputFile.fromPath("/path/to/file.png"), + permissions = listOf( + Permission.read(Role.any()), + Permission.update(Role.user("[USER_ID]")), + Permission.delete(Role.user("[USER_ID]")), + ) +) +``` + +> **When to set permissions:** Set document/file-level permissions when you need per-resource access control. If all documents in a collection share the same rules, configure permissions at the collection/bucket level and leave document 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 + diff --git a/skills/appwrite-php/SKILL.md b/skills/appwrite-php/SKILL.md new file mode 100644 index 0000000..a9dc6e4 --- /dev/null +++ b/skills/appwrite-php/SKILL.md @@ -0,0 +1,386 @@ +--- +name: appwrite-php +description: Appwrite PHP SDK skill. Use when building server-side PHP applications with Appwrite, including Laravel and Symfony integrations. Covers user management, database/table CRUD, file storage, and functions via API keys. +--- + + +# Appwrite PHP SDK + +## Installation + +```bash +composer require appwrite/appwrite +``` + +## Setting Up the Client + +```php +use Appwrite\Client; +use Appwrite\ID; +use Appwrite\Query; +use Appwrite\Services\Users; +use Appwrite\Services\TablesDB; +use Appwrite\Services\Storage; +use Appwrite\Services\Functions; +use Appwrite\InputFile; + +$client = (new Client()) + ->setEndpoint('https://.cloud.appwrite.io/v1') + ->setProject(getenv('APPWRITE_PROJECT_ID')) + ->setKey(getenv('APPWRITE_API_KEY')); +``` + +## Code Examples + +### User Management + +```php +$users = new Users($client); + +// Create user +$user = $users->create(ID::unique(), 'user@example.com', null, 'password123', 'User Name'); + +// List users +$list = $users->list([Query::limit(25)]); + +// Get user +$fetched = $users->get('[USER_ID]'); + +// Delete user +$users->delete('[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 arguments (PHP 8+, 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. + +```php +$tablesDB = new TablesDB($client); + +// Create database +$db = $tablesDB->create(ID::unique(), 'My Database'); + +// Create row +$doc = $tablesDB->createRow('[DATABASE_ID]', '[TABLE_ID]', ID::unique(), [ + 'title' => 'Hello World' +]); + +// Query rows +$results = $tablesDB->listRows('[DATABASE_ID]', '[TABLE_ID]', [ + Query::equal('title', ['Hello World']), + Query::limit(10) +]); + +// Get row +$row = $tablesDB->getRow('[DATABASE_ID]', '[TABLE_ID]', '[ROW_ID]'); + +// Update row +$tablesDB->updateRow('[DATABASE_ID]', '[TABLE_ID]', '[ROW_ID]', [ + 'title' => 'Updated' +]); + +// Delete row +$tablesDB->deleteRow('[DATABASE_ID]', '[TABLE_ID]', '[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. + +```php +// Create table with explicit string column types +$tablesDB->createTable('[DATABASE_ID]', ID::unique(), 'articles', [ + ['key' => 'title', 'type' => 'varchar', 'size' => 255, 'required' => true], + ['key' => 'summary', 'type' => 'text', 'required' => false], + ['key' => 'body', 'type' => 'mediumtext', 'required' => false], + ['key' => 'raw_data', 'type' => 'longtext', 'required' => false], +]); +``` + +### Query Methods + +```php +// Filtering +Query::equal('field', ['value']) // == (always pass array) +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 (string or array) +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) +``` + +### File Storage + +```php +$storage = new Storage($client); + +// Upload file +$file = $storage->createFile('[BUCKET_ID]', ID::unique(), InputFile::withPath('/path/to/file.png')); + +// List files +$files = $storage->listFiles('[BUCKET_ID]'); + +// Delete file +$storage->deleteFile('[BUCKET_ID]', '[FILE_ID]'); +``` + +#### InputFile Factory Methods + +```php +use Appwrite\InputFile; + +InputFile::withPath('/path/to/file.png') // from filesystem path +InputFile::withData('Hello world', 'hello.txt') // from string content +``` + +### Teams + +```php +$teams = new Teams($client); + +// Create team +$team = $teams->create(ID::unique(), 'Engineering'); + +// List teams +$list = $teams->list(); + +// Create membership (invite user by email) +$membership = $teams->createMembership('[TEAM_ID]', ['editor'], email: 'user@example.com'); + +// List memberships +$members = $teams->listMemberships('[TEAM_ID]'); + +// Update membership roles +$teams->updateMembership('[TEAM_ID]', '[MEMBERSHIP_ID]', ['admin']); + +// Delete team +$teams->delete('[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. + +### Serverless Functions + +```php +$functions = new Functions($client); + +// Execute function +$execution = $functions->createExecution('[FUNCTION_ID]', '{"key": "value"}'); + +// List executions +$executions = $functions->listExecutions('[FUNCTION_ID]'); +``` + +#### Writing a Function Handler (PHP runtime) + +```php +// src/main.php — Appwrite Function entry point +return function ($context) { + // $context->req->body — raw body (string) + // $context->req->bodyJson — parsed JSON (array or null) + // $context->req->headers — headers (array) + // $context->req->method — HTTP method + // $context->req->path — URL path + // $context->req->query — query params (array) + + $context->log('Processing: ' . $context->req->method . ' ' . $context->req->path); + + if ($context->req->method === 'GET') { + return $context->res->json(['message' => 'Hello from Appwrite Function!']); + } + + $data = $context->req->bodyJson ?? []; + if (!isset($data['name'])) { + $context->error('Missing name field'); + return $context->res->json(['error' => 'Name is required'], 400); + } + + 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 (Laravel, Symfony, etc.) use the **server SDK** 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) + +```php +use Appwrite\Client; +use Appwrite\Services\Account; + +// Admin client (reusable) +$adminClient = (new Client()) + ->setEndpoint('https://.cloud.appwrite.io/v1') + ->setProject('[PROJECT_ID]') + ->setKey(getenv('APPWRITE_API_KEY')); + +// Session client (create per-request) +$sessionClient = (new Client()) + ->setEndpoint('https://.cloud.appwrite.io/v1') + ->setProject('[PROJECT_ID]'); + +$session = $_COOKIE['a_session_[PROJECT_ID]'] ?? null; +if ($session) { + $sessionClient->setSession($session); +} +``` + +#### Email/Password Login + +```php +$account = new Account($adminClient); +$session = $account->createEmailPasswordSession($email, $password); + +// Cookie name must be a_session_ +setcookie('a_session_[PROJECT_ID]', $session['secret'], [ + 'httpOnly' => true, + 'secure' => true, + 'sameSite' => 'strict', + 'expires' => strtotime($session['expire']), + 'path' => '/', +]); +``` + +#### Authenticated Requests + +```php +$session = $_COOKIE['a_session_[PROJECT_ID]'] ?? null; +if (!$session) { + http_response_code(401); + exit; +} + +$sessionClient->setSession($session); +$account = new Account($sessionClient); +$user = $account->get(); +``` + +#### OAuth2 SSR Flow + +```php +// Step 1: Redirect to OAuth provider +$account = new Account($adminClient); +$redirectUrl = $account->createOAuth2Token( + OAuthProvider::GITHUB(), + 'https://example.com/oauth/success', + 'https://example.com/oauth/failure', +); +header('Location: ' . $redirectUrl); + +// Step 2: Handle callback — exchange token for session +$account = new Account($adminClient); +$session = $account->createSession($_GET['userId'], $_GET['secret']); + +setcookie('a_session_[PROJECT_ID]', $session['secret'], [ + 'httpOnly' => true, 'secure' => true, 'sameSite' => 'strict', + 'expires' => strtotime($session['expire']), 'path' => '/', +]); +``` + +> **Cookie security:** Always use `httpOnly`, `secure`, and `sameSite: 'strict'` to prevent XSS. The cookie name must be `a_session_`. + +> **Forwarding user agent:** Call `$sessionClient->setForwardedUserAgent($_SERVER['HTTP_USER_AGENT'])` to record the end-user's browser info for debugging and security. + +## Error Handling + +```php +use Appwrite\AppwriteException; + +try { + $row = $tablesDB->getRow('[DATABASE_ID]', '[TABLE_ID]', '[ROW_ID]'); +} catch (AppwriteException $e) { + echo $e->getMessage(); // human-readable error message + echo $e->getCode(); // HTTP status code (int) + echo $e->getType(); // Appwrite error type string (e.g. 'document_not_found') + echo $e->getResponse(); // full response body (array) +} +``` + +**Common error codes:** + +| Code | Meaning | +|------|---------| +| `401` | Unauthorized — missing or invalid session/API key | +| `403` | Forbidden — insufficient permissions for this action | +| `404` | Not found — resource does not exist | +| `409` | Conflict — duplicate ID or unique constraint violation | +| `429` | Rate limited — too many requests, retry after backoff | + +## 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 document/file level or inherited from the collection/bucket settings. Permissions are arrays of strings built with the `Permission` and `Role` helpers. + +```php +use Appwrite\Permission; +use Appwrite\Role; +``` + +### Database Row with Permissions + +```php +$doc = $tablesDB->createRow('[DATABASE_ID]', '[TABLE_ID]', ID::unique(), [ + 'title' => 'Hello World' +], [ + 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 + +```php +$file = $storage->createFile('[BUCKET_ID]', ID::unique(), InputFile::withPath('/path/to/file.png'), [ + Permission::read(Role::any()), + Permission::update(Role::user('[USER_ID]')), + Permission::delete(Role::user('[USER_ID]')), +]); +``` + +> **When to set permissions:** Set document/file-level permissions when you need per-resource access control. If all documents in a collection share the same rules, configure permissions at the collection/bucket level and leave document 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 + diff --git a/skills/appwrite-python/SKILL.md b/skills/appwrite-python/SKILL.md new file mode 100644 index 0000000..952da83 --- /dev/null +++ b/skills/appwrite-python/SKILL.md @@ -0,0 +1,409 @@ +--- +name: appwrite-python +description: Appwrite Python SDK skill. Use when building server-side Python applications with Appwrite, including Django, Flask, and FastAPI integrations. Covers user management, database/table CRUD, file storage, and functions via API keys. +--- + + +# Appwrite Python SDK + +## Installation + +```bash +pip install appwrite +``` + +## Setting Up the Client + +```python +from appwrite.client import Client +from appwrite.id import ID +from appwrite.query import Query +from appwrite.services.users import Users +from appwrite.services.tablesdb import TablesDB +from appwrite.services.storage import Storage +from appwrite.services.functions import Functions +from appwrite.enums.o_auth_provider import OAuthProvider + +import os + +client = (Client() + .set_endpoint('https://.cloud.appwrite.io/v1') + .set_project(os.environ['APPWRITE_PROJECT_ID']) + .set_key(os.environ['APPWRITE_API_KEY'])) +``` + +## Code Examples + +### User Management + +```python +users = Users(client) + +# Create user +user = users.create(ID.unique(), 'user@example.com', None, 'password123', 'User Name') + +# List users +result = users.list([Query.limit(25)]) + +# Get user +fetched = users.get('[USER_ID]') + +# Delete user +users.delete('[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 keyword arguments (e.g., `database_id='...'`) over positional arguments for all SDK method calls. Only use positional style if the existing codebase already uses it or the user explicitly requests it. + +```python +tables_db = TablesDB(client) + +# Create database +db = tables_db.create(ID.unique(), 'My Database') + +# Create row +doc = tables_db.create_row('[DATABASE_ID]', '[TABLE_ID]', ID.unique(), { + 'title': 'Hello World' +}) + +# Query rows +results = tables_db.list_rows('[DATABASE_ID]', '[TABLE_ID]', [ + Query.equal('title', 'Hello World'), + Query.limit(10) +]) + +# Get row +row = tables_db.get_row('[DATABASE_ID]', '[TABLE_ID]', '[ROW_ID]') + +# Update row +tables_db.update_row('[DATABASE_ID]', '[TABLE_ID]', '[ROW_ID]', { + 'title': 'Updated' +}) + +# Delete row +tables_db.delete_row('[DATABASE_ID]', '[TABLE_ID]', '[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. + +```python +# Create table with explicit string column types +tables_db.create_table( + database_id='[DATABASE_ID]', + table_id=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 + +```python +# Filtering +Query.equal('field', 'value') # == (or pass list for IN) +Query.not_equal('field', 'value') # != +Query.less_than('field', 100) # < +Query.less_than_equal('field', 100) # <= +Query.greater_than('field', 100) # > +Query.greater_than_equal('field', 100) # >= +Query.between('field', 1, 100) # 1 <= field <= 100 +Query.is_null('field') # is null +Query.is_not_null('field') # is not null +Query.starts_with('field', 'prefix') # starts with +Query.ends_with('field', 'suffix') # ends with +Query.contains('field', 'sub') # contains (string or array) +Query.search('field', 'keywords') # full-text search (requires index) + +# Sorting +Query.order_asc('field') +Query.order_desc('field') + +# Pagination +Query.limit(25) # max rows (default 25, max 100) +Query.offset(0) # skip N rows +Query.cursor_after('[ROW_ID]') # cursor pagination (preferred) +Query.cursor_before('[ROW_ID]') + +# Selection & Logic +Query.select(['field1', 'field2']) # return only specified fields +Query.or_queries([Query.equal('a', 1), Query.equal('b', 2)]) # OR +Query.and_queries([Query.greater_than('age', 18), Query.less_than('age', 65)]) # AND (default) +``` + +### File Storage + +```python +from appwrite.input_file import InputFile + +storage = Storage(client) + +# Upload file +file = storage.create_file('[BUCKET_ID]', ID.unique(), InputFile.from_path('/path/to/file.png')) + +# List files +files = storage.list_files('[BUCKET_ID]') + +# Delete file +storage.delete_file('[BUCKET_ID]', '[FILE_ID]') +``` + +#### InputFile Factory Methods + +```python +from appwrite.input_file import InputFile + +InputFile.from_path('/path/to/file.png') # from filesystem path +InputFile.from_bytes(byte_data, 'file.png') # from bytes +InputFile.from_string('Hello world', 'hello.txt') # from string content +``` + +### Teams + +```python +from appwrite.services.teams import Teams + +teams = Teams(client) + +# Create team +team = teams.create(ID.unique(), 'Engineering') + +# List teams +team_list = teams.list() + +# Create membership (invite user by email) +membership = teams.create_membership('[TEAM_ID]', roles=['editor'], email='user@example.com') + +# List memberships +members = teams.list_memberships('[TEAM_ID]') + +# Update membership roles +teams.update_membership('[TEAM_ID]', '[MEMBERSHIP_ID]', roles=['admin']) + +# Delete team +teams.delete('[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. + +### Serverless Functions + +```python +functions = Functions(client) + +# Execute function +execution = functions.create_execution('[FUNCTION_ID]', body='{"key": "value"}') + +# List executions +executions = functions.list_executions('[FUNCTION_ID]') +``` + +#### Writing a Function Handler (Python runtime) + +```python +# src/main.py — Appwrite Function entry point +def main(context): + # context.req — request object + # .body — raw request body (string) + # .body_json — parsed JSON body (dict, or None if not JSON) + # .headers — request headers (dict) + # .method — HTTP method (GET, POST, etc.) + # .path — URL path + # .query — parsed query parameters (dict) + # .query_string — raw query string + + context.log('Processing: ' + context.req.method + ' ' + context.req.path) + + if context.req.method == 'GET': + return context.res.json({'message': 'Hello from Appwrite Function!'}) + + data = context.req.body_json or {} + if 'name' not in data: + context.error('Missing name field') + return context.res.json({'error': 'Name is required'}, 400) + + # Response methods + return context.res.json({'success': True}) # JSON response + # return context.res.text('Hello') # plain text + # return context.res.empty() # 204 No Content + # return context.res.redirect('https://example.com') # 302 Redirect + # return context.res.send('data', 200, {'X-Custom': '1'}) # custom response +``` + +### Server-Side Rendering (SSR) Authentication + +SSR apps (Flask, Django, FastAPI, etc.) use the **server SDK** 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) + +```python +from appwrite.client import Client +from appwrite.services.account import Account +from flask import request, jsonify, make_response, redirect + +# Admin client (reusable) +admin_client = (Client() + .set_endpoint('https://.cloud.appwrite.io/v1') + .set_project('[PROJECT_ID]') + .set_key(os.environ['APPWRITE_API_KEY'])) + +# Session client (create per-request) +session_client = (Client() + .set_endpoint('https://.cloud.appwrite.io/v1') + .set_project('[PROJECT_ID]')) + +session = request.cookies.get('a_session_[PROJECT_ID]') +if session: + session_client.set_session(session) +``` + +#### Email/Password Login + +```python +@app.post('/login') +def login(): + account = Account(admin_client) + session = account.create_email_password_session( + request.json['email'], request.json['password'] + ) + + # Cookie name must be a_session_ + resp = make_response(jsonify({'success': True})) + resp.set_cookie('a_session_[PROJECT_ID]', session['secret'], + httponly=True, secure=True, samesite='Strict', + expires=session['expire'], path='/') + return resp +``` + +#### Authenticated Requests + +```python +@app.get('/user') +def get_user(): + session = request.cookies.get('a_session_[PROJECT_ID]') + if not session: + return jsonify({'error': 'Unauthorized'}), 401 + + session_client = (Client() + .set_endpoint('https://.cloud.appwrite.io/v1') + .set_project('[PROJECT_ID]') + .set_session(session)) + + account = Account(session_client) + return jsonify(account.get()) +``` + +#### OAuth2 SSR Flow + +```python +# Step 1: Redirect to OAuth provider +@app.get('/oauth') +def oauth(): + account = Account(admin_client) + redirect_url = account.create_o_auth2_token( + OAuthProvider.Github, + 'https://example.com/oauth/success', + 'https://example.com/oauth/failure', + ) + return redirect(redirect_url) + +# Step 2: Handle callback — exchange token for session +@app.get('/oauth/success') +def oauth_success(): + account = Account(admin_client) + session = account.create_session(request.args['userId'], request.args['secret']) + + resp = make_response(jsonify({'success': True})) + resp.set_cookie('a_session_[PROJECT_ID]', session['secret'], + httponly=True, secure=True, samesite='Strict', + expires=session['expire'], path='/') + return resp +``` + +> **Cookie security:** Always use `httponly`, `secure`, and `samesite='Strict'` to prevent XSS. The cookie name must be `a_session_`. + +> **Forwarding user agent:** Call `session_client.set_forwarded_user_agent(request.headers.get('user-agent'))` to record the end-user's browser info for debugging and security. + +## Error Handling + +```python +from appwrite.exception import AppwriteException + +try: + row = tables_db.get_row('[DATABASE_ID]', '[TABLE_ID]', '[ROW_ID]') +except AppwriteException as e: + print(e.message) # human-readable error message + print(e.code) # HTTP status code (int) + print(e.type) # Appwrite error type string (e.g. 'document_not_found') + print(e.response) # full response body (dict) +``` + +**Common error codes:** + +| Code | Meaning | +|------|---------| +| `401` | Unauthorized — missing or invalid session/API key | +| `403` | Forbidden — insufficient permissions for this action | +| `404` | Not found — resource does not exist | +| `409` | Conflict — duplicate ID or unique constraint violation | +| `429` | Rate limited — too many requests, retry after backoff | + +## 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 document/file level or inherited from the collection/bucket settings. Permissions are arrays of strings built with the `Permission` and `Role` helpers. + +```python +from appwrite.permission import Permission +from appwrite.role import Role +``` + +### Database Row with Permissions + +```python +doc = tables_db.create_row('[DATABASE_ID]', '[TABLE_ID]', ID.unique(), { + 'title': 'Hello World' +}, [ + 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 + +```python +file = storage.create_file('[BUCKET_ID]', ID.unique(), InputFile.from_path('/path/to/file.png'), [ + Permission.read(Role.any()), + Permission.update(Role.user('[USER_ID]')), + Permission.delete(Role.user('[USER_ID]')), +]) +``` + +> **When to set permissions:** Set document/file-level permissions when you need per-resource access control. If all documents in a collection share the same rules, configure permissions at the collection/bucket level and leave document 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 + diff --git a/skills/appwrite-ruby/SKILL.md b/skills/appwrite-ruby/SKILL.md new file mode 100644 index 0000000..e13c73d --- /dev/null +++ b/skills/appwrite-ruby/SKILL.md @@ -0,0 +1,422 @@ +--- +name: appwrite-ruby +description: Appwrite Ruby SDK skill. Use when building server-side Ruby applications with Appwrite, including Rails and Sinatra integrations. Covers user management, database/table CRUD, file storage, and functions via API keys. +--- + + +# Appwrite Ruby SDK + +## Installation + +```bash +gem install appwrite +``` + +## Setting Up the Client + +```ruby +require 'appwrite' + +include Appwrite + +client = Client.new + .set_endpoint('https://.cloud.appwrite.io/v1') + .set_project(ENV['APPWRITE_PROJECT_ID']) + .set_key(ENV['APPWRITE_API_KEY']) +``` + +## Code Examples + +### User Management + +```ruby +users = Users.new(client) + +# Create user +user = users.create(user_id: ID.unique, email: 'user@example.com', password: 'password123', name: 'User Name') + +# List users +list = users.list(queries: [Query.limit(25)]) + +# Get user +fetched = users.get(user_id: '[USER_ID]') + +# Delete user +users.delete(user_id: '[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 keyword arguments (e.g., `database_id: '...'`) for all SDK method calls. Only use positional arguments if the existing codebase already uses them or the user explicitly requests it. + +```ruby +tables_db = TablesDB.new(client) + +# Create database +db = tables_db.create(database_id: ID.unique, name: 'My Database') + +# Create row +doc = tables_db.create_row( + database_id: '[DATABASE_ID]', + table_id: '[TABLE_ID]', + row_id: ID.unique, + data: { title: 'Hello World' } +) + +# Query rows +results = tables_db.list_rows( + database_id: '[DATABASE_ID]', + table_id: '[TABLE_ID]', + queries: [Query.equal('title', 'Hello World'), Query.limit(10)] +) + +# Get row +row = tables_db.get_row(database_id: '[DATABASE_ID]', table_id: '[TABLE_ID]', row_id: '[ROW_ID]') + +# Update row +tables_db.update_row( + database_id: '[DATABASE_ID]', + table_id: '[TABLE_ID]', + row_id: '[ROW_ID]', + data: { title: 'Updated' } +) + +# Delete row +tables_db.delete_row(database_id: '[DATABASE_ID]', table_id: '[TABLE_ID]', row_id: '[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. + +```ruby +# Create table with explicit string column types +tables_db.create_table( + database_id: '[DATABASE_ID]', + table_id: 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 + +```ruby +# Filtering +Query.equal('field', 'value') # == (or pass array for IN) +Query.not_equal('field', 'value') # != +Query.less_than('field', 100) # < +Query.less_than_equal('field', 100) # <= +Query.greater_than('field', 100) # > +Query.greater_than_equal('field', 100) # >= +Query.between('field', 1, 100) # 1 <= field <= 100 +Query.is_null('field') # is null +Query.is_not_null('field') # is not null +Query.starts_with('field', 'prefix') # starts with +Query.ends_with('field', 'suffix') # ends with +Query.contains('field', 'sub') # contains +Query.search('field', 'keywords') # full-text search (requires index) + +# Sorting +Query.order_asc('field') +Query.order_desc('field') + +# Pagination +Query.limit(25) # max rows (default 25, max 100) +Query.offset(0) # skip N rows +Query.cursor_after('[ROW_ID]') # cursor pagination (preferred) +Query.cursor_before('[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.greater_than('age', 18), Query.less_than('age', 65)]) # AND (default) +``` + +### File Storage + +```ruby +storage = Storage.new(client) + +# Upload file +file = storage.create_file(bucket_id: '[BUCKET_ID]', file_id: ID.unique, file: InputFile.from_path('/path/to/file.png')) + +# List files +files = storage.list_files(bucket_id: '[BUCKET_ID]') + +# Delete file +storage.delete_file(bucket_id: '[BUCKET_ID]', file_id: '[FILE_ID]') +``` + +#### InputFile Factory Methods + +```ruby +InputFile.from_path('/path/to/file.png') # from filesystem path +InputFile.from_string('Hello world', 'hello.txt') # from string content +``` + +### Teams + +```ruby +teams = Teams.new(client) + +# Create team +team = teams.create(team_id: ID.unique, name: 'Engineering') + +# List teams +list = teams.list + +# Create membership (invite user by email) +membership = teams.create_membership( + team_id: '[TEAM_ID]', + roles: ['editor'], + email: 'user@example.com' +) + +# List memberships +members = teams.list_memberships(team_id: '[TEAM_ID]') + +# Update membership roles +teams.update_membership(team_id: '[TEAM_ID]', membership_id: '[MEMBERSHIP_ID]', roles: ['admin']) + +# Delete team +teams.delete(team_id: '[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. + +### Serverless Functions + +```ruby +functions = Functions.new(client) + +# Execute function +execution = functions.create_execution(function_id: '[FUNCTION_ID]', body: '{"key": "value"}') + +# List executions +executions = functions.list_executions(function_id: '[FUNCTION_ID]') +``` + +#### Writing a Function Handler (Ruby runtime) + +```ruby +# src/main.rb — Appwrite Function entry point +def main(context) + # context.req.body — raw body (String) + # context.req.body_json — parsed JSON (Hash or nil) + # context.req.headers — headers (Hash) + # context.req.method — HTTP method + # context.req.path — URL path + # context.req.query — query params (Hash) + + context.log("Processing: #{context.req.method} #{context.req.path}") + + if context.req.method == 'GET' + return context.res.json({ message: 'Hello from Appwrite Function!' }) + end + + context.res.json({ success: true }) # JSON + # context.res.text('Hello') # plain text + # context.res.empty # 204 + # context.res.redirect('https://...') # 302 +end +``` + +### Server-Side Rendering (SSR) Authentication + +SSR apps using Ruby frameworks (Rails, Sinatra, etc.) use the **server SDK** 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) + +```ruby +require 'appwrite' +include Appwrite + +# Admin client (reusable) +admin_client = Client.new + .set_endpoint('https://.cloud.appwrite.io/v1') + .set_project('[PROJECT_ID]') + .set_key(ENV['APPWRITE_API_KEY']) + +# Session client (create per-request) +session_client = Client.new + .set_endpoint('https://.cloud.appwrite.io/v1') + .set_project('[PROJECT_ID]') + +session = cookies['a_session_[PROJECT_ID]'] +session_client.set_session(session) if session +``` + +#### Email/Password Login (Sinatra) + +```ruby +post '/login' do + account = Account.new(admin_client) + session = account.create_email_password_session( + email: params[:email], + password: params[:password] + ) + + # Cookie name must be a_session_ + response.set_cookie('a_session_[PROJECT_ID]', { + value: session.secret, + httponly: true, + secure: true, + same_site: :strict, + path: '/', + }) + + content_type :json + { success: true }.to_json +end +``` + +#### Authenticated Requests + +```ruby +get '/user' do + session = request.cookies['a_session_[PROJECT_ID]'] + halt 401, { error: 'Unauthorized' }.to_json unless session + + session_client = Client.new + .set_endpoint('https://.cloud.appwrite.io/v1') + .set_project('[PROJECT_ID]') + .set_session(session) + + account = Account.new(session_client) + user = account.get + + content_type :json + user.to_json +end +``` + +#### OAuth2 SSR Flow + +```ruby +# Step 1: Redirect to OAuth provider +get '/oauth' do + account = Account.new(admin_client) + redirect_url = account.create_o_auth2_token( + provider: OAuthProvider::GITHUB, + success: 'https://example.com/oauth/success', + failure: 'https://example.com/oauth/failure' + ) + redirect redirect_url +end + +# Step 2: Handle callback — exchange token for session +get '/oauth/success' do + account = Account.new(admin_client) + session = account.create_session( + user_id: params[:userId], + secret: params[:secret] + ) + + response.set_cookie('a_session_[PROJECT_ID]', { + value: session.secret, + httponly: true, secure: true, same_site: :strict, path: '/', + }) + + content_type :json + { success: true }.to_json +end +``` + +> **Cookie security:** Always use `httponly`, `secure`, and `same_site: :strict` to prevent XSS. The cookie name must be `a_session_`. + +> **Forwarding user agent:** Call `session_client.set_forwarded_user_agent(request.user_agent)` to record the end-user's browser info for debugging and security. + +## Error Handling + +```ruby +require 'appwrite' +include Appwrite + +begin + row = tables_db.get_row(database_id: '[DATABASE_ID]', table_id: '[TABLE_ID]', row_id: '[ROW_ID]') +rescue Appwrite::Exception => e + puts e.message # human-readable message + puts e.code # HTTP status code (Integer) + puts e.type # error type (e.g. 'document_not_found') + puts e.response # full response body (Hash) +end +``` + +**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 document/file level or inherited from the collection/bucket settings. Permissions are arrays of strings built with the `Permission` and `Role` helpers. + +```ruby +# Permission and Role are included in the main require +require 'appwrite' +include Appwrite +``` + +### Database Row with Permissions + +```ruby +doc = tables_db.create_row( + database_id: '[DATABASE_ID]', + table_id: '[TABLE_ID]', + row_id: 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 + +```ruby +file = storage.create_file( + bucket_id: '[BUCKET_ID]', + file_id: ID.unique, + file: InputFile.from_path('/path/to/file.png'), + permissions: [ + Permission.read(Role.any), + Permission.update(Role.user('[USER_ID]')), + Permission.delete(Role.user('[USER_ID]')), + ] +) +``` + +> **When to set permissions:** Set document/file-level permissions when you need per-resource access control. If all documents in a collection share the same rules, configure permissions at the collection/bucket level and leave document 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 + diff --git a/skills/appwrite-swift/SKILL.md b/skills/appwrite-swift/SKILL.md new file mode 100644 index 0000000..596ef48 --- /dev/null +++ b/skills/appwrite-swift/SKILL.md @@ -0,0 +1,479 @@ +--- +name: appwrite-swift +description: Appwrite Swift SDK skill. Use when building native iOS, macOS, watchOS, or tvOS apps, or server-side Swift applications with Appwrite. Covers client-side auth (email, OAuth), database queries, file uploads, real-time subscriptions with async/await, and server-side admin via API keys for user management, database administration, storage, and functions. +--- + + +# Appwrite Swift SDK + +## Installation + +```swift +// Swift Package Manager — Package.swift +.package(url: "https://github.com/appwrite/sdk-for-swift", branch: "main") +``` + +## Setting Up the Client + +### Client-side (Apple platforms) + +```swift +import Appwrite + +let client = Client() + .setEndpoint("https://.cloud.appwrite.io/v1") + .setProject("[PROJECT_ID]") +``` + +### Server-side (Swift) + +```swift +import Appwrite + +let client = Client() + .setEndpoint("https://.cloud.appwrite.io/v1") + .setProject(ProcessInfo.processInfo.environment["APPWRITE_PROJECT_ID"]!) + .setKey(ProcessInfo.processInfo.environment["APPWRITE_API_KEY"]!) +``` + +## Code Examples + +### Authentication (client-side) + +```swift +let account = Account(client) + +// Signup +let user = try await account.create(userId: ID.unique(), email: "user@example.com", password: "password123", name: "User Name") + +// Login +let session = try await account.createEmailPasswordSession(email: "user@example.com", password: "password123") + +// OAuth +try await account.createOAuth2Session(provider: .google) + +// Get current user +let me = try await account.get() + +// Logout +try await account.deleteSession(sessionId: "current") +``` + +### User Management (server-side) + +```swift +let users = Users(client) + +// Create user +let user = try await users.create(userId: ID.unique(), email: "user@example.com", password: "password123", name: "User Name") + +// List users +let list = try await users.list(queries: [Query.limit(25)]) + +// Get user +let fetched = try await users.get(userId: "[USER_ID]") + +// Delete user +try 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. + +```swift +let tablesDB = TablesDB(client) + +// Create database (server-side only) +let db = try await tablesDB.create(databaseId: ID.unique(), name: "My Database") + +// Create row +let doc = try await tablesDB.createRow(databaseId: "[DATABASE_ID]", tableId: "[TABLE_ID]", rowId: ID.unique(), data: [ + "title": "Hello", + "done": false +]) + +// Query rows +let results = try await tablesDB.listRows(databaseId: "[DATABASE_ID]", tableId: "[TABLE_ID]", queries: [ + Query.equal("done", value: false), + Query.limit(10) +]) + +// Get row +let row = try await tablesDB.getRow(databaseId: "[DATABASE_ID]", tableId: "[TABLE_ID]", rowId: "[ROW_ID]") + +// Update row +try await tablesDB.updateRow(databaseId: "[DATABASE_ID]", tableId: "[TABLE_ID]", rowId: "[ROW_ID]", data: ["done": true]) + +// Delete row +try 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. + +```swift +// Create table with explicit string column types +try await tablesDB.createTable( + databaseId: "[DATABASE_ID]", + tableId: ID.unique(), + name: "articles", + columns: [ + ["key": "title", "type": "varchar", "size": 255, "required": true], + ["key": "summary", "type": "text", "required": false], + ["key": "body", "type": "mediumtext", "required": false], + ["key": "raw_data", "type": "longtext", "required": false], + ] +) +``` + +### Query Methods + +```swift +// Filtering +Query.equal("field", value: "value") // == (or pass array for IN) +Query.notEqual("field", value: "value") // != +Query.lessThan("field", value: 100) // < +Query.lessThanEqual("field", value: 100) // <= +Query.greaterThan("field", value: 100) // > +Query.greaterThanEqual("field", value: 100) // >= +Query.between("field", start: 1, end: 100) // 1 <= field <= 100 +Query.isNull("field") // is null +Query.isNotNull("field") // is not null +Query.startsWith("field", value: "prefix") // starts with +Query.endsWith("field", value: "suffix") // ends with +Query.contains("field", value: "sub") // contains +Query.search("field", value: "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"]) +Query.or([Query.equal("a", value: 1), Query.equal("b", value: 2)]) // OR +Query.and([Query.greaterThan("age", value: 18), Query.lessThan("age", value: 65)]) // AND (default) +``` + +### File Storage + +```swift +let storage = Storage(client) + +// Upload file +let file = try await storage.createFile(bucketId: "[BUCKET_ID]", fileId: ID.unique(), file: InputFile.fromPath("/path/to/file.png")) + +// List files +let files = try await storage.listFiles(bucketId: "[BUCKET_ID]") + +// Delete file +try await storage.deleteFile(bucketId: "[BUCKET_ID]", fileId: "[FILE_ID]") +``` + +#### InputFile Factory Methods + +```swift +InputFile.fromPath("/path/to/file.png") // from filesystem path +InputFile.fromData(data, filename: "file.png", mimeType: "image/png") // from Data +``` + +### Teams + +```swift +let teams = Teams(client) + +// Create team +let team = try await teams.create(teamId: ID.unique(), name: "Engineering") + +// List teams +let list = try await teams.list() + +// Create membership (invite user by email) +let membership = try await teams.createMembership( + teamId: "[TEAM_ID]", + roles: ["editor"], + email: "user@example.com" +) + +// List memberships +let members = try await teams.listMemberships(teamId: "[TEAM_ID]") + +// Update membership roles +try await teams.updateMembership(teamId: "[TEAM_ID]", membershipId: "[MEMBERSHIP_ID]", roles: ["admin"]) + +// Delete team +try 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) + +```swift +let realtime = Realtime(client) + +let subscription = realtime.subscribe(channels: ["databases.[DATABASE_ID].tables.[TABLE_ID].rows"]) { response in + print(response.events) // e.g. ["databases.*.tables.*.rows.*.create"] + print(response.payload) // the affected resource +} + +// Subscribe to multiple channels +let multi = realtime.subscribe(channels: [ + "databases.[DATABASE_ID].tables.[TABLE_ID].rows", + "buckets.[BUCKET_ID].files", +]) { response in /* ... */ } + +// Cleanup +subscription.close() +``` + +**Available channels:** + +| Channel | Description | +|---------|-------------| +| `account` | Changes to the authenticated user's account | +| `databases.[DB_ID].tables.[TABLE_ID].rows` | All rows in a table | +| `databases.[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 | +| `functions.[FUNCTION_ID].executions` | Function execution updates | + +Response fields: `events` (array), `payload` (resource), `channels` (matched), `timestamp` (ISO 8601). + +### Serverless Functions (server-side) + +```swift +let functions = Functions(client) + +// Execute function +let execution = try await functions.createExecution(functionId: "[FUNCTION_ID]", body: "{\"key\": \"value\"}") + +// List executions +let executions = try await functions.listExecutions(functionId: "[FUNCTION_ID]") +``` + +#### Writing a Function Handler (Swift runtime) + +```swift +// Sources/main.swift — Appwrite Function entry point +func main(context: RuntimeContext) async throws -> RuntimeOutput { + // context.req.body — raw body (String) + // context.req.bodyJson — parsed JSON ([String: Any]?) + // context.req.headers — headers ([String: String]) + // context.req.method — HTTP method + // context.req.path — URL path + // context.req.query — query params ([String: String]) + + 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 + // context.res.text("Hello") // plain text + // context.res.empty() // 204 + // context.res.redirect("https://...") // 302 +} +``` + +### Server-Side Rendering (SSR) Authentication + +SSR apps using server-side Swift (Vapor, Hummingbird, etc.) use the **server SDK** 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) + +```swift +import Appwrite + +// Admin client (reusable) +let adminClient = Client() + .setEndpoint("https://.cloud.appwrite.io/v1") + .setProject("[PROJECT_ID]") + .setKey(Environment.get("APPWRITE_API_KEY")!) + +// Session client (create per-request) +let sessionClient = Client() + .setEndpoint("https://.cloud.appwrite.io/v1") + .setProject("[PROJECT_ID]") + +if let session = req.cookies["a_session_[PROJECT_ID]"]?.string { + sessionClient.setSession(session) +} +``` + +#### Email/Password Login (Vapor) + +```swift +app.post("login") { req async throws -> Response in + let body = try req.content.decode(LoginRequest.self) + let account = Account(adminClient) + let session = try await account.createEmailPasswordSession( + email: body.email, + password: body.password + ) + + // Cookie name must be a_session_ + let response = Response(status: .ok, body: .init(string: "{\"success\": true}")) + response.cookies["a_session_[PROJECT_ID]"] = HTTPCookies.Value( + string: session.secret, + isHTTPOnly: true, + isSecure: true, + sameSite: .strict, + path: "/" + ) + return response +} +``` + +#### Authenticated Requests + +```swift +app.get("user") { req async throws -> Response in + guard let session = req.cookies["a_session_[PROJECT_ID]"]?.string else { + throw Abort(.unauthorized) + } + + let sessionClient = Client() + .setEndpoint("https://.cloud.appwrite.io/v1") + .setProject("[PROJECT_ID]") + .setSession(session) + + let account = Account(sessionClient) + let user = try await account.get() + // Return user as JSON +} +``` + +#### OAuth2 SSR Flow + +```swift +// Step 1: Redirect to OAuth provider +app.get("oauth") { req async throws -> Response in + let account = Account(adminClient) + let redirectUrl = try await account.createOAuth2Token( + provider: .github, + success: "https://example.com/oauth/success", + failure: "https://example.com/oauth/failure" + ) + return req.redirect(to: redirectUrl) +} + +// Step 2: Handle callback — exchange token for session +app.get("oauth", "success") { req async throws -> Response in + let userId = try req.query.get(String.self, at: "userId") + let secret = try req.query.get(String.self, at: "secret") + + let account = Account(adminClient) + let session = try await account.createSession(userId: userId, secret: secret) + + let response = Response(status: .ok, body: .init(string: "{\"success\": true}")) + response.cookies["a_session_[PROJECT_ID]"] = HTTPCookies.Value( + string: session.secret, + isHTTPOnly: true, isSecure: true, sameSite: .strict, path: "/" + ) + return response +} +``` + +> **Cookie security:** Always use `isHTTPOnly`, `isSecure`, and `sameSite: .strict` to prevent XSS. The cookie name must be `a_session_`. + +> **Forwarding user agent:** Call `sessionClient.setForwardedUserAgent(req.headers.first(name: .userAgent) ?? "")` to record the end-user's browser info for debugging and security. + +## Error Handling + +```swift +import Appwrite +// AppwriteException is included in the main module + +do { + let row = try await tablesDB.getRow(databaseId: "[DATABASE_ID]", tableId: "[TABLE_ID]", rowId: "[ROW_ID]") +} catch let error as AppwriteException { + print(error.message) // human-readable message + print(error.code) // HTTP status code (Int) + print(error.type) // error type (e.g. "document_not_found") + print(error.response) // full response body +} +``` + +**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 document/file level or inherited from the collection/bucket settings. Permissions are arrays of strings built with the `Permission` and `Role` helpers. + +```swift +import Appwrite +// Permission and Role are included in the main module import +``` + +### Database Row with Permissions + +```swift +let doc = try 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 + +```swift +let file = try await storage.createFile( + bucketId: "[BUCKET_ID]", + fileId: ID.unique(), + file: InputFile.fromPath("/path/to/file.png"), + permissions: [ + Permission.read(Role.any()), + Permission.update(Role.user("[USER_ID]")), + Permission.delete(Role.user("[USER_ID]")), + ] +) +``` + +> **When to set permissions:** Set document/file-level permissions when you need per-resource access control. If all documents in a collection share the same rules, configure permissions at the collection/bucket level and leave document 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 + diff --git a/skills/appwrite-typescript/SKILL.md b/skills/appwrite-typescript/SKILL.md new file mode 100644 index 0000000..544b1dc --- /dev/null +++ b/skills/appwrite-typescript/SKILL.md @@ -0,0 +1,630 @@ +--- +name: appwrite-typescript +description: Appwrite TypeScript SDK skill. Use when building browser-based JavaScript/TypeScript apps, React Native mobile apps, or server-side Node.js/Deno backends with Appwrite. Covers client-side auth (email, OAuth, anonymous), database queries, file uploads, real-time subscriptions, and server-side admin via API keys for user management, database administration, storage, and functions. +--- + + +# Appwrite TypeScript SDK + +## Installation + +```bash +# Web +npm install appwrite + +# React Native +npm install react-native-appwrite + +# Node.js / Deno +npm install node-appwrite +``` + +## Setting Up the Client + +### Client-side (Web / React Native) + +```typescript +// Web +import { Client, Account, TablesDB, Storage, ID, Query } from 'appwrite'; + +// React Native +import { Client, Account, TablesDB, Storage, ID, Query } from 'react-native-appwrite'; + +const client = new Client() + .setEndpoint('https://.cloud.appwrite.io/v1') + .setProject('[PROJECT_ID]'); +``` + +### Server-side (Node.js / Deno) + +```typescript +import { Client, Users, TablesDB, Storage, Functions, ID, Query } from 'node-appwrite'; + +const client = new Client() + .setEndpoint('https://.cloud.appwrite.io/v1') + .setProject(process.env.APPWRITE_PROJECT_ID) + .setKey(process.env.APPWRITE_API_KEY); +``` + +## Code Examples + +### Authentication (client-side) + +```typescript +const account = new Account(client); + +// Email signup +await account.create({ + userId: ID.unique(), + email: 'user@example.com', + password: 'password123', + name: 'User Name' +}); + +// Email login +const session = await account.createEmailPasswordSession({ + email: 'user@example.com', + password: 'password123' +}); + +// OAuth login +account.createOAuth2Session({ + provider: 'github', + success: 'https://example.com/success', + failure: 'https://example.com/fail' +}); + +// Get current user +const user = await account.get(); + +// Logout +await account.deleteSession({ sessionId: 'current' }); +``` + +### User Management (server-side) + +```typescript +const users = new Users(client); + +// Create user +const user = await users.create({ + userId: ID.unique(), + email: 'user@example.com', + password: 'password123', + name: 'User Name' +}); + +// List users +const list = await users.list({ queries: [Query.limit(25)] }); + +// Get user +const 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 the object-params calling style (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. + +```typescript +const tablesDB = new TablesDB(client); + +// Create database (server-side only) +const db = await tablesDB.create({ databaseId: ID.unique(), name: 'My Database' }); + +// Create table (server-side only) +const col = await tablesDB.createTable({ + databaseId: '[DATABASE_ID]', + tableId: ID.unique(), + name: 'My Table' +}); + +// Create row +const doc = await tablesDB.createRow({ + databaseId: '[DATABASE_ID]', + tableId: '[TABLE_ID]', + rowId: ID.unique(), + data: { title: 'Hello World', content: 'Example content' } +}); + +// List rows with query +const results = await tablesDB.listRows({ + databaseId: '[DATABASE_ID]', + tableId: '[TABLE_ID]', + queries: [Query.equal('status', 'active'), Query.limit(10)] +}); + +// Get row +const 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: { title: 'Updated Title' } +}); + +// 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. + +```typescript +// 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 + ] +}); +``` + +#### TypeScript Generics + +```typescript +import { Models } from 'appwrite'; +// Server-side: import from 'node-appwrite' + +// Define a typed interface for your row data +interface Todo { + title: string; + done: boolean; + priority: number; +} + +// listRows returns Models.DocumentList by default +// Cast or use generics for typed results +const results = await tablesDB.listRows({ + databaseId: '[DATABASE_ID]', + tableId: '[TABLE_ID]', + queries: [Query.equal('done', false)] +}); + +// Each document includes built-in fields alongside your data +const doc = results.documents[0]; +doc.$id; // string — unique row ID +doc.$createdAt; // string — ISO 8601 creation timestamp +doc.$updatedAt; // string — ISO 8601 update timestamp +doc.$permissions; // string[] — permission strings +doc.$databaseId; // string +doc.$collectionId; // string + +// Common model types +// Models.User — user account +// Models.Session — auth session +// Models.File — storage file metadata +// Models.Team — team object +// Models.Execution — function execution result +// Models.DocumentList — paginated list with total count +``` + +### Query Methods + +```typescript +// Filtering +Query.equal('field', 'value') // field == value (or pass array for IN) +Query.notEqual('field', 'value') // field != value +Query.lessThan('field', 100) // field < value +Query.lessThanEqual('field', 100) // field <= value +Query.greaterThan('field', 100) // field > value +Query.greaterThanEqual('field', 100) // field >= value +Query.between('field', 1, 100) // 1 <= field <= 100 +Query.isNull('field') // field is null +Query.isNotNull('field') // field is not null +Query.startsWith('field', 'prefix') // string starts with prefix +Query.endsWith('field', 'suffix') // string ends with suffix +Query.contains('field', 'substring') // string/array contains value +Query.search('field', 'keywords') // full-text search (requires full-text index) + +// Sorting +Query.orderAsc('field') // sort ascending +Query.orderDesc('field') // sort descending + +// Pagination +Query.limit(25) // max rows returned (default 25, max 100) +Query.offset(0) // skip N rows +Query.cursorAfter('[ROW_ID]') // paginate after this row ID (preferred for large datasets) +Query.cursorBefore('[ROW_ID]') // paginate before this row ID + +// Selection +Query.select(['field1', 'field2']) // return only specified fields + +// Logical +Query.or([Query.equal('a', 1), Query.equal('b', 2)]) // OR condition +Query.and([Query.greaterThan('age', 18), Query.lessThan('age', 65)]) // explicit AND (queries are AND by default) +``` + +### File Storage + +```typescript +const storage = new Storage(client); + +// Upload file (client-side — from file input) +const file = await storage.createFile({ + bucketId: '[BUCKET_ID]', + fileId: ID.unique(), + file: document.getElementById('file-input').files[0] +}); + +// Upload file (server-side — from path) +import { InputFile } from 'node-appwrite/file'; + +const file2 = await storage.createFile({ + bucketId: '[BUCKET_ID]', + fileId: ID.unique(), + file: InputFile.fromPath('/path/to/file.png', 'file.png') +}); + +// List files +const files = await storage.listFiles({ bucketId: '[BUCKET_ID]' }); + +// Get file preview (image) +const preview = storage.getFilePreview({ + bucketId: '[BUCKET_ID]', + fileId: '[FILE_ID]', + width: 300, + height: 300 +}); + +// Download file +const download = await storage.getFileDownload({ + bucketId: '[BUCKET_ID]', + fileId: '[FILE_ID]' +}); + +// Delete file +await storage.deleteFile({ bucketId: '[BUCKET_ID]', fileId: '[FILE_ID]' }); +``` + +#### InputFile Factory Methods (server-side) + +```typescript +import { InputFile } from 'node-appwrite/file'; + +InputFile.fromPath('/path/to/file.png', 'file.png') // from filesystem path +InputFile.fromBuffer(buffer, 'file.png') // from Buffer +InputFile.fromStream(readableStream, 'file.png', size) // from ReadableStream (size in bytes required) +InputFile.fromPlainText('Hello world', 'hello.txt') // from string content +``` + +### Teams + +```typescript +const teams = new Teams(client); + +// Create team +const team = await teams.create({ teamId: ID.unique(), name: 'Engineering' }); + +// List teams +const list = await teams.list(); + +// Create membership (invite a user by email) +const membership = await teams.createMembership({ + teamId: '[TEAM_ID]', + roles: ['editor'], + email: 'user@example.com', +}); + +// List memberships +const 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) + +```typescript +// Subscribe to row changes +const unsubscribe = client.subscribe('databases.[DATABASE_ID].tables.[TABLE_ID].rows', (response) => { + console.log(response.events); // e.g. ['databases.*.tables.*.rows.*.create'] + console.log(response.payload); // the affected resource +}); + +// Subscribe to file changes +client.subscribe('buckets.[BUCKET_ID].files', (response) => { + console.log(response.payload); +}); + +// Subscribe to multiple channels +client.subscribe([ + 'databases.[DATABASE_ID].tables.[TABLE_ID].rows', + 'buckets.[BUCKET_ID].files', +], (response) => { /* ... */ }); + +// Unsubscribe +unsubscribe(); +``` + +**Available channels:** + +| Channel | Description | +|---------|-------------| +| `account` | Changes to the authenticated user's account | +| `databases.[DB_ID].tables.[TABLE_ID].rows` | All rows in a table | +| `databases.[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]` | Changes to a specific team | +| `memberships` | Changes to the user's team memberships | +| `memberships.[MEMBERSHIP_ID]` | A specific membership | +| `functions.[FUNCTION_ID].executions` | Execution updates for a function | + +The `response` object includes: `events` (array of event strings), `payload` (the affected resource), `channels` (channels matched), and `timestamp` (ISO 8601). + +### Serverless Functions (server-side) + +```typescript +const functions = new Functions(client); + +// Execute function +const execution = await functions.createExecution({ + functionId: '[FUNCTION_ID]', + body: JSON.stringify({ key: 'value' }) +}); + +// List executions +const executions = await functions.listExecutions({ functionId: '[FUNCTION_ID]' }); +``` + +#### Writing a Function Handler (Node.js runtime) + +When deploying your own Appwrite Function, the entry point file must export a default async function: + +```typescript +// src/main.js (or src/main.ts) +export default async ({ req, res, log, error }) => { + // Request properties + // req.body — raw request body (string) + // req.bodyJson — parsed JSON body (object, or undefined if not JSON) + // req.headers — request headers (object) + // req.method — HTTP method (GET, POST, PUT, DELETE, PATCH) + // req.path — URL path (e.g. '/hello') + // req.query — parsed query parameters (object) + // req.queryString — raw query string + + log('Processing request: ' + req.method + ' ' + req.path); + + if (req.method === 'GET') { + return res.json({ message: 'Hello from Appwrite Function!' }); + } + + const data = req.bodyJson; + if (!data?.name) { + error('Missing name field'); + return res.json({ error: 'Name is required' }, 400); + } + + // Response methods + return res.json({ success: true }); // JSON (sets Content-Type automatically) + // return res.text('Hello'); // plain text + // return res.empty(); // 204 No Content + // return res.redirect('https://example.com'); // 302 Redirect + // return res.send('data', 200, { 'X-Custom': '1' }); // custom body, status, headers +}; +``` + +### Server-Side Rendering (SSR) Authentication + +SSR apps (Next.js, SvelteKit, Nuxt, Remix, Astro) use the **server SDK** (`node-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) + +```typescript +import { Client, Account, OAuthProvider } from 'node-appwrite'; + +// Admin client (reusable) +const adminClient = new Client() + .setEndpoint('https://.cloud.appwrite.io/v1') + .setProject('[PROJECT_ID]') + .setKey(process.env.APPWRITE_API_KEY); + +// Session client (create per-request) +const sessionClient = new Client() + .setEndpoint('https://.cloud.appwrite.io/v1') + .setProject('[PROJECT_ID]'); + +const session = req.cookies['a_session_[PROJECT_ID]']; +if (session) { + sessionClient.setSession(session); +} +``` + +#### Email/Password Login + +```typescript +app.post('/login', async (req, res) => { + const account = new Account(adminClient); + const session = await account.createEmailPasswordSession({ + email: req.body.email, + password: req.body.password, + }); + + // Cookie name must be a_session_ + res.cookie('a_session_[PROJECT_ID]', session.secret, { + httpOnly: true, + secure: true, + sameSite: 'strict', + expires: new Date(session.expire), + path: '/', + }); + + res.json({ success: true }); +}); +``` + +#### Authenticated Requests + +```typescript +app.get('/user', async (req, res) => { + const session = req.cookies['a_session_[PROJECT_ID]']; + if (!session) return res.status(401).json({ error: 'Unauthorized' }); + + // Create a fresh session client per request + const sessionClient = new Client() + .setEndpoint('https://.cloud.appwrite.io/v1') + .setProject('[PROJECT_ID]') + .setSession(session); + + const account = new Account(sessionClient); + const user = await account.get(); + res.json(user); +}); +``` + +#### OAuth2 SSR Flow + +```typescript +// Step 1: Redirect to OAuth provider +app.get('/oauth', async (req, res) => { + const account = new Account(adminClient); + const redirectUrl = await account.createOAuth2Token({ + provider: OAuthProvider.Github, + success: 'https://example.com/oauth/success', + failure: 'https://example.com/oauth/failure', + }); + res.redirect(redirectUrl); +}); + +// Step 2: Handle callback — exchange token for session +app.get('/oauth/success', async (req, res) => { + const account = new Account(adminClient); + const session = await account.createSession({ + userId: req.query.userId, + secret: req.query.secret, + }); + + res.cookie('a_session_[PROJECT_ID]', session.secret, { + httpOnly: true, secure: true, sameSite: 'strict', + expires: new Date(session.expire), path: '/', + }); + res.json({ success: true }); +}); +``` + +> **Cookie security:** Always use `httpOnly`, `secure`, and `sameSite: 'strict'` to prevent XSS. The cookie name must be `a_session_`. + +> **Forwarding user agent:** Call `sessionClient.setForwardedUserAgent(req.headers['user-agent'])` to record the end-user's browser info for debugging and security. + +## Error Handling + +```typescript +import { AppwriteException } from 'appwrite'; +// Server-side: import from 'node-appwrite' + +try { + const doc = await tablesDB.getRow({ + databaseId: '[DATABASE_ID]', + tableId: '[TABLE_ID]', + rowId: '[ROW_ID]', + }); +} catch (err) { + if (err instanceof AppwriteException) { + console.log(err.message); // human-readable error message + console.log(err.code); // HTTP status code (number) + console.log(err.type); // Appwrite error type string (e.g. 'document_not_found') + console.log(err.response); // full response body (object) + } +} +``` + +**Common error codes:** + +| Code | Meaning | +|------|---------| +| `401` | Unauthorized — missing or invalid session/API key | +| `403` | Forbidden — insufficient permissions for this action | +| `404` | Not found — resource does not exist | +| `409` | Conflict — duplicate ID or unique constraint violation | +| `429` | Rate limited — too many requests, retry after backoff | + +## 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 document/file level or inherited from the collection/bucket settings. Permissions are arrays of strings built with the `Permission` and `Role` helpers. + +```typescript +import { Permission, Role } from 'appwrite'; +// Server-side: import from 'node-appwrite' +``` + +### Database Row with Permissions + +```typescript +const 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 + +```typescript +const file = await storage.createFile({ + bucketId: '[BUCKET_ID]', + fileId: ID.unique(), + file: document.getElementById('file-input').files[0], + permissions: [ + Permission.read(Role.any()), + Permission.update(Role.user('[USER_ID]')), + Permission.delete(Role.user('[USER_ID]')), + ] +}); +``` + +> **When to set permissions:** Set document/file-level permissions when you need per-resource access control. If all documents in a collection share the same rules, configure permissions at the collection/bucket level and leave document 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 + From b4674204a9727b279ff3916eec463e2f3af0aa36 Mon Sep 17 00:00:00 2001 From: root Date: Mon, 23 Feb 2026 06:11:03 +0000 Subject: [PATCH 2/3] changes --- .cursor-plugin/plugin.json | 2 +- README.md | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.cursor-plugin/plugin.json b/.cursor-plugin/plugin.json index 1a3c579..1ba04a3 100644 --- a/.cursor-plugin/plugin.json +++ b/.cursor-plugin/plugin.json @@ -1,7 +1,7 @@ { "name": "appwrite-plugin", "version": "0.1.0", - "description": "Appwrite is an open-source backend as a service server that abstracts and simplifies complex and repetitive development tasks behind a very simple to use REST API. Appwrite aims to help you develop your apps faster and in a more secure way. Use the CursorPlugin SDK to integrate your app with the Appwrite server to easily start interacting with all of Appwrite backend APIs and tools. For full API documentation and tutorials go to [https://appwrite.io/docs](https://appwrite.io/docs)", + "description": "The Appwrite plugin for Cursor includes skills and MCP servers, allowing AI agents to access your projects and correctly integrate with your projects.", "author": { "name": "Appwrite", "email": "team@appwrite.io" diff --git a/README.md b/README.md index 96b0aed..5e60a80 100644 --- a/README.md +++ b/README.md @@ -1,11 +1,11 @@ -# Appwrite CursorPlugin +# Appwrite Cursor Plugin ![License](https://img.shields.io/github/license/appwrite/cursor-plugin.svg?style=flat-square) ![Version](https://img.shields.io/badge/api%20version-1.8.1-blue.svg?style=flat-square) [![Twitter Account](https://img.shields.io/twitter/follow/appwrite?color=00acee&label=twitter&style=flat-square)](https://twitter.com/appwrite) [![Discord](https://img.shields.io/discord/564160730845151244?label=discord&style=flat-square)](https://appwrite.io/discord) -**This SDK is compatible with Appwrite server version 1.8.x. For older versions, please check [previous releases](https://github.com/appwrite/cursor-plugin.git/releases).** +**This SDK is compatible with Appwrite server version latest. For older versions, please check [previous releases](https://github.com/appwrite/cursor-plugin.git/releases).** Appwrite is an open-source backend as a service server that abstracts and simplifies complex and repetitive development tasks behind a very simple to use REST API. Appwrite aims to help you develop your apps faster and in a more secure way. Use the CursorPlugin SDK to integrate your app with the Appwrite server to easily start interacting with all of Appwrite backend APIs and tools. For full API documentation and tutorials go to [https://appwrite.io/docs](https://appwrite.io/docs) From 6ea839ba338d2591a78d278b0c01555b758fb1b5 Mon Sep 17 00:00:00 2001 From: root Date: Mon, 23 Feb 2026 06:36:56 +0000 Subject: [PATCH 3/3] changes --- .cursor-plugin/plugin.json | 2 +- LICENSE | 25 +++++++++++++++++-------- 2 files changed, 18 insertions(+), 9 deletions(-) diff --git a/.cursor-plugin/plugin.json b/.cursor-plugin/plugin.json index 1ba04a3..f6135e4 100644 --- a/.cursor-plugin/plugin.json +++ b/.cursor-plugin/plugin.json @@ -7,5 +7,5 @@ "email": "team@appwrite.io" }, "repository": "https://github.com/appwrite/cursor-plugin", - "logo": "https://github.com/appwrite/appwrite/raw/main/public/images/github.png" + "logo": "https://appwrite.io/images/logos/logo.svg" } diff --git a/LICENSE b/LICENSE index 6b6252a..989ddf8 100644 --- a/LICENSE +++ b/LICENSE @@ -1,12 +1,21 @@ -Copyright (c) 2026 Appwrite (https://appwrite.io) and individual contributors. -All rights reserved. +MIT License -Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: +Copyright (c) 2026 Appwrite - 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: - 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. - 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. - -THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. \ No newline at end of file