| 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. |
Appwrite TypeScript SDK
Installation
npm install appwrite
npm install react-native-appwrite
npm install node-appwrite
Setting Up the Client
Client-side (Web / React Native)
import { Client, Account, TablesDB, Storage, ID, Query } from 'appwrite';
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);
await account.create({
userId: ID.unique(),
email: 'user@example.com',
password: 'password123',
name: 'User Name'
});
const session = await account.createEmailPasswordSession({
email: 'user@example.com',
password: 'password123'
});
account.createOAuth2Session({
provider: OAuthProvider.Github,
success: 'https://example.com/success',
failure: 'https://example.com/fail',
scopes: ['repo', 'user']
});
const user = await account.get();
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) {
const deepLink = new URL(makeRedirectUri({ preferLocalhost: true }));
const scheme = `${deepLink.protocol}//`;
const loginUrl = await account.createOAuth2Token({
provider,
success: `${deepLink}`,
failure: `${deepLink}`,
});
const result = await WebBrowser.openAuthSessionAsync(`${loginUrl}`, scheme);
if (result.type !== 'success') return;
const url = new URL(result.url);
const secret = url.searchParams.get('secret');
const userId = url.searchParams.get('userId');
await account.createSession({ userId, secret });
}
await oauthLogin(OAuthProvider.Github);
await oauthLogin(OAuthProvider.Google);
User Management (server-side)
const users = new Users(client);
const user = await users.create({
userId: ID.unique(),
email: 'user@example.com',
password: 'password123',
name: 'User Name'
});
const list = await users.list({ queries: [Query.limit(25)] });
const fetched = await users.get({ userId: '[USER_ID]' });
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);
const db = await tablesDB.create({ databaseId: ID.unique(), name: 'My Database' });
const col = await tablesDB.createTable({
databaseId: '[DATABASE_ID]',
tableId: ID.unique(),
name: 'My Table'
});
const doc = await tablesDB.createRow({
databaseId: '[DATABASE_ID]',
tableId: '[TABLE_ID]',
rowId: ID.unique(),
data: { title: 'Hello World', content: 'Example content' }
});
const results = await tablesDB.listRows({
databaseId: '[DATABASE_ID]',
tableId: '[TABLE_ID]',
queries: [Query.equal('status', 'active'), Query.limit(10)]
});
const row = await tablesDB.getRow({
databaseId: '[DATABASE_ID]',
tableId: '[TABLE_ID]',
rowId: '[ROW_ID]'
});
await tablesDB.updateRow({
databaseId: '[DATABASE_ID]',
tableId: '[TABLE_ID]',
rowId: '[ROW_ID]',
data: { title: 'Updated Title' }
});
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.
await tablesDB.createTable({
databaseId: '[DATABASE_ID]',
tableId: ID.unique(),
name: 'articles',
columns: [
{ key: 'title', type: 'varchar', size: 255, required: true },
{ key: 'summary', type: 'text', required: false },
{ key: 'body', type: 'mediumtext', required: false },
{ key: 'raw_data', type: 'longtext', required: false },
]
});
TypeScript Generics
import { Models } from 'appwrite';
interface Todo {
title: string;
done: boolean;
priority: number;
}
const results = await tablesDB.listRows({
databaseId: '[DATABASE_ID]',
tableId: '[TABLE_ID]',
queries: [Query.equal('done', false)]
});
const doc = results.documents[0];
doc.$id;
doc.$createdAt;
doc.$updatedAt;
doc.$permissions;
doc.$databaseId;
doc.$collectionId;
Query Methods
Query.equal('field', 'value')
Query.notEqual('field', 'value')
Query.lessThan('field', 100)
Query.lessThanEqual('field', 100)
Query.greaterThan('field', 100)
Query.greaterThanEqual('field', 100)
Query.between('field', 1, 100)
Query.isNull('field')
Query.isNotNull('field')
Query.startsWith('field', 'prefix')
Query.endsWith('field', 'suffix')
Query.contains('field', 'substring')
Query.search('field', 'keywords')
Query.orderAsc('field')
Query.orderDesc('field')
Query.limit(25)
Query.offset(0)
Query.cursorAfter('[ROW_ID]')
Query.cursorBefore('[ROW_ID]')
Query.select(['field1', 'field2'])
Query.or([Query.equal('a', 1), Query.equal('b', 2)])
Query.and([Query.greaterThan('age', 18), Query.lessThan('age', 65)])
File Storage
const storage = new Storage(client);
const file = await storage.createFile({
bucketId: '[BUCKET_ID]',
fileId: ID.unique(),
file: document.getElementById('file-input').files[0]
});
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')
});
const files = await storage.listFiles({ bucketId: '[BUCKET_ID]' });
const preview = storage.getFilePreview({
bucketId: '[BUCKET_ID]',
fileId: '[FILE_ID]',
width: 300,
height: 300
});
const download = await storage.getFileDownload({
bucketId: '[BUCKET_ID]',
fileId: '[FILE_ID]'
});
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')
InputFile.fromBuffer(buffer, 'file.png')
InputFile.fromStream(readableStream, 'file.png', size)
InputFile.fromPlainText('Hello world', 'hello.txt')
Teams
const teams = new Teams(client);
const team = await teams.create({ teamId: ID.unique(), name: 'Engineering' });
const list = await teams.list();
const membership = await teams.createMembership({
teamId: '[TEAM_ID]',
roles: ['editor'],
email: 'user@example.com',
});
const members = await teams.listMemberships({ teamId: '[TEAM_ID]' });
await teams.updateMembership({
teamId: '[TEAM_ID]',
membershipId: '[MEMBERSHIP_ID]',
roles: ['admin'],
});
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);
const subscription = await realtime.subscribe(
Channel.tablesdb('[DATABASE_ID]').table('[TABLE_ID]').row(),
(response) => {
console.log(response.events);
console.log(response.payload);
}
);
await realtime.subscribe(
Channel.tablesdb('[DATABASE_ID]').table('[TABLE_ID]').row('[ROW_ID]'),
(response) => { }
);
await realtime.subscribe([
Channel.tablesdb('[DATABASE_ID]').table('[TABLE_ID]').row(),
Channel.bucket('[BUCKET_ID]').file(),
], (response) => { });
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);
const execution = await functions.createExecution({
functionId: '[FUNCTION_ID]',
body: JSON.stringify({ key: 'value' })
});
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:
export default async ({ req, res, log, error }) => {
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);
}
return res.json({ success: true });
};
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';
const adminClient = new Client()
.setEndpoint('https://<REGION>.cloud.appwrite.io/v1')
.setProject('[PROJECT_ID]')
.setKey(process.env.APPWRITE_API_KEY);
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,
});
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' });
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
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);
});
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';
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);
console.log(err.code);
console.log(err.type);
console.log(err.response);
}
}
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 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';
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]')),
Permission.update(Role.user('[USER_ID]')),
Permission.read(Role.team('[TEAM_ID]')),
Permission.read(Role.any()),
]
});
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