| 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: 'github',
success: 'https://example.com/success',
failure: 'https://example.com/fail'
});
const user = await account.get();
await account.deleteSession({ sessionId: 'current' });
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.
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]'
});
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)
const unsubscribe = client.subscribe('databases.[DATABASE_ID].tables.[TABLE_ID].rows', (response) => {
console.log(response.events);
console.log(response.payload);
});
client.subscribe('buckets.[BUCKET_ID].files', (response) => {
console.log(response.payload);
});
client.subscribe([
'databases.[DATABASE_ID].tables.[TABLE_ID].rows',
'buckets.[BUCKET_ID].files',
], (response) => { });
unsubscribe();
Available channels:
| Channel | Description |
|---|
account | Changes to the authenticated user's account |
databases.[DB_ID].tables.[TABLE_ID].rows | All rows in a table |
databases.[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 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';
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 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