appwrite-php — Claude Code skill for the Appwrite PHP SDK
Teaches your agent to write server-side PHP against Appwrite: users, tables, files, functions. Useful in Laravel and Symfony projects.
npx skills add appwrite/skills --skill appwrite-php -a claude-codeWhat appwrite-php does
The skill hands your agent ready-made examples for the Appwrite PHP SDK. It covers client setup with setEndpoint, setProject and setKey, user management, table work through TablesDB, file uploads to Storage, and calling Functions. There is also a reference for Query methods: filters, sorting, cursor pagination, aggregations, joins and a fluent builder.
You need an API key. The client reads it from the APPWRITE_API_KEY environment variable and the project ID from APPWRITE_PROJECT_ID — without the key, requests to the server fail because that is how the SDK authenticates. The package itself installs with composer require appwrite/appwrite.
The skill flags two deprecated pieces: the Databases class and the string column type. New code should use TablesDB and pick varchar, text, mediumtext or longtext instead. A varchar holds up to 16,383 characters and is stored inline, while text and larger types live off-page. If your codebase already runs on Databases, the skill says to leave it alone.
What it helps with
- create a user and assign a team role
- build a query with a filter, sort order and cursor pagination
- upload a file to a bucket and delete it by ID
- pick a column type instead of the deprecated
string - call an Appwrite function from PHP code
Example prompts for the agent
- Set up the Appwrite PHP SDK and create a user with email and password
- Query the articles table with a title filter and a limit of 10
- Upload a file to a bucket with Storage and return its ID
How to install appwrite-php
1Skills CLI (Vercel)
npx skills add appwrite/skills --skill appwrite-php -a claude-code2Manually: put the files in place
~/.claude/skills/appwrite-php/SKILL.mdCopy the whole skill folder; for one project use .claude/skills/appwrite-php/
1Skills CLI (Vercel)
npx skills add appwrite/skills --skill appwrite-php -a codex2Manually: put the files in place
.agents/skills/appwrite-php/SKILL.mdFor all projects use ~/.agents/skills/
1Skills CLI (Vercel)
npx skills add appwrite/skills --skill appwrite-php -a cursor2Manually: put the files in place
.cursor/skills/appwrite-php/SKILL.mdCursor also reads .agents/skills/ and .claude/skills/
1GitHub CLI
gh skill install appwrite/skills appwrite-php2Manually: put the files in place
.github/skills/appwrite-php/SKILL.md1Skills CLI (Vercel)
npx skills add appwrite/skills --skill appwrite-php -a gemini-cli2Platform CLI
gemini skills install https://github.com/appwrite/skills --path skills/appwrite-phpManually: put the files in place
.devin/skills/appwrite-php/SKILL.mdThe legacy .windsurf/skills/ path still works
Skills CLI (Vercel)
npx skills add appwrite/skills --skill appwrite-php -a opencodeClaude.ai
Zip the folder and upload it in Settings → Capabilities → Skills
Step by step, with commands for each platform: How to install skills in Claude Code: five ways
Questions about appwrite-php
How do I install appwrite-php in Claude?
Run npx skills add appwrite/skills --skill appwrite-php -a claude-code. The skill lands in ~/.claude/skills/appwrite-php/SKILL.md.
What does appwrite-php do?
It gives your agent examples for the Appwrite PHP SDK: users, tables, files, teams and functions. Handy for server-side code in Laravel or Symfony.
Which class should I use for the database?
TablesDB. The Databases class is deprecated — keep it only if your existing code already depends on it.
Source text
SKILL.md · BSD-3-Clause
---
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://<REGION>.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(), '[email protected]', 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)
Query::relationship('comments', [Query::equal('approved', [true]), Query::limit(25)]) // related documents
// Aggregations (optional alias)
Query::count('*', 'total')
Query::sum('amount', 'revenue')
Query::avg('score', 'avgScore')
Query::min('price', 'minPrice')
Query::max('price', 'maxPrice')
// Grouping
Query::groupBy(['status', 'year'])
Query::having([Query::greaterThan('total', 1)])
Query::distinct()
// Joins (table id, left key, right key, operator, alias)
Query::join('orders', '$id', 'customerId')
Query::leftJoin('orders', '$id', 'customerId', '=', 'ord')
// Nested join ON: extra predicates stay on the JOIN, not WHERE
Query::leftJoin('orders', 'ord', [
Query::on('$id', 'customerId'),
Query::equal('ord.status', 'paid'),
])
// Fluent builder. Pass build() into list methods.
$queries = Query::builder()
->equal('status', 'published')
->leftJoin('orders', '$id', 'customerId', '=', 'ord')
->count('*', 'total')
->groupBy(['status'])
->limit(25)
->build();
$tablesDB->listRows('[DATABASE_ID]', '[TABLE_ID]', $queries);
```
### 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: '[email protected]');
// 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://<REGION>.cloud.appwrite.io/v1')
->setProject('[PROJECT_ID]')
->setKey(getenv('APPWRITE_API_KEY'));
// Session client (create per-request)
$sessionClient = (new Client())
->setEndpoint('https://<REGION>.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_<PROJECT_ID>
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_<PROJECT_ID>`.
> **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 row/file level or inherited from the table/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 row/file-level permissions when you need per-resource access control. If all rows in a table share the same rules, configure permissions at the table/bucket level and leave row permissions empty.
> **Common mistakes:**
> - **Forgetting permissions** — the resource becomes inaccessible to all users (including the creator)
> - **`Role::any()` with `write`/`update`/`delete`** — allows any user, including unauthenticated guests, to modify or remove the resource
> - **`Permission::read(Role::any())` on sensitive data** — makes the resource publicly readable
© appwrite · BSD-3-Clause · appwrite/skills