--- 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 (Web) account.createOAuth2Session({ provider: OAuthProvider.Github, success: 'https://example.com/success', failure: 'https://example.com/fail', scopes: ['repo', 'user'] // optional — provider-specific scopes }); // Get current user const user = await account.get(); // Logout await account.deleteSession({ sessionId: 'current' }); ``` ### OAuth 2 Login (React Native) > **Important:** `createOAuth2Session()` does **not** work on React Native. You must use `createOAuth2Token()` with deep linking instead. #### Setup Install the required dependencies: ```bash npx expo install react-native-appwrite react-native-url-polyfill npm install expo-auth-session expo-web-browser expo-linking ``` Set the URL scheme in your `app.json`: ```json { "expo": { "scheme": "appwrite-callback-[PROJECT_ID]" } } ``` #### OAuth Flow ```typescript import { Client, Account, OAuthProvider } from 'react-native-appwrite'; import { makeRedirectUri } from 'expo-auth-session'; import * as WebBrowser from 'expo-web-browser'; const client = new Client() .setEndpoint('https://.cloud.appwrite.io/v1') .setProject('[PROJECT_ID]'); const account = new Account(client); async function oauthLogin(provider: OAuthProvider) { // Create deep link that works across Expo environments const deepLink = new URL(makeRedirectUri({ preferLocalhost: true })); const scheme = `${deepLink.protocol}//`; // e.g. 'exp://' or 'appwrite-callback-[PROJECT_ID]://' // Get the OAuth login URL const loginUrl = await account.createOAuth2Token({ provider, success: `${deepLink}`, failure: `${deepLink}`, }); // Open browser and listen for the scheme redirect const result = await WebBrowser.openAuthSessionAsync(`${loginUrl}`, scheme); if (result.type !== 'success') return; // Extract credentials from the redirect URL const url = new URL(result.url); const secret = url.searchParams.get('secret'); const userId = url.searchParams.get('userId'); // Create session with the OAuth credentials await account.createSession({ userId, secret }); } // Usage await oauthLogin(OAuthProvider.Github); await oauthLogin(OAuthProvider.Google); ``` ### 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 import { Realtime, Channel } from 'appwrite'; const realtime = new Realtime(client); // Subscribe to row changes const subscription = await realtime.subscribe( Channel.tablesdb('[DATABASE_ID]').table('[TABLE_ID]').row(), (response) => { console.log(response.events); // e.g. ['tablesdb.*.tables.*.rows.*.create'] console.log(response.payload); // the affected resource } ); // Subscribe to a specific row await realtime.subscribe( Channel.tablesdb('[DATABASE_ID]').table('[TABLE_ID]').row('[ROW_ID]'), (response) => { /* ... */ } ); // Subscribe to multiple channels await realtime.subscribe([ Channel.tablesdb('[DATABASE_ID]').table('[TABLE_ID]').row(), Channel.bucket('[BUCKET_ID]').file(), ], (response) => { /* ... */ }); // Unsubscribe await subscription.close(); ``` **Available channels:** | Channel | Description | |---------|-------------| | `account` | Changes to the authenticated user's account | | `tablesdb.[DB_ID].tables.[TABLE_ID].rows` | All rows in a table | | `tablesdb.[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 row/file level or inherited from the table/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 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