| 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
composer require appwrite/appwrite
Setting Up the Client
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
$users = new Users($client);
$user = $users->create(ID::unique(), 'user@example.com', null, 'password123', 'User Name');
$list = $users->list([Query::limit(25)]);
$fetched = $users->get('[USER_ID]');
$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.
$tablesDB = new TablesDB($client);
$db = $tablesDB->create(ID::unique(), 'My Database');
$doc = $tablesDB->createRow('[DATABASE_ID]', '[TABLE_ID]', ID::unique(), [
'title' => 'Hello World'
]);
$results = $tablesDB->listRows('[DATABASE_ID]', '[TABLE_ID]', [
Query::equal('title', ['Hello World']),
Query::limit(10)
]);
$row = $tablesDB->getRow('[DATABASE_ID]', '[TABLE_ID]', '[ROW_ID]');
$tablesDB->updateRow('[DATABASE_ID]', '[TABLE_ID]', '[ROW_ID]', [
'title' => 'Updated'
]);
$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.
$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
Query::equal('field', ['value'])
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)
Query::isNull('field')
Query::isNotNull('field')
Query::startsWith('field', 'prefix')
Query::endsWith('field', 'suffix')
Query::contains('field', ['sub'])
Query::search('field', 'keywords')
Query::orderAsc('field')
Query::orderDesc('field')
Query::limit(25)
Query::offset(0)
Query::cursorAfter('[ROW_ID]')
Query::cursorBefore('[ROW_ID]')
Query::select(['field1', 'field2'])
Query::or([Query::equal('a', [1]), Query::equal('b', [2])])
Query::and([Query::greaterThan('age', 18), Query::lessThan('age', 65)])
File Storage
$storage = new Storage($client);
$file = $storage->createFile('[BUCKET_ID]', ID::unique(), InputFile::withPath('/path/to/file.png'));
$files = $storage->listFiles('[BUCKET_ID]');
$storage->deleteFile('[BUCKET_ID]', '[FILE_ID]');
InputFile Factory Methods
use Appwrite\InputFile;
InputFile::withPath('/path/to/file.png')
InputFile::withData('Hello world', 'hello.txt')
Teams
$teams = new Teams($client);
$team = $teams->create(ID::unique(), 'Engineering');
$list = $teams->list();
$membership = $teams->createMembership('[TEAM_ID]', ['editor'], email: 'user@example.com');
$members = $teams->listMemberships('[TEAM_ID]');
$teams->updateMembership('[TEAM_ID]', '[MEMBERSHIP_ID]', ['admin']);
$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
$functions = new Functions($client);
$execution = $functions->createExecution('[FUNCTION_ID]', '{"key": "value"}');
$executions = $functions->listExecutions('[FUNCTION_ID]');
Writing a Function Handler (PHP runtime)
return function ($context) {
$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]);
};
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)
use Appwrite\Client;
use Appwrite\Services\Account;
$adminClient = (new Client())
->setEndpoint('https://<REGION>.cloud.appwrite.io/v1')
->setProject('[PROJECT_ID]')
->setKey(getenv('APPWRITE_API_KEY'));
$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
$account = new Account($adminClient);
$session = $account->createEmailPasswordSession($email, $password);
setcookie('a_session_[PROJECT_ID]', $session['secret'], [
'httpOnly' => true,
'secure' => true,
'sameSite' => 'strict',
'expires' => strtotime($session['expire']),
'path' => '/',
]);
Authenticated Requests
$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
$account = new Account($adminClient);
$redirectUrl = $account->createOAuth2Token(
OAuthProvider::GITHUB(),
'https://example.com/oauth/success',
'https://example.com/oauth/failure',
);
header('Location: ' . $redirectUrl);
$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
use Appwrite\AppwriteException;
try {
$row = $tablesDB->getRow('[DATABASE_ID]', '[TABLE_ID]', '[ROW_ID]');
} catch (AppwriteException $e) {
echo $e->getMessage();
echo $e->getCode();
echo $e->getType();
echo $e->getResponse();
}
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.
use Appwrite\Permission;
use Appwrite\Role;
Database Row with Permissions
$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
$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