appwrite-rust — Claude Code skill: what it does, how to install
Teaches AI assistants to build backend Rust applications using the Appwrite SDK, Tokio async runtime, TablesDB tables, and user authentication.
npx skills add appwrite/skills --skill appwrite-rust -a claude-codeWhat appwrite-rust does
This skill supplies coding rules and code patterns for the appwrite crate. The Rust SDK relies on an async Tokio runtime and authenticates server-side through API keys. The guide enforces the SDK calling convention: methods use positional parameters, while optional values require Option<T> with explicit None when omitted.
For storage schemas, the skill guides the agent toward TablesDB instead of the legacy Databases service. It tracks column boundaries carefully. An inline varchar field consumes the 64 KB row budget, while off-page types like text or mediumtext do not. Projects require tokio with full features and serde_json in Cargo.toml to compile these examples.
What it helps with
- Configure the async Appwrite Rust client using Tokio and environment variables
- Design TablesDB schemas balancing inline varchar limits and off-page text types
- Construct stringified Query filters and aggregations for database queries
- Implement user registration and deletion via the Users service
Example prompts for the agent
- Write a Rust function to register a new user using the Appwrite Users service
- Create a TablesDB table in Rust with both varchar and longtext columns
- Build an Appwrite query in Rust filtering by status and limiting results
How to install appwrite-rust
1Skills CLI (Vercel)
npx skills add appwrite/skills --skill appwrite-rust -a claude-code2Manually: put the files in place
~/.claude/skills/appwrite-rust/SKILL.mdCopy the whole skill folder; for one project use .claude/skills/appwrite-rust/
1Skills CLI (Vercel)
npx skills add appwrite/skills --skill appwrite-rust -a codex2Manually: put the files in place
.agents/skills/appwrite-rust/SKILL.mdFor all projects use ~/.agents/skills/
1Skills CLI (Vercel)
npx skills add appwrite/skills --skill appwrite-rust -a cursor2Manually: put the files in place
.cursor/skills/appwrite-rust/SKILL.mdCursor also reads .agents/skills/ and .claude/skills/
1GitHub CLI
gh skill install appwrite/skills appwrite-rust2Manually: put the files in place
.github/skills/appwrite-rust/SKILL.md1Skills CLI (Vercel)
npx skills add appwrite/skills --skill appwrite-rust -a gemini-cli2Platform CLI
gemini skills install https://github.com/appwrite/skills --path skills/appwrite-rustManually: put the files in place
.devin/skills/appwrite-rust/SKILL.mdThe legacy .windsurf/skills/ path still works
Skills CLI (Vercel)
npx skills add appwrite/skills --skill appwrite-rust -a opencodeClaude.ai
Zip the folder and upload it in Settings → Capabilities → Skills
Step by step, with commands for each platform: How to install skills in Claude Code: five ways
Questions about appwrite-rust
How to install appwrite-rust in Claude Code?
Run npx skills add appwrite/skills --skill appwrite-rust -a claude-code. You can also place the skill file directly into ~/.claude/skills/appwrite-rust/SKILL.md.
What does the appwrite-rust skill do?
It guides AI models on using the Appwrite Rust SDK for server-side code. It demonstrates Tokio client configuration, TablesDB operations, and Query filtering patterns.
Why does the skill prefer TablesDB over Databases?
The skill marks Databases as a legacy API and reserves it solely for existing codebases. New implementations are instructed to use TablesDB with modern column types.
Source text
SKILL.md · BSD-3-Clause
---
name: appwrite-rust
description: Appwrite Rust SDK skill. Use when building server-side Rust applications with Appwrite. Covers async client setup with API keys, user management, TablesDB database/table/row operations, file storage, function executions, permissions, queries, and error handling. Uses the crates.io `appwrite` package and Tokio.
---
# Appwrite Rust SDK
## Installation
```bash
cargo add appwrite
cargo add tokio --features full
cargo add serde_json
```
## Setting Up the Client
The Rust SDK is async. Use it from a Tokio runtime and authenticate server-side with an API key.
```rust
use appwrite::Client;
use std::env;
let client = Client::new()
.set_endpoint("https://<REGION>.cloud.appwrite.io/v1")
.set_project(env::var("APPWRITE_PROJECT_ID")?)
.set_key(env::var("APPWRITE_API_KEY")?);
```
### Complete server skeleton
```rust
use appwrite::query::Query;
use appwrite::services::Users;
use appwrite::Client;
use std::env;
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
let client = Client::new()
.set_endpoint("https://<REGION>.cloud.appwrite.io/v1")
.set_project(env::var("APPWRITE_PROJECT_ID")?)
.set_key(env::var("APPWRITE_API_KEY")?);
let users = Users::new(&client);
let _list = users
.list(Some(vec![Query::limit(25).to_string()]), None, Some(true))
.await?;
Ok(())
}
```
## Code Examples
### User Management
```rust
use appwrite::id::ID;
use appwrite::query::Query;
use appwrite::services::Users;
let users = Users::new(&client);
// Create user
let _user = users
.create(
ID::unique(),
Some("[email protected]"),
None,
Some("password123"),
Some("User Name"),
)
.await?;
// List users
let _list = users
.list(Some(vec![Query::limit(25).to_string()]), None, Some(true))
.await?;
// Get user
let _fetched = users.get("[USER_ID]").await?;
// Delete user
users.delete("[USER_ID]").await?;
```
### Database Operations
> **Note:** Use `TablesDB` for new code. Only use `Databases` if the existing project explicitly depends on the legacy Databases API.
>
> **Rust SDK calling convention:** Methods are async, use positional parameters, and represent optional API parameters as `Option<T>`. Pass `None` for optional values you are not setting.
```rust
use appwrite::id::ID;
use appwrite::permission::Permission;
use appwrite::query::Query;
use appwrite::role::Role;
use appwrite::services::TablesDB;
use serde_json::json;
let tables_db = TablesDB::new(&client);
// Create database
let _database = tables_db
.create(ID::unique(), "My Database", Some(true))
.await?;
// Create table
let _table = tables_db
.create_table(
"[DATABASE_ID]",
ID::unique(),
"articles",
Some(vec![
Permission::read(Role::any()).to_string(),
Permission::create(Role::users(None)).to_string(),
]),
Some(true),
Some(true),
None,
None,
)
.await?;
// Create row
let _row = tables_db
.create_row(
"[DATABASE_ID]",
"[TABLE_ID]",
ID::unique(),
json!({
"title": "Hello World",
"done": false
}),
None,
None,
)
.await?;
// Query rows
let _rows = tables_db
.list_rows(
"[DATABASE_ID]",
"[TABLE_ID]",
Some(vec![
Query::equal("done", false).to_string(),
Query::limit(10).to_string(),
]),
None,
Some(true),
None,
)
.await?;
// Get row
let _row = tables_db
.get_row("[DATABASE_ID]", "[TABLE_ID]", "[ROW_ID]", None, None)
.await?;
// Update row
let _updated = tables_db
.update_row(
"[DATABASE_ID]",
"[TABLE_ID]",
"[ROW_ID]",
Some(json!({ "done": true })),
None,
None,
)
.await?;
// Delete row
tables_db
.delete_row("[DATABASE_ID]", "[TABLE_ID]", "[ROW_ID]", None)
.await?;
```
#### String Column Types
> **Note:** The legacy `string` column type is deprecated. Use explicit string column types for new tables.
| 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 toward the 64 KB row size limit. Prefer it for short, indexed fields like names, slugs, and identifiers.
- `text`, `mediumtext`, and `longtext` are stored off-page, so they do not consume the row size budget. `size` is not required for these types.
```rust
use appwrite::enums::{OrderBy, TablesDBIndexType};
// Short, indexed string
let _title = tables_db
.create_varchar_column(
"[DATABASE_ID]",
"[TABLE_ID]",
"title",
255,
true,
None,
None,
None,
)
.await?;
// Off-page longer text
let _summary = tables_db
.create_text_column("[DATABASE_ID]", "[TABLE_ID]", "summary", false, None, None, None)
.await?;
let _body = tables_db
.create_mediumtext_column("[DATABASE_ID]", "[TABLE_ID]", "body", false, None, None, None)
.await?;
let _raw_data = tables_db
.create_longtext_column("[DATABASE_ID]", "[TABLE_ID]", "raw_data", false, None, None, None)
.await?;
// Index only the varchar column fully.
let _index = tables_db
.create_index(
"[DATABASE_ID]",
"[TABLE_ID]",
"title_idx",
TablesDBIndexType::Key,
vec!["title"],
Some(vec![OrderBy::Asc]),
Some(vec![255]),
)
.await?;
```
### Query Methods
`TablesDB` methods accept `Option<Vec<String>>` for queries. Convert each `Query` to a string.
```rust
use appwrite::query::Query;
use serde_json::Value;
let queries = vec![
Query::equal("status", "published").to_string(),
Query::not_equal("archived", true).to_string(),
Query::greater_than("views", 100).to_string(),
Query::less_than_equal("priority", 5).to_string(),
Query::between("score", 1, 100).to_string(),
Query::is_not_null("publishedAt").to_string(),
Query::search("title", "rust appwrite").to_string(),
Query::contains("tags", "rust").to_string(),
Query::order_desc("$createdAt").to_string(),
Query::limit(25).to_string(),
Query::offset(50).to_string(),
];
let multi_value = Query::equal(
"status",
Value::Array(vec![
Value::String("draft".to_string()),
Value::String("published".to_string()),
]),
)
.to_string();
let related = Query::relationship(
"comments",
vec![
Query::equal("approved", true).to_string(),
Query::limit(25).to_string(),
],
)
.to_string();
// Aggregations (pass "" for no alias)
let aggregations = vec![
Query::count("*", "total").to_string(),
Query::sum("amount", "revenue").to_string(),
Query::avg("score", "avgScore").to_string(),
Query::min("price", "minPrice").to_string(),
Query::max("price", "maxPrice").to_string(),
];
// Grouping
let grouping = vec![
Query::group_by(["status", "year"]).to_string(),
Query::having([Query::greater_than("total", 1).to_string()]).to_string(),
Query::distinct().to_string(),
];
// Joins (table id, left key, right key, operator, alias; "" for no alias)
let joins = vec![
Query::join("orders", "$id", "customerId", "=", "").to_string(),
Query::left_join("orders", "$id", "customerId", "=", "ord").to_string(),
Query::left_join_on(
"orders",
"ord",
[
Query::on("$id", "customerId", "=").to_string(),
Query::equal("ord.status", "paid").to_string(),
],
)
.to_string(),
];
// Fluent builder. Pass build() into list methods.
let queries = Query::builder()
.equal("status", "published")
.left_join("orders", "$id", "customerId", "=", "ord")
.count("*", "total")
.group_by(["status"])
.limit(25)
.build();
let _rows = tables_db
.list_rows("[DATABASE_ID]", "[TABLE_ID]", Some(queries), None, None, None)
.await?;
```
### File Storage
```rust
use appwrite::id::ID;
use appwrite::input_file::InputFile;
use appwrite::permission::Permission;
use appwrite::query::Query;
use appwrite::role::Role;
use appwrite::services::Storage;
let storage = Storage::new(&client);
// Create bucket
let _bucket = storage
.create_bucket(
ID::unique(),
"Uploads",
Some(vec![
Permission::read(Role::any()).to_string(),
Permission::create(Role::users(None)).to_string(),
]),
Some(true),
Some(true),
Some(30_000_000),
Some(vec!["jpg".to_string(), "png".to_string(), "pdf".to_string()]),
None,
Some(true),
Some(true),
Some(true),
)
.await?;
// Upload from disk
let input = InputFile::from_path("avatar.png", Some("image/png")).await?;
let _file = storage
.create_file(
"[BUCKET_ID]",
ID::unique(),
input,
Some(vec![Permission::read(Role::any()).to_string()]),
)
.await?;
// Upload from bytes
let input = InputFile::from_bytes(b"hello".to_vec(), "hello.txt", Some("text/plain"));
let _file = storage.create_file("[BUCKET_ID]", ID::unique(), input, None).await?;
// List files
let _files = storage
.list_files("[BUCKET_ID]", Some(vec![Query::limit(10).to_string()]), None, Some(true))
.await?;
// Download file bytes
let _content = storage
.get_file_download("[BUCKET_ID]", "[FILE_ID]", None)
.await?;
// Delete file
storage.delete_file("[BUCKET_ID]", "[FILE_ID]").await?;
```
### Functions
```rust
use appwrite::enums::ExecutionMethod;
use appwrite::query::Query;
use appwrite::services::Functions;
use serde_json::json;
let functions = Functions::new(&client);
// List functions
let _functions = functions
.list(Some(vec![Query::limit(25).to_string()]), None, Some(true))
.await?;
// Execute function
let _execution = functions
.create_execution(
"[FUNCTION_ID]",
Some(r#"{"hello":"world"}"#),
Some(false),
Some("/jobs/sync"),
Some(ExecutionMethod::POST),
Some(json!({ "content-type": "application/json" })),
None,
)
.await?;
// Get execution
let _execution = functions
.get_execution("[FUNCTION_ID]", "[EXECUTION_ID]")
.await?;
```
### Permissions
```rust
use appwrite::permission::Permission;
use appwrite::role::Role;
let permissions = vec![
Permission::read(Role::any()).to_string(),
Permission::create(Role::users(None)).to_string(),
Permission::update(Role::user("[USER_ID]", None)).to_string(),
Permission::delete(Role::team("[TEAM_ID]", Some("owner"))).to_string(),
];
```
### Error Handling
```rust
match users.get("[USER_ID]").await {
Ok(user) => {
let _user = user;
}
Err(error) if error.status_code() == 404 => {
eprintln!("User not found: {}", error.get_message());
}
Err(error) => {
eprintln!("Appwrite error {}: {}", error.status_code(), error.get_message());
return Err(Box::new(error) as Box<dyn std::error::Error>);
}
}
```
## Common Pitfalls
- The Rust SDK is currently a server-side SDK. Prefer TypeScript/Web, Flutter, Apple, Android, or React Native SDKs for browser/mobile client auth flows.
- Always `await` service calls.
- Pass `None` for optional parameters you are not using.
- Use `TablesDB`, not legacy `Databases`, for new database code.
- Convert queries with `.to_string()` before passing them to APIs that expect `Option<Vec<String>>`.
- Use `serde_json::json!({...})` for row data and JSON bodies.
- Use `InputFile::from_path(...).await?` or `InputFile::from_bytes(...)` for uploads.
© appwrite · BSD-3-Clause · appwrite/skills