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

Safety Notice

This listing is imported from skills.sh public index metadata. Review upstream SKILL.md and repository scripts before running.

Copy this and send it to your AI assistant to learn

Install skill "appwrite-typescript" with this command: npx skills add chiragagg5k/appwrite-agent-skills/chiragagg5k-appwrite-agent-skills-appwrite-typescript

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
account.createOAuth2Session({
    provider: 'github',
    success: 'https://example.com/success',
    failure: 'https://example.com/fail'
});

// Get current user
const user = await account.get();

// Logout
await account.deleteSession({ sessionId: 'current' });

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.

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]'
});

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)

// Subscribe to row changes
const unsubscribe = client.subscribe('databases.[DATABASE_ID].tables.[TABLE_ID].rows', (response) => {
    console.log(response.events);   // e.g. ['databases.*.tables.*.rows.*.create']
    console.log(response.payload);  // the affected resource
});

// Subscribe to file changes
client.subscribe('buckets.[BUCKET_ID].files', (response) => {
    console.log(response.payload);
});

// Subscribe to multiple channels
client.subscribe([
    'databases.[DATABASE_ID].tables.[TABLE_ID].rows',
    'buckets.[BUCKET_ID].files',
], (response) => { /* ... */ });

// Unsubscribe
unsubscribe();

Available channels:

ChannelDescription
accountChanges to the authenticated user's account
databases.[DB_ID].tables.[TABLE_ID].rowsAll rows in a table
databases.[DB_ID].tables.[TABLE_ID].rows.[ROW_ID]A specific row
buckets.[BUCKET_ID].filesAll files in a bucket
buckets.[BUCKET_ID].files.[FILE_ID]A specific file
teamsChanges to teams the user belongs to
teams.[TEAM_ID]Changes to a specific team
membershipsChanges to the user's team memberships
memberships.[MEMBERSHIP_ID]A specific membership
functions.[FUNCTION_ID].executionsExecution 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:

CodeMeaning
401Unauthorized — missing or invalid session/API key
403Forbidden — insufficient permissions for this action
404Not found — resource does not exist
409Conflict — duplicate ID or unique constraint violation
429Rate 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.

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

Source Transparency

This detail page is rendered from real SKILL.md content. Trust labels are metadata-based hints, not a safety guarantee.

Related Skills

Related by shared tags or category signals.

Coding

appwrite-typescript

No summary provided by upstream source.

Repository SourceNeeds Review
Coding

dokidoki

Control interactive BLE devices (scan/connect/playback/timeline) from terminal.

Registry SourceRecently Updated
Coding

code

Comprehensive code review assistant that analyzes code quality, identifies bugs, suggests improvements, and ensures adherence to best practices. Use when rev...

Registry SourceRecently Updated
Coding

Markdown Lint

Use this skill immediately when the user needs to: set up markdownlint-cli2 and pre-commit hooks in a repository, fix or batch-repair markdownlint errors lik...

Registry SourceRecently Updated