Imported from Meldiron/getauthui (
.agents/skills/appwrite-typescript/SKILL.md). Install upstream withnpx skills add Meldiron/getauthui --skill appwrite-typescript. Copyright stays with the author.
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 usecreateOAuth2Token()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 deprecatedDatabasesclass) for all new code. Only useDatabasesif 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
stringtype 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 |
varcharis stored inline and counts towards the 64 KB row size limit. Prefer for short, indexed fields like names, slugs, or identifiers.text,mediumtext, andlongtextare stored off-page (only a 20-byte pointer lives in the row), so they don't consume the row size budget.sizeis 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 orRole.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, andsameSite: 'strict'to prevent XSS. The cookie name must bea_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 & 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()withwrite/update/delete— allows any user, including unauthenticated guests, to modify or remove the resourcePermission.read(Role.any())on sensitive data — makes the resource publicly readable