appwrite-swift — скилл для Claude Code: что делает и как установить
Настраивает работу с Appwrite Swift SDK в приложениях под iOS, macOS или сервер. Помогает писать запросы к TablesDB, настраивать сессии и управлять пользователями.
npx skills add appwrite/skills --skill appwrite-swift -a claude-codeЧто делает appwrite-swift
Скилл обучает ИИ-агента писать современный код под Appwrite Swift SDK без устаревших методов. Для работы с базой данных здесь используется новый класс TablesDB, а не старый Databases. Агент сразу подставляет именованные параметры и точные типы колонок: varchar для коротких индексируемых полей и text или mediumtext для данных вне строки таблицы.
В клиентском коде для Apple-платформ скилл генерирует авторизацию по email или через OAuth с использованием async/await. Для серверного Swift агент передаёт ключ доступа через APPWRITE_API_KEY, открывая методы администрирования через Users. Без серверного ключа административные операции с аккаунтами выполнить не удастся.
Какие задачи решает
- настроить клиентскую авторизацию по email и OAuth в приложении на Swift
- построить типобезопасные запросы к базе через
TablesDBиQuery.builder - реализовать серверное администрирование пользователей с API-ключом
- спроектировать структуру таблицы с явными типами
varchar,textилиmediumtext
Примеры запросов агенту
- Напиши Swift-код для авторизации пользователя через OAuth Google в Appwrite
- Создай запрос к TablesDB с фильтрацией по статусу и пагинацией через курсор
- Добавь серверный скрипт на Swift для удаления пользователя по ID через класс Users
Как установить appwrite-swift
1Через Skills CLI (Vercel)
npx skills add appwrite/skills --skill appwrite-swift -a claude-code2Вручную: положить файлы
~/.claude/skills/appwrite-swift/SKILL.mdПапка скилла целиком; для одного проекта — .claude/skills/appwrite-swift/
1Через Skills CLI (Vercel)
npx skills add appwrite/skills --skill appwrite-swift -a codex2Вручную: положить файлы
.agents/skills/appwrite-swift/SKILL.mdДля всех проектов — ~/.agents/skills/
1Через Skills CLI (Vercel)
npx skills add appwrite/skills --skill appwrite-swift -a cursor2Вручную: положить файлы
.cursor/skills/appwrite-swift/SKILL.mdCursor читает и .agents/skills/, и .claude/skills/
1GitHub CLI
gh skill install appwrite/skills appwrite-swift2Вручную: положить файлы
.github/skills/appwrite-swift/SKILL.md1Через Skills CLI (Vercel)
npx skills add appwrite/skills --skill appwrite-swift -a gemini-cli2Командой платформы
gemini skills install https://github.com/appwrite/skills --path skills/appwrite-swiftВручную: положить файлы
.devin/skills/appwrite-swift/SKILL.mdСтарый путь .windsurf/skills/ тоже читается
Через Skills CLI (Vercel)
npx skills add appwrite/skills --skill appwrite-swift -a opencodeClaude.ai
Упакуйте папку в zip и загрузите в Settings → Capabilities → Skills
По шагам, с командами для каждой платформы: Как установить скилл в Claude Code: пять способов
Вопросы о appwrite-swift
Как установить appwrite-swift в Claude Code?
Выполните команду npx skills add appwrite/skills --skill appwrite-swift -a claude-code. Файл навыка появится по пути ~/.claude/skills/appwrite-swift/SKILL.md.
Что делает скилл appwrite-swift?
Он даёт ассистенту правила и примеры для работы с Appwrite Swift SDK. Агент учится вызывать методы TablesDB, настраивать сессии Account и управлять пользователями на сервере.
Какой класс базы данных используется вместо Databases?
Скилл предписывает использовать TablesDB для всего нового кода, поскольку класс Databases объявлен устаревшим. Также вместо типа string задаются явные типы вроде varchar или mediumtext.
Исходный текст
SKILL.md · BSD-3-Clause
---
name: appwrite-swift
description: Appwrite Swift SDK skill. Use when building native iOS, macOS, watchOS, or tvOS apps, or server-side Swift applications with Appwrite. Covers client-side auth (email, OAuth), database queries, file uploads, real-time subscriptions with async/await, and server-side admin via API keys for user management, database administration, storage, and functions.
---
# Appwrite Swift SDK
## Installation
```swift
// Swift Package Manager — Package.swift
.package(url: "https://github.com/appwrite/sdk-for-swift", branch: "main")
```
## Setting Up the Client
### Client-side (Apple platforms)
```swift
import Appwrite
let client = Client()
.setEndpoint("https://<REGION>.cloud.appwrite.io/v1")
.setProject("[PROJECT_ID]")
```
### Server-side (Swift)
```swift
import Appwrite
let client = Client()
.setEndpoint("https://<REGION>.cloud.appwrite.io/v1")
.setProject(ProcessInfo.processInfo.environment["APPWRITE_PROJECT_ID"]!)
.setKey(ProcessInfo.processInfo.environment["APPWRITE_API_KEY"]!)
```
## Code Examples
### Authentication (client-side)
```swift
let account = Account(client)
// Signup
let user = try await account.create(userId: ID.unique(), email: "[email protected]", password: "password123", name: "User Name")
// Login
let session = try await account.createEmailPasswordSession(email: "[email protected]", password: "password123")
// OAuth
try await account.createOAuth2Session(provider: .google)
// Get current user
let me = try await account.get()
// Logout
try await account.deleteSession(sessionId: "current")
```
### User Management (server-side)
```swift
let users = Users(client)
// Create user
let user = try await users.create(userId: ID.unique(), email: "[email protected]", password: "password123", name: "User Name")
// List users
let list = try await users.list(queries: [Query.limit(25)])
// Get user
let fetched = try await users.get(userId: "[USER_ID]")
// Delete user
try 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.
>
> **Tip:** Prefer named parameters (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.
```swift
let tablesDB = TablesDB(client)
// Create database (server-side only)
let db = try await tablesDB.create(databaseId: ID.unique(), name: "My Database")
// Create row
let doc = try await tablesDB.createRow(databaseId: "[DATABASE_ID]", tableId: "[TABLE_ID]", rowId: ID.unique(), data: [
"title": "Hello",
"done": false
])
// Query rows
let results = try await tablesDB.listRows(databaseId: "[DATABASE_ID]", tableId: "[TABLE_ID]", queries: [
Query.equal("done", value: false),
Query.limit(10)
])
// Get row
let row = try await tablesDB.getRow(databaseId: "[DATABASE_ID]", tableId: "[TABLE_ID]", rowId: "[ROW_ID]")
// Update row
try await tablesDB.updateRow(databaseId: "[DATABASE_ID]", tableId: "[TABLE_ID]", rowId: "[ROW_ID]", data: ["done": true])
// Delete row
try await tablesDB.deleteRow(databaseId: "[DATABASE_ID]", tableId: "[TABLE_ID]", rowId: "[ROW_ID]")
```
#### String Column Types
> **Note:** The legacy `string` type 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 |
- `varchar` is stored inline and counts towards the 64 KB row size limit. Prefer for short, indexed fields like names, slugs, or identifiers.
- `text`, `mediumtext`, and `longtext` are stored off-page (only a 20-byte pointer lives in the row), so they don't consume the row size budget. `size` is not required for these types.
```swift
// Create table with explicit string column types
try await tablesDB.createTable(
databaseId: "[DATABASE_ID]",
tableId: ID.unique(),
name: "articles",
columns: [
["key": "title", "type": "varchar", "size": 255, "required": true],
["key": "summary", "type": "text", "required": false],
["key": "body", "type": "mediumtext", "required": false],
["key": "raw_data", "type": "longtext", "required": false],
]
)
```
### Query Methods
```swift
// Filtering
Query.equal("field", value: "value") // == (or pass array for IN)
Query.notEqual("field", value: "value") // !=
Query.lessThan("field", value: 100) // <
Query.lessThanEqual("field", value: 100) // <=
Query.greaterThan("field", value: 100) // >
Query.greaterThanEqual("field", value: 100) // >=
Query.between("field", start: 1, end: 100) // 1 <= field <= 100
Query.isNull("field") // is null
Query.isNotNull("field") // is not null
Query.startsWith("field", value: "prefix") // starts with
Query.endsWith("field", value: "suffix") // ends with
Query.contains("field", value: "sub") // contains
Query.search("field", value: "keywords") // full-text search (requires index)
// Sorting
Query.orderAsc("field")
Query.orderDesc("field")
// Pagination
Query.limit(25) // max rows (default 25, max 100)
Query.offset(0) // skip N rows
Query.cursorAfter("[ROW_ID]") // cursor pagination (preferred)
Query.cursorBefore("[ROW_ID]")
// Selection & Logic
Query.select(["field1", "field2"])
Query.or([Query.equal("a", value: 1), Query.equal("b", value: 2)]) // OR
Query.and([Query.greaterThan("age", value: 18), Query.lessThan("age", value: 65)]) // AND (default)
Query.relationship("comments", queries: [Query.equal("approved", value: true), Query.limit(25)]) // related documents
// Aggregations (optional alias)
Query.count("*", alias: "total")
Query.sum("amount", alias: "revenue")
Query.avg("score", alias: "avgScore")
Query.min("price", alias: "minPrice")
Query.max("price", alias: "maxPrice")
// Grouping
Query.groupBy(["status", "year"])
Query.having([Query.greaterThan("total", value: 1)])
Query.distinct()
// Joins (table id, left key, right key, operator, alias)
Query.join("orders", left: "$id", right: "customerId")
Query.leftJoin("orders", left: "$id", right: "customerId", operator: "=", alias: "ord")
Query.leftJoin("orders", alias: "ord", on: [
Query.on("$id", "customerId"),
Query.equal("ord.status", value: "paid"),
])
// Fluent builder. Pass build() into list methods.
let queries = Query.builder()
.equal("status", value: "published")
.leftJoin("orders", left: "$id", right: "customerId", operator: "=", alias: "ord")
.count("*", alias: "total")
.groupBy(["status"])
.limit(25)
.build()
_ = try await tablesDB.listRows(databaseId: "[DATABASE_ID]", tableId: "[TABLE_ID]", queries: queries)
```
### File Storage
```swift
let storage = Storage(client)
// Upload file
let file = try await storage.createFile(bucketId: "[BUCKET_ID]", fileId: ID.unique(), file: InputFile.fromPath("/path/to/file.png"))
// List files
let files = try await storage.listFiles(bucketId: "[BUCKET_ID]")
// Delete file
try await storage.deleteFile(bucketId: "[BUCKET_ID]", fileId: "[FILE_ID]")
```
#### InputFile Factory Methods
```swift
InputFile.fromPath("/path/to/file.png") // from filesystem path
InputFile.fromData(data, filename: "file.png", mimeType: "image/png") // from Data
```
### Teams
```swift
let teams = Teams(client)
// Create team
let team = try await teams.create(teamId: ID.unique(), name: "Engineering")
// List teams
let list = try await teams.list()
// Create membership (invite user by email)
let membership = try await teams.createMembership(
teamId: "[TEAM_ID]",
roles: ["editor"],
email: "[email protected]"
)
// List memberships
let members = try await teams.listMemberships(teamId: "[TEAM_ID]")
// Update membership roles
try await teams.updateMembership(teamId: "[TEAM_ID]", membershipId: "[MEMBERSHIP_ID]", roles: ["admin"])
// Delete team
try 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)
```swift
let realtime = Realtime(client)
// Subscribe to row changes
let subscription = try await realtime.subscribe(channels: [
Channel.tablesdb("[DATABASE_ID]").table("[TABLE_ID]").row()
]) { response in
print(response.events) // e.g. ["tablesdb.*.tables.*.rows.*.create"]
print(response.payload) // the affected resource
}
// Subscribe to multiple channels
let multi = try await realtime.subscribe(channels: [
Channel.tablesdb("[DATABASE_ID]").table("[TABLE_ID]").row(),
Channel.files(),
]) { response in /* ... */ }
// Cleanup
try 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]` | A specific team |
| `memberships` | The user's team memberships |
| `functions.[FUNCTION_ID].executions` | Function execution updates |
Response fields: `events` (array), `payload` (resource), `channels` (matched), `timestamp` (ISO 8601).
### Serverless Functions (server-side)
```swift
let functions = Functions(client)
// Execute function
let execution = try await functions.createExecution(functionId: "[FUNCTION_ID]", body: "{\"key\": \"value\"}")
// List executions
let executions = try await functions.listExecutions(functionId: "[FUNCTION_ID]")
```
#### Writing a Function Handler (Swift runtime)
```swift
// Sources/main.swift — Appwrite Function entry point
func main(context: RuntimeContext) async throws -> RuntimeOutput {
// context.req.body — raw body (String)
// context.req.bodyJson — parsed JSON ([String: Any]?)
// context.req.headers — headers ([String: String])
// context.req.method — HTTP method
// context.req.path — URL path
// context.req.query — query params ([String: String])
context.log("Processing: \(context.req.method) \(context.req.path)")
if context.req.method == "GET" {
return context.res.json(["message": "Hello from Appwrite Function!"])
}
return context.res.json(["success": true]) // JSON
// context.res.text("Hello") // plain text
// context.res.empty() // 204
// context.res.redirect("https://...") // 302
}
```
### Server-Side Rendering (SSR) Authentication
SSR apps using server-side Swift (Vapor, Hummingbird, etc.) use the **server SDK** 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)
```swift
import Appwrite
// Admin client (reusable)
let adminClient = Client()
.setEndpoint("https://<REGION>.cloud.appwrite.io/v1")
.setProject("[PROJECT_ID]")
.setKey(Environment.get("APPWRITE_API_KEY")!)
// Session client (create per-request)
let sessionClient = Client()
.setEndpoint("https://<REGION>.cloud.appwrite.io/v1")
.setProject("[PROJECT_ID]")
if let session = req.cookies["a_session_[PROJECT_ID]"]?.string {
sessionClient.setSession(session)
}
```
#### Email/Password Login (Vapor)
```swift
app.post("login") { req async throws -> Response in
let body = try req.content.decode(LoginRequest.self)
let account = Account(adminClient)
let session = try await account.createEmailPasswordSession(
email: body.email,
password: body.password
)
// Cookie name must be a_session_<PROJECT_ID>
let response = Response(status: .ok, body: .init(string: "{\"success\": true}"))
response.cookies["a_session_[PROJECT_ID]"] = HTTPCookies.Value(
string: session.secret,
isHTTPOnly: true,
isSecure: true,
sameSite: .strict,
path: "/"
)
return response
}
```
#### Authenticated Requests
```swift
app.get("user") { req async throws -> Response in
guard let session = req.cookies["a_session_[PROJECT_ID]"]?.string else {
throw Abort(.unauthorized)
}
let sessionClient = Client()
.setEndpoint("https://<REGION>.cloud.appwrite.io/v1")
.setProject("[PROJECT_ID]")
.setSession(session)
let account = Account(sessionClient)
let user = try await account.get()
// Return user as JSON
}
```
#### OAuth2 SSR Flow
```swift
// Step 1: Redirect to OAuth provider
app.get("oauth") { req async throws -> Response in
let account = Account(adminClient)
let redirectUrl = try await account.createOAuth2Token(
provider: .github,
success: "https://example.com/oauth/success",
failure: "https://example.com/oauth/failure"
)
return req.redirect(to: redirectUrl)
}
// Step 2: Handle callback — exchange token for session
app.get("oauth", "success") { req async throws -> Response in
let userId = try req.query.get(String.self, at: "userId")
let secret = try req.query.get(String.self, at: "secret")
let account = Account(adminClient)
let session = try await account.createSession(userId: userId, secret: secret)
let response = Response(status: .ok, body: .init(string: "{\"success\": true}"))
response.cookies["a_session_[PROJECT_ID]"] = HTTPCookies.Value(
string: session.secret,
isHTTPOnly: true, isSecure: true, sameSite: .strict, path: "/"
)
return response
}
```
> **Cookie security:** Always use `isHTTPOnly`, `isSecure`, and `sameSite: .strict` to prevent XSS. The cookie name must be `a_session_<PROJECT_ID>`.
> **Forwarding user agent:** Call `sessionClient.setForwardedUserAgent(req.headers.first(name: .userAgent) ?? "")` to record the end-user's browser info for debugging and security.
## Error Handling
```swift
import Appwrite
// AppwriteException is included in the main module
do {
let row = try await tablesDB.getRow(databaseId: "[DATABASE_ID]", tableId: "[TABLE_ID]", rowId: "[ROW_ID]")
} catch let error as AppwriteException {
print(error.message) // human-readable message
print(error.code) // HTTP status code (Int)
print(error.type) // error type (e.g. "document_not_found")
print(error.response) // full response body
}
```
**Common error codes:**
| Code | Meaning |
|------|---------|
| `401` | Unauthorized — missing or invalid session/API key |
| `403` | Forbidden — insufficient permissions |
| `404` | Not found — resource does not exist |
| `409` | Conflict — duplicate ID or unique constraint |
| `429` | Rate limited — too many requests |
## 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.
```swift
import Appwrite
// Permission and Role are included in the main module import
```
### Database Row with Permissions
```swift
let doc = try 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
```swift
let file = try await storage.createFile(
bucketId: "[BUCKET_ID]",
fileId: ID.unique(),
file: InputFile.fromPath("/path/to/file.png"),
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()` 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
© appwrite · BSD-3-Clause · appwrite/skills