Skip to content
Blog / How to use Appwrite's Server SDK vs Client SDK
5 min

How to use Appwrite's Server SDK vs Client SDK

Understand the difference between Appwrite's Client and Server SDKs, when to use each one, and how to avoid common security mistakes in your app.

One of the most common points of confusion when starting with Appwrite is which SDK to use. Appwrite ships two distinct sets of SDKs: one for client-side code running in browsers and mobile apps, and one for server-side code. They share a similar structure, but they authenticate differently and have different levels of access. Using the wrong one in the wrong context is a real security problem.

This post breaks down how each SDK authenticates, what it can access, and which one belongs in each part of your architecture.

The core difference: sessions vs API keys

The Client SDK authenticates on behalf of a user using a session. When a user logs in through your app, Appwrite creates a session for that user. All subsequent requests made with the Client SDK carry that session and are subject to the permissions you have set on your tables, buckets, and functions. The Client SDK can only do what the currently logged-in user is allowed to do.

The Server SDK authenticates using an API key. API keys are created in the Appwrite Console and scoped to specific capabilities. A request made with a Server SDK and a valid API key can bypass the permissions system entirely, depending on the key's scope. This is intentional: server-side code often needs to perform operations across users, run scheduled jobs, or do administrative work that no individual user session should be able to authorize.

This distinction matters a great deal. An API key embedded in a mobile app or browser JavaScript is a serious security risk because anyone who extracts it gains the access level of that key. API keys belong on the server, never in client code.

Client SDKs: for browsers and mobile

The available Client SDKs are:

  • Web (JavaScript/TypeScript)
  • Flutter
  • React Native (beta)
  • Apple (Swift)
  • Android (Kotlin/Java)

These SDKs are built around the Account service. Users sign up, log in, manage their sessions, and interact with resources they own. Typical client-side operations include:

  • Creating and managing user accounts
  • Reading and writing rows the user owns or has been granted access to
  • Uploading and downloading files from buckets with appropriate permissions
  • Subscribing to real-time events on resources the user can access

A basic client-side login and data fetch in JavaScript looks like this:

JavaScript
import { Client, Account, TablesDB, Query } from "appwrite";

const client = new Client()
  .setEndpoint("https://cloud.appwrite.io/v1")
  .setProject("<PROJECT_ID>");

const account = new Account(client);
const tablesDB = new TablesDB(client);

// Log in
await account.createEmailPasswordSession("user@example.com", "password");

// Read data — scoped to what this user can access
const docs = await tablesDB.listRows({
  databaseId: "<DATABASE_ID>",
  tableId: "<TABLE_ID>",
  queries: [Query.equal("userId", ["<CURRENT_USER_ID>"])],
});

The session is maintained automatically (as a cookie in browsers, or in secure storage on mobile). There is nothing you need to pass manually after login.

Server SDKs: for backends and functions

The available Server SDKs are: Node.js, Python, Dart, PHP, Ruby, .NET, Go, Swift, Kotlin

Server SDKs are initialized with an API key rather than a user session. The key is created in the Appwrite Console under your project settings, and you assign it only the scopes it needs. For example, a function that sends emails only needs messaging.write, not databases.write.

A Node.js server-side example:

JavaScript
import { Client, TablesDB, Users, Query } from "node-appwrite";

const client = new Client()
  .setEndpoint("https://cloud.appwrite.io/v1")
  .setProject("<PROJECT_ID>")
  .setKey("<API_KEY>");

const tablesDB = new TablesDB(client);
const users = new Users(client);

// List all users — not possible from the Client SDK
const allUsers = await users.list();

// Write a row on behalf of the system
await tablesDB.createRow({
  databaseId: "<DATABASE_ID>",
  tableId: "<TABLE_ID>",
  rowId: "unique()",
  data: { type: "system_event", payload: "job_completed" },
});

This code has no user session. It operates at the API key level, which means it can access any resource within the key's scopes regardless of row permissions.

Inside Appwrite Functions

Appwrite Functions run on the server, so they use the Server SDK. When a function is triggered (by an HTTP request, an event, or a schedule), Appwrite injects a context object that includes a pre-authenticated client with elevated privileges. You do not need to manage an API key manually in most cases:

JavaScript
export default async ({ req, res, log, error }) => {
  const client = new Client()
    .setEndpoint(process.env.APPWRITE_FUNCTION_API_ENDPOINT)
    .setProject(process.env.APPWRITE_FUNCTION_PROJECT_ID)
    .setKey(req.headers["x-appwrite-key"]);

  const tablesDB = new TablesDB(client);

  // Perform elevated operations
  const result = await tablesDB.listRows({
    databaseId: "<DATABASE_ID>",
    tableId: "<TABLE_ID>",
  });

  return res.json(result);
};

