← AppwriteCONTENT HISTORY

Update to Appwrite

Snapshot Sep 30, 2026 · 23:07 UTC · version 1.0.0

Collection source: not recorded for this historical snapshot.

WHAT CHANGED · RULE-BASED ANALYSIS

First saved snapshot

No earlier snapshot is available to establish a change.

Compare saved observations

Download comparison JSON
Full technical diff · 0 changed fields
Full snapshot data
{
  "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.",
  "included_files": [],
  "skill_md_contents": "---\r\nname: appwrite-swift\r\ndescription: 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.\r\n---\r\n\r\n\r\n# Appwrite Swift SDK\r\n\r\n## Installation\r\n\r\n```swift\r\n// Swift Package Manager — Package.swift\r\n.package(url: \"https://github.com/appwrite/sdk-for-swift\", branch: \"main\")\r\n```\r\n\r\n## Setting Up the Client\r\n\r\n### Client-side (Apple platforms)\r\n\r\n```swift\r\nimport Appwrite\r\n\r\nlet client = Client()\r\n    .setEndpoint(\"https://<REGION>.cloud.appwrite.io/v1\")\r\n    .setProject(\"[PROJECT_ID]\")\r\n```\r\n\r\n### Server-side (Swift)\r\n\r\n```swift\r\nimport Appwrite\r\n\r\nlet client = Client()\r\n    .setEndpoint(\"https://<REGION>.cloud.appwrite.io/v1\")\r\n    .setProject(ProcessInfo.processInfo.environment[\"APPWRITE_PROJECT_ID\"]!)\r\n    .setKey(ProcessInfo.processInfo.environment[\"APPWRITE_API_KEY\"]!)\r\n```\r\n\r\n## Code Examples\r\n\r\n### Authentication (client-side)\r\n\r\n```swift\r\nlet account = Account(client)\r\n\r\n// Signup\r\nlet user = try await account.create(userId: ID.unique(), email: \"user@example.com\", password: \"password123\", name: \"User Name\")\r\n\r\n// Login\r\nlet session = try await account.createEmailPasswordSession(email: \"user@example.com\", password: \"password123\")\r\n\r\n// OAuth\r\ntry await account.createOAuth2Session(provider: .google)\r\n\r\n// Get current user\r\nlet me = try await account.get()\r\n\r\n// Logout\r\ntry await account.deleteSession(sessionId: \"current\")\r\n```\r\n\r\n### User Management (server-side)\r\n\r\n```swift\r\nlet users = Users(client)\r\n\r\n// Create user\r\nlet user = try await users.create(userId: ID.unique(), email: \"user@example.com\", password: \"password123\", name: \"User Name\")\r\n\r\n// List users\r\nlet list = try await users.list(queries: [Query.limit(25)])\r\n\r\n// Get user\r\nlet fetched = try await users.get(userId: \"[USER_ID]\")\r\n\r\n// Delete user\r\ntry await users.delete(userId: \"[USER_ID]\")\r\n```\r\n\r\n### Database Operations\r\n\r\n> **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.\r\n>\r\n> **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.\r\n\r\n```swift\r\nlet tablesDB = TablesDB(client)\r\n\r\n// Create database (server-side only)\r\nlet db = try await tablesDB.create(databaseId: ID.unique(), name: \"My Database\")\r\n\r\n// Create row\r\nlet doc = try await tablesDB.createRow(databaseId: \"[DATABASE_ID]\", tableId: \"[TABLE_ID]\", rowId: ID.unique(), data: [\r\n    \"title\": \"Hello\",\r\n    \"done\": false\r\n])\r\n\r\n// Query rows\r\nlet results = try await tablesDB.listRows(databaseId: \"[DATABASE_ID]\", tableId: \"[TABLE_ID]\", queries: [\r\n    Query.equal(\"done\", value: false),\r\n    Query.limit(10)\r\n])\r\n\r\n// Get row\r\nlet row = try await tablesDB.getRow(databaseId: \"[DATABASE_ID]\", tableId: \"[TABLE_ID]\", rowId: \"[ROW_ID]\")\r\n\r\n// Update row\r\ntry await tablesDB.updateRow(databaseId: \"[DATABASE_ID]\", tableId: \"[TABLE_ID]\", rowId: \"[ROW_ID]\", data: [\"done\": true])\r\n\r\n// Delete row\r\ntry await tablesDB.deleteRow(databaseId: \"[DATABASE_ID]\", tableId: \"[TABLE_ID]\", rowId: \"[ROW_ID]\")\r\n```\r\n\r\n#### String Column Types\r\n\r\n> **Note:** The legacy `string` type is deprecated. Use explicit column types for all new columns.\r\n\r\n| Type | Max characters | Indexing | Storage |\r\n|------|---------------|----------|---------|\r\n| `varchar` | 16,383 | Full index (if size ≤ 768) | Inline in row |\r\n| `text` | 16,383 | Prefix only | Off-page |\r\n| `mediumtext` | 4,194,303 | Prefix only | Off-page |\r\n| `longtext` | 1,073,741,823 | Prefix only | Off-page |\r\n\r\n- `varchar` is stored inline and counts towards the 64 KB row size limit. Prefer for short, indexed fields like names, slugs, or identifiers.\r\n- `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.\r\n\r\n```swift\r\n// Create table with explicit string column types\r\ntry await tablesDB.createTable(\r\n    databaseId: \"[DATABASE_ID]\",\r\n    tableId: ID.unique(),\r\n    name: \"articles\",\r\n    columns: [\r\n        [\"key\": \"title\",    \"type\": \"varchar\",    \"size\": 255, \"required\": true],\r\n        [\"key\": \"summary\",  \"type\": \"text\",                    \"required\": false],\r\n        [\"key\": \"body\",     \"type\": \"mediumtext\",              \"required\": false],\r\n        [\"key\": \"raw_data\", \"type\": \"longtext\",                \"required\": false],\r\n    ]\r\n)\r\n```\r\n\r\n### Query Methods\r\n\r\n```swift\r\n// Filtering\r\nQuery.equal(\"field\", value: \"value\")          // == (or pass array for IN)\r\nQuery.notEqual(\"field\", value: \"value\")       // !=\r\nQuery.lessThan(\"field\", value: 100)           // <\r\nQuery.lessThanEqual(\"field\", value: 100)      // <=\r\nQuery.greaterThan(\"field\", value: 100)        // >\r\nQuery.greaterThanEqual(\"field\", value: 100)   // >=\r\nQuery.between(\"field\", start: 1, end: 100)    // 1 <= field <= 100\r\nQuery.isNull(\"field\")                         // is null\r\nQuery.isNotNull(\"field\")                      // is not null\r\nQuery.startsWith(\"field\", value: \"prefix\")    // starts with\r\nQuery.endsWith(\"field\", value: \"suffix\")      // ends with\r\nQuery.contains(\"field\", value: \"sub\")         // contains\r\nQuery.search(\"field\", value: \"keywords\")      // full-text search (requires index)\r\n\r\n// Sorting\r\nQuery.orderAsc(\"field\")\r\nQuery.orderDesc(\"field\")\r\n\r\n// Pagination\r\nQuery.limit(25)                               // max rows (default 25, max 100)\r\nQuery.offset(0)                               // skip N rows\r\nQuery.cursorAfter(\"[ROW_ID]\")                 // cursor pagination (preferred)\r\nQuery.cursorBefore(\"[ROW_ID]\")\r\n\r\n// Selection & Logic\r\nQuery.select([\"field1\", \"field2\"])\r\nQuery.or([Query.equal(\"a\", value: 1), Query.equal(\"b\", value: 2)])   // OR\r\nQuery.and([Query.greaterThan(\"age\", value: 18), Query.lessThan(\"age\", value: 65)])  // AND (default)\r\n```\r\n\r\n### File Storage\r\n\r\n```swift\r\nlet storage = Storage(client)\r\n\r\n// Upload file\r\nlet file = try await storage.createFile(bucketId: \"[BUCKET_ID]\", fileId: ID.unique(), file: InputFile.fromPath(\"/path/to/file.png\"))\r\n\r\n// List files\r\nlet files = try await storage.listFiles(bucketId: \"[BUCKET_ID]\")\r\n\r\n// Delete file\r\ntry await storage.deleteFile(bucketId: \"[BUCKET_ID]\", fileId: \"[FILE_ID]\")\r\n```\r\n\r\n#### InputFile Factory Methods\r\n\r\n```swift\r\nInputFile.fromPath(\"/path/to/file.png\")                    // from filesystem path\r\nInputFile.fromData(data, filename: \"file.png\", mimeType: \"image/png\")  // from Data\r\n```\r\n\r\n### Teams\r\n\r\n```swift\r\nlet teams = Teams(client)\r\n\r\n// Create team\r\nlet team = try await teams.create(teamId: ID.unique(), name: \"Engineering\")\r\n\r\n// List teams\r\nlet list = try await teams.list()\r\n\r\n// Create membership (invite user by email)\r\nlet membership = try await teams.createMembership(\r\n    teamId: \"[TEAM_ID]\",\r\n    roles: [\"editor\"],\r\n    email: \"user@example.com\"\r\n)\r\n\r\n// List memberships\r\nlet members = try await teams.listMemberships(teamId: \"[TEAM_ID]\")\r\n\r\n// Update membership roles\r\ntry await teams.updateMembership(teamId: \"[TEAM_ID]\", membershipId: \"[MEMBERSHIP_ID]\", roles: [\"admin\"])\r\n\r\n// Delete team\r\ntry await teams.delete(teamId: \"[TEAM_ID]\")\r\n```\r\n\r\n> **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.\r\n\r\n### Real-time Subscriptions (client-side)\r\n\r\n```swift\r\nlet realtime = Realtime(client)\r\n\r\n// Subscribe to row changes\r\nlet subscription = try await realtime.subscribe(channels: [\r\n    Channel.tablesdb(\"[DATABASE_ID]\").table(\"[TABLE_ID]\").row()\r\n]) { response in\r\n    print(response.events)   // e.g. [\"tablesdb.*.tables.*.rows.*.create\"]\r\n    print(response.payload)  // the affected resource\r\n}\r\n\r\n// Subscribe to multiple channels\r\nlet multi = try await realtime.subscribe(channels: [\r\n    Channel.tablesdb(\"[DATABASE_ID]\").table(\"[TABLE_ID]\").row(),\r\n    Channel.files(),\r\n]) { response in /* ... */ }\r\n\r\n// Cleanup\r\ntry await subscription.close()\r\n```\r\n\r\n**Available channels:**\r\n\r\n| Channel | Description |\r\n|---------|-------------|\r\n| `account` | Changes to the authenticated user's account |\r\n| `tablesdb.[DB_ID].tables.[TABLE_ID].rows` | All rows in a table |\r\n| `tablesdb.[DB_ID].tables.[TABLE_ID].rows.[ROW_ID]` | A specific row |\r\n| `buckets.[BUCKET_ID].files` | All files in a bucket |\r\n| `buckets.[BUCKET_ID].files.[FILE_ID]` | A specific file |\r\n| `teams` | Changes to teams the user belongs to |\r\n| `teams.[TEAM_ID]` | A specific team |\r\n| `memberships` | The user's team memberships |\r\n| `functions.[FUNCTION_ID].executions` | Function execution updates |\r\n\r\nResponse fields: `events` (array), `payload` (resource), `channels` (matched), `timestamp` (ISO 8601).\r\n\r\n### Serverless Functions (server-side)\r\n\r\n```swift\r\nlet functions = Functions(client)\r\n\r\n// Execute function\r\nlet execution = try await functions.createExecution(functionId: \"[FUNCTION_ID]\", body: \"{\\\"key\\\": \\\"value\\\"}\")\r\n\r\n// List executions\r\nlet executions = try await functions.listExecutions(functionId: \"[FUNCTION_ID]\")\r\n```\r\n\r\n#### Writing a Function Handler (Swift runtime)\r\n\r\n```swift\r\n// Sources/main.swift — Appwrite Function entry point\r\nfunc main(context: RuntimeContext) async throws -> RuntimeOutput {\r\n    // context.req.body        — raw body (String)\r\n    // context.req.bodyJson    — parsed JSON ([String: Any]?)\r\n    // context.req.headers     — headers ([String: String])\r\n    // context.req.method      — HTTP method\r\n    // context.req.path        — URL path\r\n    // context.req.query       — query params ([String: String])\r\n\r\n    context.log(\"Processing: \\(context.req.method) \\(context.req.path)\")\r\n\r\n    if context.req.method == \"GET\" {\r\n        return context.res.json([\"message\": \"Hello from Appwrite Function!\"])\r\n    }\r\n\r\n    return context.res.json([\"success\": true])       // JSON\r\n    // context.res.text(\"Hello\")                     // plain text\r\n    // context.res.empty()                           // 204\r\n    // context.res.redirect(\"https://...\")            // 302\r\n}\r\n```\r\n\r\n### Server-Side Rendering (SSR) Authentication\r\n\r\nSSR apps using server-side Swift (Vapor, Hummingbird, etc.) use the **server SDK** to handle auth. You need two clients:\r\n\r\n- **Admin client** — uses an API key, creates sessions, bypasses rate limits (reusable singleton)\r\n- **Session client** — uses a session cookie, acts on behalf of a user (create per-request, never share)\r\n\r\n```swift\r\nimport Appwrite\r\n\r\n// Admin client (reusable)\r\nlet adminClient = Client()\r\n    .setEndpoint(\"https://<REGION>.cloud.appwrite.io/v1\")\r\n    .setProject(\"[PROJECT_ID]\")\r\n    .setKey(Environment.get(\"APPWRITE_API_KEY\")!)\r\n\r\n// Session client (create per-request)\r\nlet sessionClient = Client()\r\n    .setEndpoint(\"https://<REGION>.cloud.appwrite.io/v1\")\r\n    .setProject(\"[PROJECT_ID]\")\r\n\r\nif let session = req.cookies[\"a_session_[PROJECT_ID]\"]?.string {\r\n    sessionClient.setSession(session)\r\n}\r\n```\r\n\r\n#### Email/Password Login (Vapor)\r\n\r\n```swift\r\napp.post(\"login\") { req async throws -> Response in\r\n    let body = try req.content.decode(LoginRequest.self)\r\n    let account = Account(adminClient)\r\n    let session = try await account.createEmailPasswordSession(\r\n        email: body.email,\r\n        password: body.password\r\n    )\r\n\r\n    // Cookie name must be a_session_<PROJECT_ID>\r\n    let response = Response(status: .ok, body: .init(string: \"{\\\"success\\\": true}\"))\r\n    response.cookies[\"a_session_[PROJECT_ID]\"] = HTTPCookies.Value(\r\n        string: session.secret,\r\n        isHTTPOnly: true,\r\n        isSecure: true,\r\n        sameSite: .strict,\r\n        path: \"/\"\r\n    )\r\n    return response\r\n}\r\n```\r\n\r\n#### Authenticated Requests\r\n\r\n```swift\r\napp.get(\"user\") { req async throws -> Response in\r\n    guard let session = req.cookies[\"a_session_[PROJECT_ID]\"]?.string else {\r\n        throw Abort(.unauthorized)\r\n    }\r\n\r\n    let sessionClient = Client()\r\n        .setEndpoint(\"https://<REGION>.cloud.appwrite.io/v1\")\r\n        .setProject(\"[PROJECT_ID]\")\r\n        .setSession(session)\r\n\r\n    let account = Account(sessionClient)\r\n    let user = try await account.get()\r\n    // Return user as JSON\r\n}\r\n```\r\n\r\n#### OAuth2 SSR Flow\r\n\r\n```swift\r\n// Step 1: Redirect to OAuth provider\r\napp.get(\"oauth\") { req async throws -> Response in\r\n    let account = Account(adminClient)\r\n    let redirectUrl = try await account.createOAuth2Token(\r\n        provider: .github,\r\n        success: \"https://example.com/oauth/success\",\r\n        failure: \"https://example.com/oauth/failure\"\r\n    )\r\n    return req.redirect(to: redirectUrl)\r\n}\r\n\r\n// Step 2: Handle callback — exchange token for session\r\napp.get(\"oauth\", \"success\") { req async throws -> Response in\r\n    let userId = try req.query.get(String.self, at: \"userId\")\r\n    let secret = try req.query.get(String.self, at: \"secret\")\r\n\r\n    let account = Account(adminClient)\r\n    let session = try await account.createSession(userId: userId, secret: secret)\r\n\r\n    let response = Response(status: .ok, body: .init(string: \"{\\\"success\\\": true}\"))\r\n    response.cookies[\"a_session_[PROJECT_ID]\"] = HTTPCookies.Value(\r\n        string: session.secret,\r\n        isHTTPOnly: true, isSecure: true, sameSite: .strict, path: \"/\"\r\n    )\r\n    return response\r\n}\r\n```\r\n\r\n> **Cookie security:** Always use `isHTTPOnly`, `isSecure`, and `sameSite: .strict` to prevent XSS. The cookie name must be `a_session_<PROJECT_ID>`.\r\n\r\n> **Forwarding user agent:** Call `sessionClient.setForwardedUserAgent(req.headers.first(name: .userAgent) ?? \"\")` to record the end-user's browser info for debugging and security.\r\n\r\n## Error Handling\r\n\r\n```swift\r\nimport Appwrite\r\n// AppwriteException is included in the main module\r\n\r\ndo {\r\n    let row = try await tablesDB.getRow(databaseId: \"[DATABASE_ID]\", tableId: \"[TABLE_ID]\", rowId: \"[ROW_ID]\")\r\n} catch let error as AppwriteException {\r\n    print(error.message)     // human-readable message\r\n    print(error.code)        // HTTP status code (Int)\r\n    print(error.type)        // error type (e.g. \"document_not_found\")\r\n    print(error.response)    // full response body\r\n}\r\n```\r\n\r\n**Common error codes:**\r\n\r\n| Code | Meaning |\r\n|------|---------|\r\n| `401` | Unauthorized — missing or invalid session/API key |\r\n| `403` | Forbidden — insufficient permissions |\r\n| `404` | Not found — resource does not exist |\r\n| `409` | Conflict — duplicate ID or unique constraint |\r\n| `429` | Rate limited — too many requests |\r\n\r\n## Permissions & Roles (Critical)\r\n\r\nAppwrite 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.\r\n\r\n```swift\r\nimport Appwrite\r\n// Permission and Role are included in the main module import\r\n```\r\n\r\n### Database Row with Permissions\r\n\r\n```swift\r\nlet doc = try await tablesDB.createRow(\r\n    databaseId: \"[DATABASE_ID]\",\r\n    tableId: \"[TABLE_ID]\",\r\n    rowId: ID.unique(),\r\n    data: [\"title\": \"Hello World\"],\r\n    permissions: [\r\n        Permission.read(Role.user(\"[USER_ID]\")),     // specific user can read\r\n        Permission.update(Role.user(\"[USER_ID]\")),   // specific user can update\r\n        Permission.read(Role.team(\"[TEAM_ID]\")),     // all team members can read\r\n        Permission.read(Role.any()),                 // anyone (including guests) can read\r\n    ]\r\n)\r\n```\r\n\r\n### File Upload with Permissions\r\n\r\n```swift\r\nlet file = try await storage.createFile(\r\n    bucketId: \"[BUCKET_ID]\",\r\n    fileId: ID.unique(),\r\n    file: InputFile.fromPath(\"/path/to/file.png\"),\r\n    permissions: [\r\n        Permission.read(Role.any()),\r\n        Permission.update(Role.user(\"[USER_ID]\")),\r\n        Permission.delete(Role.user(\"[USER_ID]\")),\r\n    ]\r\n)\r\n```\r\n\r\n> **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.\r\n\r\n> **Common mistakes:**\r\n> - **Forgetting permissions** — the resource becomes inaccessible to all users (including the creator)\r\n> - **`Role.any()` with `write`/`update`/`delete`** — allows any user, including unauthenticated guests, to modify or remove the resource\r\n> - **`Permission.read(Role.any())` on sensitive data** — makes the resource publicly readable\r\n\r\n"
}

SHA-256: a2b3859588a0b86bdbf58a87d460f9e1c581413bcad86e2eabd80cb95df8f42d