← AppwriteCONTENT HISTORY

Update to Appwrite

Snapshot Sep 30, 2026 · 23:07 UTC · version 1.0.0

Collection source: not recorded for this historical snapshot.

WHAT CHANGED · RULE-BASED ANALYSIS

First saved snapshot

No earlier snapshot is available to establish a change.

Compare saved observations

Download comparison JSON
Full technical diff · 0 changed fields
Full snapshot data
{
  "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.",
  "included_files": [],
  "skill_md_contents": "---\r\nname: appwrite-typescript\r\ndescription: 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.\r\n---\r\n\r\n\r\n# Appwrite TypeScript SDK\r\n\r\n## Installation\r\n\r\n```bash\r\n# Web\r\nnpm install appwrite\r\n\r\n# React Native\r\nnpm install react-native-appwrite\r\n\r\n# Node.js / Deno\r\nnpm install node-appwrite\r\n```\r\n\r\n## Setting Up the Client\r\n\r\n### Client-side (Web / React Native)\r\n\r\n```typescript\r\n// Web\r\nimport { Client, Account, TablesDB, Storage, ID, Query } from 'appwrite';\r\n\r\n// React Native\r\nimport { Client, Account, TablesDB, Storage, ID, Query } from 'react-native-appwrite';\r\n\r\nconst client = new Client()\r\n    .setEndpoint('https://<REGION>.cloud.appwrite.io/v1')\r\n    .setProject('[PROJECT_ID]');\r\n```\r\n\r\n### Server-side (Node.js / Deno)\r\n\r\n```typescript\r\nimport { Client, Users, TablesDB, Storage, Functions, ID, Query } from 'node-appwrite';\r\n\r\nconst client = new Client()\r\n    .setEndpoint('https://<REGION>.cloud.appwrite.io/v1')\r\n    .setProject(process.env.APPWRITE_PROJECT_ID)\r\n    .setKey(process.env.APPWRITE_API_KEY);\r\n```\r\n\r\n## Code Examples\r\n\r\n### Authentication (client-side)\r\n\r\n```typescript\r\nconst account = new Account(client);\r\n\r\n// Email signup\r\nawait account.create({\r\n    userId: ID.unique(),\r\n    email: 'user@example.com',\r\n    password: 'password123',\r\n    name: 'User Name'\r\n});\r\n\r\n// Email login\r\nconst session = await account.createEmailPasswordSession({\r\n    email: 'user@example.com',\r\n    password: 'password123'\r\n});\r\n\r\n// OAuth login (Web)\r\naccount.createOAuth2Session({\r\n    provider: OAuthProvider.Github,\r\n    success: 'https://example.com/success',\r\n    failure: 'https://example.com/fail',\r\n    scopes: ['repo', 'user'] // optional — provider-specific scopes\r\n});\r\n\r\n// Get current user\r\nconst user = await account.get();\r\n\r\n// Logout\r\nawait account.deleteSession({ sessionId: 'current' });\r\n```\r\n\r\n### OAuth 2 Login (React Native)\r\n\r\n> **Important:** `createOAuth2Session()` does **not** work on React Native. You must use `createOAuth2Token()` with deep linking instead.\r\n\r\n#### Setup\r\n\r\nInstall the required dependencies:\r\n\r\n```bash\r\nnpx expo install react-native-appwrite react-native-url-polyfill\r\nnpm install expo-auth-session expo-web-browser expo-linking\r\n```\r\n\r\nSet the URL scheme in your `app.json`:\r\n\r\n```json\r\n{\r\n  \"expo\": {\r\n    \"scheme\": \"appwrite-callback-[PROJECT_ID]\"\r\n  }\r\n}\r\n```\r\n\r\n#### OAuth Flow\r\n\r\n```typescript\r\nimport { Client, Account, OAuthProvider } from 'react-native-appwrite';\r\nimport { makeRedirectUri } from 'expo-auth-session';\r\nimport * as WebBrowser from 'expo-web-browser';\r\n\r\nconst client = new Client()\r\n    .setEndpoint('https://<REGION>.cloud.appwrite.io/v1')\r\n    .setProject('[PROJECT_ID]');\r\n\r\nconst account = new Account(client);\r\n\r\nasync function oauthLogin(provider: OAuthProvider) {\r\n    // Create deep link that works across Expo environments\r\n    const deepLink = new URL(makeRedirectUri({ preferLocalhost: true }));\r\n    const scheme = `${deepLink.protocol}//`; // e.g. 'exp://' or 'appwrite-callback-[PROJECT_ID]://'\r\n\r\n    // Get the OAuth login URL\r\n    const loginUrl = await account.createOAuth2Token({\r\n        provider,\r\n        success: `${deepLink}`,\r\n        failure: `${deepLink}`,\r\n    });\r\n\r\n    // Open browser and listen for the scheme redirect\r\n    const result = await WebBrowser.openAuthSessionAsync(`${loginUrl}`, scheme);\r\n\r\n    if (result.type !== 'success') return;\r\n\r\n    // Extract credentials from the redirect URL\r\n    const url = new URL(result.url);\r\n    const secret = url.searchParams.get('secret');\r\n    const userId = url.searchParams.get('userId');\r\n\r\n    // Create session with the OAuth credentials\r\n    await account.createSession({ userId, secret });\r\n}\r\n\r\n// Usage\r\nawait oauthLogin(OAuthProvider.Github);\r\nawait oauthLogin(OAuthProvider.Google);\r\n```\r\n\r\n### User Management (server-side)\r\n\r\n```typescript\r\nconst users = new Users(client);\r\n\r\n// Create user\r\nconst user = await users.create({\r\n    userId: ID.unique(),\r\n    email: 'user@example.com',\r\n    password: 'password123',\r\n    name: 'User Name'\r\n});\r\n\r\n// List users\r\nconst list = await users.list({ queries: [Query.limit(25)] });\r\n\r\n// Get user\r\nconst fetched = await users.get({ userId: '[USER_ID]' });\r\n\r\n// Delete user\r\nawait users.delete({ userId: '[USER_ID]' });\r\n```\r\n\r\n### Database Operations\r\n\r\n> **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.\r\n>\r\n> **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.\r\n\r\n```typescript\r\nconst tablesDB = new TablesDB(client);\r\n\r\n// Create database (server-side only)\r\nconst db = await tablesDB.create({ databaseId: ID.unique(), name: 'My Database' });\r\n\r\n// Create table (server-side only)\r\nconst col = await tablesDB.createTable({\r\n    databaseId: '[DATABASE_ID]',\r\n    tableId: ID.unique(),\r\n    name: 'My Table'\r\n});\r\n\r\n// Create row\r\nconst doc = await tablesDB.createRow({\r\n    databaseId: '[DATABASE_ID]',\r\n    tableId: '[TABLE_ID]',\r\n    rowId: ID.unique(),\r\n    data: { title: 'Hello World', content: 'Example content' }\r\n});\r\n\r\n// List rows with query\r\nconst results = await tablesDB.listRows({\r\n    databaseId: '[DATABASE_ID]',\r\n    tableId: '[TABLE_ID]',\r\n    queries: [Query.equal('status', 'active'), Query.limit(10)]\r\n});\r\n\r\n// Get row\r\nconst row = await tablesDB.getRow({\r\n    databaseId: '[DATABASE_ID]',\r\n    tableId: '[TABLE_ID]',\r\n    rowId: '[ROW_ID]'\r\n});\r\n\r\n// Update row\r\nawait tablesDB.updateRow({\r\n    databaseId: '[DATABASE_ID]',\r\n    tableId: '[TABLE_ID]',\r\n    rowId: '[ROW_ID]',\r\n    data: { title: 'Updated Title' }\r\n});\r\n\r\n// Delete row\r\nawait tablesDB.deleteRow({\r\n    databaseId: '[DATABASE_ID]',\r\n    tableId: '[TABLE_ID]',\r\n    rowId: '[ROW_ID]'\r\n});\r\n```\r\n\r\n#### String Column Types\r\n\r\n> **Note:** The legacy `string` type is deprecated. Use explicit column types for all new columns.\r\n\r\n| Type | Max characters | Indexing | Storage |\r\n|------|---------------|----------|---------|\r\n| `varchar` | 16,383 | Full index (if size ≤ 768) | Inline in row |\r\n| `text` | 16,383 | Prefix only | Off-page |\r\n| `mediumtext` | 4,194,303 | Prefix only | Off-page |\r\n| `longtext` | 1,073,741,823 | Prefix only | Off-page |\r\n\r\n- `varchar` is stored inline and counts towards the 64 KB row size limit. Prefer for short, indexed fields like names, slugs, or identifiers.\r\n- `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.\r\n\r\n```typescript\r\n// Create table with explicit string column types\r\nawait tablesDB.createTable({\r\n    databaseId: '[DATABASE_ID]',\r\n    tableId: ID.unique(),\r\n    name: 'articles',\r\n    columns: [\r\n        { key: 'title',    type: 'varchar',    size: 255, required: true  },  // inline, fully indexable\r\n        { key: 'summary',  type: 'text',                  required: false },  // off-page, prefix index only\r\n        { key: 'body',     type: 'mediumtext',            required: false },  // up to ~4 M chars\r\n        { key: 'raw_data', type: 'longtext',              required: false },  // up to ~1 B chars\r\n    ]\r\n});\r\n```\r\n\r\n#### TypeScript Generics\r\n\r\n```typescript\r\nimport { Models } from 'appwrite';\r\n// Server-side: import from 'node-appwrite'\r\n\r\n// Define a typed interface for your row data\r\ninterface Todo {\r\n    title: string;\r\n    done: boolean;\r\n    priority: number;\r\n}\r\n\r\n// listRows returns Models.DocumentList<Models.Document> by default\r\n// Cast or use generics for typed results\r\nconst results = await tablesDB.listRows({\r\n    databaseId: '[DATABASE_ID]',\r\n    tableId: '[TABLE_ID]',\r\n    queries: [Query.equal('done', false)]\r\n});\r\n\r\n// Each document includes built-in fields alongside your data\r\nconst doc = results.documents[0];\r\ndoc.$id;            // string — unique row ID\r\ndoc.$createdAt;     // string — ISO 8601 creation timestamp\r\ndoc.$updatedAt;     // string — ISO 8601 update timestamp\r\ndoc.$permissions;   // string[] — permission strings\r\ndoc.$databaseId;    // string\r\ndoc.$collectionId;  // string\r\n\r\n// Common model types\r\n// Models.User<Preferences>  — user account\r\n// Models.Session             — auth session\r\n// Models.File                — storage file metadata\r\n// Models.Team                — team object\r\n// Models.Execution           — function execution result\r\n// Models.DocumentList<T>     — paginated list with total count\r\n```\r\n\r\n### Query Methods\r\n\r\n```typescript\r\n// Filtering\r\nQuery.equal('field', 'value')           // field == value (or pass array for IN)\r\nQuery.notEqual('field', 'value')        // field != value\r\nQuery.lessThan('field', 100)            // field < value\r\nQuery.lessThanEqual('field', 100)       // field <= value\r\nQuery.greaterThan('field', 100)         // field > value\r\nQuery.greaterThanEqual('field', 100)    // field >= value\r\nQuery.between('field', 1, 100)          // 1 <= field <= 100\r\nQuery.isNull('field')                   // field is null\r\nQuery.isNotNull('field')                // field is not null\r\nQuery.startsWith('field', 'prefix')     // string starts with prefix\r\nQuery.endsWith('field', 'suffix')       // string ends with suffix\r\nQuery.contains('field', 'substring')    // string/array contains value\r\nQuery.search('field', 'keywords')       // full-text search (requires full-text index)\r\n\r\n// Sorting\r\nQuery.orderAsc('field')                 // sort ascending\r\nQuery.orderDesc('field')                // sort descending\r\n\r\n// Pagination\r\nQuery.limit(25)                         // max rows returned (default 25, max 100)\r\nQuery.offset(0)                         // skip N rows\r\nQuery.cursorAfter('[ROW_ID]')           // paginate after this row ID (preferred for large datasets)\r\nQuery.cursorBefore('[ROW_ID]')          // paginate before this row ID\r\n\r\n// Selection\r\nQuery.select(['field1', 'field2'])      // return only specified fields\r\n\r\n// Logical\r\nQuery.or([Query.equal('a', 1), Query.equal('b', 2)])   // OR condition\r\nQuery.and([Query.greaterThan('age', 18), Query.lessThan('age', 65)])  // explicit AND (queries are AND by default)\r\n```\r\n\r\n### File Storage\r\n\r\n```typescript\r\nconst storage = new Storage(client);\r\n\r\n// Upload file (client-side — from file input)\r\nconst file = await storage.createFile({\r\n    bucketId: '[BUCKET_ID]',\r\n    fileId: ID.unique(),\r\n    file: document.getElementById('file-input').files[0]\r\n});\r\n\r\n// Upload file (server-side — from path)\r\nimport { InputFile } from 'node-appwrite/file';\r\n\r\nconst file2 = await storage.createFile({\r\n    bucketId: '[BUCKET_ID]',\r\n    fileId: ID.unique(),\r\n    file: InputFile.fromPath('/path/to/file.png', 'file.png')\r\n});\r\n\r\n// List files\r\nconst files = await storage.listFiles({ bucketId: '[BUCKET_ID]' });\r\n\r\n// Get file preview (image)\r\nconst preview = storage.getFilePreview({\r\n    bucketId: '[BUCKET_ID]',\r\n    fileId: '[FILE_ID]',\r\n    width: 300,\r\n    height: 300\r\n});\r\n\r\n// Download file\r\nconst download = await storage.getFileDownload({\r\n    bucketId: '[BUCKET_ID]',\r\n    fileId: '[FILE_ID]'\r\n});\r\n\r\n// Delete file\r\nawait storage.deleteFile({ bucketId: '[BUCKET_ID]', fileId: '[FILE_ID]' });\r\n```\r\n\r\n#### InputFile Factory Methods (server-side)\r\n\r\n```typescript\r\nimport { InputFile } from 'node-appwrite/file';\r\n\r\nInputFile.fromPath('/path/to/file.png', 'file.png')          // from filesystem path\r\nInputFile.fromBuffer(buffer, 'file.png')                       // from Buffer\r\nInputFile.fromStream(readableStream, 'file.png', size)         // from ReadableStream (size in bytes required)\r\nInputFile.fromPlainText('Hello world', 'hello.txt')            // from string content\r\n```\r\n\r\n### Teams\r\n\r\n```typescript\r\nconst teams = new Teams(client);\r\n\r\n// Create team\r\nconst team = await teams.create({ teamId: ID.unique(), name: 'Engineering' });\r\n\r\n// List teams\r\nconst list = await teams.list();\r\n\r\n// Create membership (invite a user by email)\r\nconst membership = await teams.createMembership({\r\n    teamId: '[TEAM_ID]',\r\n    roles: ['editor'],\r\n    email: 'user@example.com',\r\n});\r\n\r\n// List memberships\r\nconst members = await teams.listMemberships({ teamId: '[TEAM_ID]' });\r\n\r\n// Update membership roles\r\nawait teams.updateMembership({\r\n    teamId: '[TEAM_ID]',\r\n    membershipId: '[MEMBERSHIP_ID]',\r\n    roles: ['admin'],\r\n});\r\n\r\n// Delete team\r\nawait teams.delete({ teamId: '[TEAM_ID]' });\r\n```\r\n\r\n> **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.\r\n\r\n### Real-time Subscriptions (client-side)\r\n\r\n```typescript\r\nimport { Realtime, Channel } from 'appwrite';\r\n\r\nconst realtime = new Realtime(client);\r\n\r\n// Subscribe to row changes\r\nconst subscription = await realtime.subscribe(\r\n    Channel.tablesdb('[DATABASE_ID]').table('[TABLE_ID]').row(),\r\n    (response) => {\r\n        console.log(response.events);   // e.g. ['tablesdb.*.tables.*.rows.*.create']\r\n        console.log(response.payload);  // the affected resource\r\n    }\r\n);\r\n\r\n// Subscribe to a specific row\r\nawait realtime.subscribe(\r\n    Channel.tablesdb('[DATABASE_ID]').table('[TABLE_ID]').row('[ROW_ID]'),\r\n    (response) => { /* ... */ }\r\n);\r\n\r\n// Subscribe to multiple channels\r\nawait realtime.subscribe([\r\n    Channel.tablesdb('[DATABASE_ID]').table('[TABLE_ID]').row(),\r\n    Channel.bucket('[BUCKET_ID]').file(),\r\n], (response) => { /* ... */ });\r\n\r\n// Unsubscribe\r\nawait subscription.close();\r\n```\r\n\r\n**Available channels:**\r\n\r\n| Channel | Description |\r\n|---------|-------------|\r\n| `account` | Changes to the authenticated user's account |\r\n| `tablesdb.[DB_ID].tables.[TABLE_ID].rows` | All rows in a table |\r\n| `tablesdb.[DB_ID].tables.[TABLE_ID].rows.[ROW_ID]` | A specific row |\r\n| `buckets.[BUCKET_ID].files` | All files in a bucket |\r\n| `buckets.[BUCKET_ID].files.[FILE_ID]` | A specific file |\r\n| `teams` | Changes to teams the user belongs to |\r\n| `teams.[TEAM_ID]` | Changes to a specific team |\r\n| `memberships` | Changes to the user's team memberships |\r\n| `memberships.[MEMBERSHIP_ID]` | A specific membership |\r\n| `functions.[FUNCTION_ID].executions` | Execution updates for a function |\r\n\r\nThe `response` object includes: `events` (array of event strings), `payload` (the affected resource), `channels` (channels matched), and `timestamp` (ISO 8601).\r\n\r\n### Serverless Functions (server-side)\r\n\r\n```typescript\r\nconst functions = new Functions(client);\r\n\r\n// Execute function\r\nconst execution = await functions.createExecution({\r\n    functionId: '[FUNCTION_ID]',\r\n    body: JSON.stringify({ key: 'value' })\r\n});\r\n\r\n// List executions\r\nconst executions = await functions.listExecutions({ functionId: '[FUNCTION_ID]' });\r\n```\r\n\r\n#### Writing a Function Handler (Node.js runtime)\r\n\r\nWhen deploying your own Appwrite Function, the entry point file must export a default async function:\r\n\r\n```typescript\r\n// src/main.js (or src/main.ts)\r\nexport default async ({ req, res, log, error }) => {\r\n    // Request properties\r\n    // req.body        — raw request body (string)\r\n    // req.bodyJson    — parsed JSON body (object, or undefined if not JSON)\r\n    // req.headers     — request headers (object)\r\n    // req.method      — HTTP method (GET, POST, PUT, DELETE, PATCH)\r\n    // req.path        — URL path (e.g. '/hello')\r\n    // req.query       — parsed query parameters (object)\r\n    // req.queryString — raw query string\r\n\r\n    log('Processing request: ' + req.method + ' ' + req.path);\r\n\r\n    if (req.method === 'GET') {\r\n        return res.json({ message: 'Hello from Appwrite Function!' });\r\n    }\r\n\r\n    const data = req.bodyJson;\r\n    if (!data?.name) {\r\n        error('Missing name field');\r\n        return res.json({ error: 'Name is required' }, 400);\r\n    }\r\n\r\n    // Response methods\r\n    return res.json({ success: true });                    // JSON (sets Content-Type automatically)\r\n    // return res.text('Hello');                           // plain text\r\n    // return res.empty();                                 // 204 No Content\r\n    // return res.redirect('https://example.com');         // 302 Redirect\r\n    // return res.send('data', 200, { 'X-Custom': '1' }); // custom body, status, headers\r\n};\r\n```\r\n\r\n### Server-Side Rendering (SSR) Authentication\r\n\r\nSSR apps (Next.js, SvelteKit, Nuxt, Remix, Astro) use the **server SDK** (`node-appwrite`) to handle auth. You need two clients:\r\n\r\n- **Admin client** — uses an API key, creates sessions, bypasses rate limits (reusable singleton)\r\n- **Session client** — uses a session cookie, acts on behalf of a user (create per-request, never share)\r\n\r\n```typescript\r\nimport { Client, Account, OAuthProvider } from 'node-appwrite';\r\n\r\n// Admin client (reusable)\r\nconst adminClient = new Client()\r\n    .setEndpoint('https://<REGION>.cloud.appwrite.io/v1')\r\n    .setProject('[PROJECT_ID]')\r\n    .setKey(process.env.APPWRITE_API_KEY);\r\n\r\n// Session client (create per-request)\r\nconst sessionClient = new Client()\r\n    .setEndpoint('https://<REGION>.cloud.appwrite.io/v1')\r\n    .setProject('[PROJECT_ID]');\r\n\r\nconst session = req.cookies['a_session_[PROJECT_ID]'];\r\nif (session) {\r\n    sessionClient.setSession(session);\r\n}\r\n```\r\n\r\n#### Email/Password Login\r\n\r\n```typescript\r\napp.post('/login', async (req, res) => {\r\n    const account = new Account(adminClient);\r\n    const session = await account.createEmailPasswordSession({\r\n        email: req.body.email,\r\n        password: req.body.password,\r\n    });\r\n\r\n    // Cookie name must be a_session_<PROJECT_ID>\r\n    res.cookie('a_session_[PROJECT_ID]', session.secret, {\r\n        httpOnly: true,\r\n        secure: true,\r\n        sameSite: 'strict',\r\n        expires: new Date(session.expire),\r\n        path: '/',\r\n    });\r\n\r\n    res.json({ success: true });\r\n});\r\n```\r\n\r\n#### Authenticated Requests\r\n\r\n```typescript\r\napp.get('/user', async (req, res) => {\r\n    const session = req.cookies['a_session_[PROJECT_ID]'];\r\n    if (!session) return res.status(401).json({ error: 'Unauthorized' });\r\n\r\n    // Create a fresh session client per request\r\n    const sessionClient = new Client()\r\n        .setEndpoint('https://<REGION>.cloud.appwrite.io/v1')\r\n        .setProject('[PROJECT_ID]')\r\n        .setSession(session);\r\n\r\n    const account = new Account(sessionClient);\r\n    const user = await account.get();\r\n    res.json(user);\r\n});\r\n```\r\n\r\n#### OAuth2 SSR Flow\r\n\r\n```typescript\r\n// Step 1: Redirect to OAuth provider\r\napp.get('/oauth', async (req, res) => {\r\n    const account = new Account(adminClient);\r\n    const redirectUrl = await account.createOAuth2Token({\r\n        provider: OAuthProvider.Github,\r\n        success: 'https://example.com/oauth/success',\r\n        failure: 'https://example.com/oauth/failure',\r\n    });\r\n    res.redirect(redirectUrl);\r\n});\r\n\r\n// Step 2: Handle callback — exchange token for session\r\napp.get('/oauth/success', async (req, res) => {\r\n    const account = new Account(adminClient);\r\n    const session = await account.createSession({\r\n        userId: req.query.userId,\r\n        secret: req.query.secret,\r\n    });\r\n\r\n    res.cookie('a_session_[PROJECT_ID]', session.secret, {\r\n        httpOnly: true, secure: true, sameSite: 'strict',\r\n        expires: new Date(session.expire), path: '/',\r\n    });\r\n    res.json({ success: true });\r\n});\r\n```\r\n\r\n> **Cookie security:** Always use `httpOnly`, `secure`, and `sameSite: 'strict'` to prevent XSS. The cookie name must be `a_session_<PROJECT_ID>`.\r\n\r\n> **Forwarding user agent:** Call `sessionClient.setForwardedUserAgent(req.headers['user-agent'])` to record the end-user's browser info for debugging and security.\r\n\r\n## Error Handling\r\n\r\n```typescript\r\nimport { AppwriteException } from 'appwrite';\r\n// Server-side: import from 'node-appwrite'\r\n\r\ntry {\r\n    const doc = await tablesDB.getRow({\r\n        databaseId: '[DATABASE_ID]',\r\n        tableId: '[TABLE_ID]',\r\n        rowId: '[ROW_ID]',\r\n    });\r\n} catch (err) {\r\n    if (err instanceof AppwriteException) {\r\n        console.log(err.message);   // human-readable error message\r\n        console.log(err.code);      // HTTP status code (number)\r\n        console.log(err.type);      // Appwrite error type string (e.g. 'document_not_found')\r\n        console.log(err.response);  // full response body (object)\r\n    }\r\n}\r\n```\r\n\r\n**Common error codes:**\r\n\r\n| Code | Meaning |\r\n|------|---------|\r\n| `401` | Unauthorized — missing or invalid session/API key |\r\n| `403` | Forbidden — insufficient permissions for this action |\r\n| `404` | Not found — resource does not exist |\r\n| `409` | Conflict — duplicate ID or unique constraint violation |\r\n| `429` | Rate limited — too many requests, retry after backoff |\r\n\r\n## Permissions & Roles (Critical)\r\n\r\nAppwrite 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.\r\n\r\n```typescript\r\nimport { Permission, Role } from 'appwrite';\r\n// Server-side: import from 'node-appwrite'\r\n```\r\n\r\n### Database Row with Permissions\r\n\r\n```typescript\r\nconst doc = await tablesDB.createRow({\r\n    databaseId: '[DATABASE_ID]',\r\n    tableId: '[TABLE_ID]',\r\n    rowId: ID.unique(),\r\n    data: { title: 'Hello World' },\r\n    permissions: [\r\n        Permission.read(Role.user('[USER_ID]')),     // specific user can read\r\n        Permission.update(Role.user('[USER_ID]')),   // specific user can update\r\n        Permission.read(Role.team('[TEAM_ID]')),     // all team members can read\r\n        Permission.read(Role.any()),                 // anyone (including guests) can read\r\n    ]\r\n});\r\n```\r\n\r\n### File Upload with Permissions\r\n\r\n```typescript\r\nconst file = await storage.createFile({\r\n    bucketId: '[BUCKET_ID]',\r\n    fileId: ID.unique(),\r\n    file: document.getElementById('file-input').files[0],\r\n    permissions: [\r\n        Permission.read(Role.any()),\r\n        Permission.update(Role.user('[USER_ID]')),\r\n        Permission.delete(Role.user('[USER_ID]')),\r\n    ]\r\n});\r\n```\r\n\r\n> **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.\r\n\r\n> **Common mistakes:**\r\n> - **Forgetting permissions** — the resource becomes inaccessible to all users (including the creator)\r\n> - **`Role.any()` with `write`/`update`/`delete`** — allows any user, including unauthenticated guests, to modify or remove the resource\r\n> - **`Permission.read(Role.any())` on sensitive data** — makes the resource publicly readable\r\n\r\n"
}

SHA-256: 29780cfdd15f18a47794ffa7545b8f4aad4dfd2d10e5df57e87dd2048309ed26