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

- **Type:** Skill
- **Install:** `agentstack add skill-appwrite-agent-skills-appwrite-typescript`
- **Verified:** Yes — security-reviewed for prompt injection and unsafe behavior
- **Seller:** [appwrite](https://agentstack.voostack.com/s/appwrite)
- **Installs:** 0
- **Category:** [Databases](https://agentstack.voostack.com/c/databases)
- **Latest version:** 0.1.0
- **License:** BSD-3-Clause
- **Upstream author:** [appwrite](https://github.com/appwrite)
- **Source:** https://github.com/appwrite/agent-skills/tree/main/skills/appwrite-typescript

## Install

```sh
agentstack add skill-appwrite-agent-skills-appwrite-typescript
```

Requires the [AgentStack CLI](https://agentstack.voostack.com/docs/cli). Works with Claude Code, Cursor, and any MCP-compatible agent.

## About

# 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.greaterThanEqual('field', 100)    // field >= value
Query.between('field', 1, 100)          // 1  **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.respons

…

## Source & license

This open-source skill is cataloged on AgentStack and links to its original source — we do not rehost the code.

- **Author:** [appwrite](https://github.com/appwrite)
- **Source:** [appwrite/agent-skills](https://github.com/appwrite/agent-skills)
- **License:** BSD-3-Clause

Install and usage instructions live in the source repository linked above.

## Pricing

- **Free** — Free

## Security capabilities

Automated source analysis of v0.1.0 — what this tool can access:

- **Network access:** no
- **Filesystem access:** no
- **Shell / process execution:** no
- **Environment & secrets:** yes
- **Dynamic code execution:** no

*"Yes" means the capability is present in the source — more access means more to trust, not that it is unsafe.*


## Versions

- **0.1.0** — security scan: passed — Imported from the upstream source.

## Links

- Listing page: https://agentstack.voostack.com/l/skill-appwrite-agent-skills-appwrite-typescript
- Seller: https://agentstack.voostack.com/s/appwrite
- Browse the marketplace: https://agentstack.voostack.com/browse

---
Listed on AgentStack — the marketplace for AI agent skills and MCP servers. Every listing is security-reviewed. Creators keep 70%.