Functions are the right place for any operation that requires elevated access but should not be exposed directly to users: processing payments, sending notifications to all users, syncing data with external services, or running periodic cleanup jobs.

Server SDKs with sessions for SSR

Server-side rendering introduces a third authentication mode that does not fit cleanly into either category above. Frameworks like Next.js, SvelteKit, Nuxt, and Astro render pages on the server, which means you cannot use the Client SDK (it assumes a browser context). But you also cannot use an API key, because you want requests to respect the permissions of the specific user loading the page. Using an API key here would bypass the permissions system entirely, exposing every user's data to every server-rendered request.

The solution is to use the Server SDK but authenticate it with the user's session token instead of an API key. Appwrite stores session tokens in a cookie named a_session_<PROJECT_ID>. Your server reads that cookie from the incoming request and passes it to the client using setSession(). The result is a server-side client that acts exactly as that user, with no elevated access.

In practice, an SSR setup requires two separate clients:

Admin client: initialized with an API key scoped to sessions.write. Used only to create and manage sessions (login, logout). This client bypasses rate limits, which matters when all requests originate from the same server IP.

Session client: initialized with the user's session token from the request cookie. Used for all data fetching on behalf of the user. This client respects permissions the same way the browser Client SDK would.

JavaScript
import { Client, Account } from "node-appwrite";

// Admin client: used to create sessions (e.g. on login)
const adminClient = new Client()
  .setEndpoint("https://cloud.appwrite.io/v1")
  .setProject("<PROJECT_ID>")
  .setKey("<API_KEY>"); // sessions.write scope

// Session client: used to fetch data as the current user
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);
}

// Fetch data as the user — permissions are enforced
const account = new Account(sessionClient);
const user = await account.get();

A few important rules for this pattern:

  • Never reuse the session client across requests. Create a new client per request. Sharing a client between requests will mix up user sessions.
  • Never set a session on the admin client. Keep the two clients entirely separate.
  • Set the cookie with httpOnly, secure, and sameSite: strict. This prevents JavaScript from reading the session token and protects against CSRF.
  • Use a custom domain for your Appwrite endpoint if you want the session cookie to also work with client-side Appwrite calls on the same domain. See custom domains.

When a user logs in through your server, create the session with the admin client and store the session.secret value in the cookie. On subsequent requests, read the cookie and pass it to a fresh session client. That client will behave identically to a browser-based Client SDK authenticated as that user.

Customer identity without the hassle

Add secure authentication in minutes, not weeks.

  • checkmark icon Built-in security and compliance
  • checkmark icon Multiple login methods
  • checkmark icon Custom authentication flows
  • checkmark icon Multi-factor authentication

Common patterns

Pattern 1: User-owned data. The user creates and reads their own rows. Use the Client SDK. Set table permissions so users can only read and write their own rows using the user(<ID>) role.

Pattern 2: Admin operations. A dashboard that lists all users, or a job that resets expired sessions. Use the Server SDK with an API key scoped to what you need.

Pattern 3: Triggered server logic. A user submits a form, and you need to send a confirmation email and write a log entry with admin-level access. Use the Client SDK for the user-facing submission, trigger an Appwrite Function from the event, and use the Server SDK inside that function for the privileged steps.

Pattern 4: Scheduled jobs. A cron-triggered function that archives old records. Server SDK only, no user session involved.

Security considerations

A few rules that save you from common mistakes:

  • Never put an API key in a mobile app or browser-side JavaScript bundle. It will be extracted and abused.
  • Scope your API keys narrowly. If a key only needs to read from one table, do not give it databases.write or users.read.
  • Use the Client SDK for anything user-facing. The permissions system is your first line of defense for user data.
  • Prefer using the injected key inside Appwrite Functions over hardcoding API keys in function environment variables when possible.
  • The Client SDK cannot use the Realtime API when authenticated with an API key. Real-time subscriptions are session-based only.

Choosing the right SDK

Both SDKs share the same ID, Query, Permission, and Role utility classes, so the programming model feels consistent. The key question is always: who is making this request, and what should they be allowed to do?

If the request comes from a user interacting with your app, use the Client SDK and let the permissions system enforce access. If the request comes from your server, a background job, or an Appwrite Function, use the Server SDK with a properly scoped API key.

Getting this right from the start avoids a whole class of access control bugs.

Start building with Appwrite today

Get started