---
title: Appwrite
description: Appwrite Storage via the official node-appwrite SDK. Auto-loads APPWRITE_ENDPOINT, APPWRITE_PROJECT_ID, and APPWRITE_API_KEY, or pass an existing client.
---

## Installation

`node-appwrite` is an optional peer dependency of `files-sdk` - install alongside the SDK so the adapter's imports resolve at runtime.

```package-install
files-sdk node-appwrite
```

## Usage

```ts lineNumbers
import { Files } from "files-sdk";
import { appwrite } from "files-sdk/appwrite";

const files = new Files({
  adapter: appwrite({
    bucket: "uploads",
    // Auto-loads from APPWRITE_ENDPOINT, APPWRITE_PROJECT_ID,
    // and APPWRITE_API_KEY. Or pass an existing node-appwrite
    // Client or Storage instance via `client`.
    //
    // Note: Appwrite keys (IDs) must start with a letter or digit,
    // use only [a-zA-Z0-9._-], and be at most 36 chars.
    // Slashes (/) are not supported.
  }),
});
```

## Options

<AutoTypeTable
  path="../../packages/files-sdk/src/appwrite/index.ts"
  name="AppwriteAdapterOptions"
/>

## Limitations

File IDs (keys) must start with an alphanumeric and use only `[a-zA-Z0-9._-]`, max 36 characters (no slashes) - every method that takes a key rejects an invalid one before the API call. `list({ prefix })` queries `startsWith("$id", ...)` against the canonical file ID, so files created outside the adapter where the display `name` differs from `$id` won't be matched by prefix. Resumable uploads (`control`) send chunks over Appwrite's REST API with the API key, so they need `key` (or `APPWRITE_API_KEY`); an adapter built from a pre-built `client` throws on them.

## Compatibility

| Method | Status | Notes |
| --- | :-: | --- |
| `upload` | ⚠️ | Stream bodies are buffered up-front - `InputFile.fromBuffer` has no streaming form, so streamed uploads can't avoid materializing the body in memory. User `metadata` and `cacheControl` throw - Appwrite's `createFile` has no equivalent fields. `contentType` is silently ignored - Appwrite auto-detects mime from the payload and has no override. Uploading onto an existing key replaces the file by deleting it and creating it again (Appwrite can't update a file's content): not atomic, and file-level permissions on the old file aren't carried over. Resumable uploads (`control`) don't replace: Appwrite rejects a chunked upload onto an existing file ID, so delete the key first. They're raw HTTP authenticated with the API key, so they need the adapter to build its own client from one: with a pre-built `client` or without an API key, `capabilities.resumable` is `false`. |
| `download` | ✅ |  |
| `delete` | ✅ |  |
| `list` | ✅ | Pages hold up to 1,000 files by default; pass `limit` for smaller pages. The total count Appwrite would otherwise compute is skipped, since the adapter never reads it. |
| `search` | ✅ |  |
| `head` | ✅ |  |
| `exists` | ✅ |  |
| `copy` | ⚠️ | Read-then-write - Appwrite has no server-side copy primitive, so the source is downloaded and re-uploaded. Costs an egress + an ingest; not atomic. An existing destination is replaced the same way `upload` replaces one. |
| `url` | ⚠️ | Throws by default because Appwrite SDKs cannot mint presigned reading URLs with keys. Set `public: true` at construction to return the constructed, permanent Appwrite `/view` URL. `url(key)` returns the permanent link; an explicit `expiresIn` throws `Unsupported`. `responseContentDisposition: "attachment"` returns the `/download` URL (served as an attachment named after the stored file name); any other disposition, such as a custom filename or `inline`, throws - Appwrite has no per-request override. |
| `signedUploadUrl` | ❌ | No presigned upload primitive in Appwrite. Use JWTs or client SDKs for direct uploads. |
