Claude Agent Skill · by Appwrite

Appwrite Typescript

Install Appwrite Typescript skill for Claude Code from appwrite/agent-skills.

Install
Terminal · npx
$npx skills add https://github.com/vercel-labs/agent-skills --skill vercel-react-best-practices
Works with Paperclip

How Appwrite Typescript fits into a Paperclip company.

Appwrite Typescript drops into any Paperclip agent that handles this kind of work. Assign it to a specialist inside a pre-configured PaperclipOrg company and the skill becomes available on every heartbeat — no prompt engineering, no tool wiring.

S
SaaS FactoryPaired

Pre-configured AI company — 18 agents, 18 skills, one-time purchase.

$27$59
Explore pack
Source file
SKILL.md705 lines
Expand
---name: appwrite-typescriptdescription: 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# Webnpm install appwrite # React Nativenpm install react-native-appwrite # Node.js / Denonpm install node-appwrite``` ## Setting Up the Client ### Client-side (Web / React Native) ```typescript// Webimport { Client, Account, TablesDB, Storage, ID, Query } from 'appwrite'; // React Nativeimport { Client, Account, TablesDB, Storage, ID, Query } from 'react-native-appwrite'; const client = new Client()    .setEndpoint('https://<REGION>.cloud.appwrite.io/v1')    .setProject('[PROJECT_ID]');``` ### Server-side (Node.js / Deno) ```typescriptimport { Client, Users, TablesDB, Storage, Functions, ID, Query } from 'node-appwrite'; const client = new Client()    .setEndpoint('https://<REGION>.cloud.appwrite.io/v1')    .setProject(process.env.APPWRITE_PROJECT_ID)    .setKey(process.env.APPWRITE_API_KEY);``` ## Code Examples ### Authentication (client-side) ```typescriptconst account = new Account(client); // Email signupawait account.create({    userId: ID.unique(),    email: 'user@example.com',    password: 'password123',    name: 'User Name'}); // Email loginconst 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 userconst user = await account.get(); // Logoutawait 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: ```bashnpx expo install react-native-appwrite react-native-url-polyfillnpm 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 ```typescriptimport { 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://<REGION>.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 });} // Usageawait oauthLogin(OAuthProvider.Github);await oauthLogin(OAuthProvider.Google);``` ### User Management (server-side) ```typescriptconst users = new Users(client); // Create userconst user = await users.create({    userId: ID.unique(),    email: 'user@example.com',    password: 'password123',    name: 'User Name'}); // List usersconst list = await users.list({ queries: [Query.limit(25)] }); // Get userconst fetched = await users.get({ userId: '[USER_ID]' }); // Delete userawait 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. ```typescriptconst 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 rowconst doc = await tablesDB.createRow({    databaseId: '[DATABASE_ID]',    tableId: '[TABLE_ID]',    rowId: ID.unique(),    data: { title: 'Hello World', content: 'Example content' }}); // List rows with queryconst results = await tablesDB.listRows({    databaseId: '[DATABASE_ID]',    tableId: '[TABLE_ID]',    queries: [Query.equal('status', 'active'), Query.limit(10)]}); // Get rowconst row = await tablesDB.getRow({    databaseId: '[DATABASE_ID]',    tableId: '[TABLE_ID]',    rowId: '[ROW_ID]'}); // Update rowawait tablesDB.updateRow({    databaseId: '[DATABASE_ID]',    tableId: '[TABLE_ID]',    rowId: '[ROW_ID]',    data: { title: 'Updated Title' }}); // Delete rowawait 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 typesawait 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 ```typescriptimport { Models } from 'appwrite';// Server-side: import from 'node-appwrite' // Define a typed interface for your row datainterface Todo {    title: string;    done: boolean;    priority: number;} // listRows returns Models.DocumentList<Models.Document> by default// Cast or use generics for typed resultsconst results = await tablesDB.listRows({    databaseId: '[DATABASE_ID]',    tableId: '[TABLE_ID]',    queries: [Query.equal('done', false)]}); // Each document includes built-in fields alongside your dataconst doc = results.documents[0];doc.$id;            // string — unique row IDdoc.$createdAt;     // string — ISO 8601 creation timestampdoc.$updatedAt;     // string — ISO 8601 update timestampdoc.$permissions;   // string[] — permission stringsdoc.$databaseId;    // stringdoc.$collectionId;  // string // Common model types// Models.User<Preferences>  — user account// Models.Session             — auth session// Models.File                — storage file metadata// Models.Team                — team object// Models.Execution           — function execution result// Models.DocumentList<T>     — paginated list with total count``` ### Query Methods ```typescript// FilteringQuery.equal('field', 'value')           // field == value (or pass array for IN)Query.notEqual('field', 'value')        // field != valueQuery.lessThan('field', 100)            // field < valueQuery.lessThanEqual('field', 100)       // field <= valueQuery.greaterThan('field', 100)         // field > valueQuery.greaterThanEqual('field', 100)    // field >= valueQuery.between('field', 1, 100)          // 1 <= field <= 100Query.isNull('field')                   // field is nullQuery.isNotNull('field')                // field is not nullQuery.startsWith('field', 'prefix')     // string starts with prefixQuery.endsWith('field', 'suffix')       // string ends with suffixQuery.contains('field', 'substring')    // string/array contains valueQuery.search('field', 'keywords')       // full-text search (requires full-text index) // SortingQuery.orderAsc('field')                 // sort ascendingQuery.orderDesc('field')                // sort descending // PaginationQuery.limit(25)                         // max rows returned (default 25, max 100)Query.offset(0)                         // skip N rowsQuery.cursorAfter('[ROW_ID]')           // paginate after this row ID (preferred for large datasets)Query.cursorBefore('[ROW_ID]')          // paginate before this row ID // SelectionQuery.select(['field1', 'field2'])      // return only specified fields // LogicalQuery.or([Query.equal('a', 1), Query.equal('b', 2)])   // OR conditionQuery.and([Query.greaterThan('age', 18), Query.lessThan('age', 65)])  // explicit AND (queries are AND by default)``` ### File Storage ```typescriptconst 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 filesconst 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 fileconst download = await storage.getFileDownload({    bucketId: '[BUCKET_ID]',    fileId: '[FILE_ID]'}); // Delete fileawait storage.deleteFile({ bucketId: '[BUCKET_ID]', fileId: '[FILE_ID]' });``` #### InputFile Factory Methods (server-side) ```typescriptimport { InputFile } from 'node-appwrite/file'; InputFile.fromPath('/path/to/file.png', 'file.png')          // from filesystem pathInputFile.fromBuffer(buffer, 'file.png')                       // from BufferInputFile.fromStream(readableStream, 'file.png', size)         // from ReadableStream (size in bytes required)InputFile.fromPlainText('Hello world', 'hello.txt')            // from string content``` ### Teams ```typescriptconst teams = new Teams(client); // Create teamconst team = await teams.create({ teamId: ID.unique(), name: 'Engineering' }); // List teamsconst 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 membershipsconst members = await teams.listMemberships({ teamId: '[TEAM_ID]' }); // Update membership rolesawait teams.updateMembership({    teamId: '[TEAM_ID]',    membershipId: '[MEMBERSHIP_ID]',    roles: ['admin'],}); // Delete teamawait 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) ```typescriptimport { Realtime, Channel } from 'appwrite'; const realtime = new Realtime(client); // Subscribe to row changesconst 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 rowawait realtime.subscribe(    Channel.tablesdb('[DATABASE_ID]').table('[TABLE_ID]').row('[ROW_ID]'),    (response) => { /* ... */ }); // Subscribe to multiple channelsawait realtime.subscribe([    Channel.tablesdb('[DATABASE_ID]').table('[TABLE_ID]').row(),    Channel.bucket('[BUCKET_ID]').file(),], (response) => { /* ... */ }); // Unsubscribeawait 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) ```typescriptconst functions = new Functions(client); // Execute functionconst execution = await functions.createExecution({    functionId: '[FUNCTION_ID]',    body: JSON.stringify({ key: 'value' })}); // List executionsconst 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) ```typescriptimport { Client, Account, OAuthProvider } from 'node-appwrite'; // Admin client (reusable)const adminClient = new Client()    .setEndpoint('https://<REGION>.cloud.appwrite.io/v1')    .setProject('[PROJECT_ID]')    .setKey(process.env.APPWRITE_API_KEY); // Session client (create per-request)const sessionClient = new Client()    .setEndpoint('https://<REGION>.cloud.appwrite.io/v1')    .setProject('[PROJECT_ID]'); const session = req.cookies['a_session_[PROJECT_ID]'];if (session) {    sessionClient.setSession(session);}``` #### Email/Password Login ```typescriptapp.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_<PROJECT_ID>    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 ```typescriptapp.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://<REGION>.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 providerapp.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 sessionapp.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_<PROJECT_ID>`. > **Forwarding user agent:** Call `sessionClient.setForwardedUserAgent(req.headers['user-agent'])` to record the end-user's browser info for debugging and security. ## Error Handling ```typescriptimport { 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. ```typescriptimport { Permission, Role } from 'appwrite';// Server-side: import from 'node-appwrite'``` ### Database Row with Permissions ```typescriptconst 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 ```typescriptconst 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