appwrite-typescript

Appwrite TypeScript SDK skill. Use when building browser-based JavaScript/TypeScript apps, React Native mobile apps, or server-side Node.js/Deno backends with…

INSTALLATION
npx skills add https://github.com/appwrite/agent-skills --skill appwrite-typescript
Run in your project or agent environment. Adjust flags if your CLI version differs.

SKILL.md

Appwrite TypeScript SDK

Installation

# 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)

// 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://<REGION>.cloud.appwrite.io/v1')

    .setProject('[PROJECT_ID]');

Server-side (Node.js / Deno)

import { 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)

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:

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:

{

  "expo": {

    "scheme": "appwrite-callback-[PROJECT_ID]"

  }

}

#### OAuth Flow

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://<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 });

}

// Usage

await oauthLogin(OAuthProvider.Github);

await oauthLogin(OAuthProvider.Google);

User Management (server-side)

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.

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.
// 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

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<Models.Document> 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<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

// 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

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)

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

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)

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)

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:

// 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)
import { 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

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_<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

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://<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

// 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_<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

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 &#x26; 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.

import { Permission, Role } from 'appwrite';

// Server-side: import from 'node-appwrite'

Database Row with Permissions

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

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
BrowserAct

Let your agent run on any real-world website

Bypass CAPTCHA & anti-bot for free. Start local, scale to cloud.

Explore BrowserAct Skills →

Stop writing automation&scrapers

Install the CLI. Run your first Skill in 30 seconds. Scale when you're ready.

Start free
free · no credit card