mcpField
A
appwrite-swift

appwrite-swift

github.com/appwriteDevelopment28854 installsUpdated 1w ago
Repository

About appwrite-swift

appwrite-swift is the Appwrite SDK for Apple platform and Swift server applications. It supports client and server integrations for authentication, TablesDB, file storage, real-time subscriptions, and function execution, and includes SSR authentication examples.

Key features

  1. 01User authentication and management
  2. 02TablesDB operations and queries
  3. 03File storage and permissions
  4. 04Team management and real-time subscriptions
  5. 05Appwrite function execution

Use cases

  1. 01Add Appwrite authentication to an Apple platform app
  2. 02Manage users and data in a Swift server
  3. 03Subscribe to real-time row or file changes

Install

Copy the command and run it in a terminal.

$npx skills add https://github.com/appwrite/skills --skill appwrite-swift

After installing, type/appwrite-swiftin Claude Code to confirm it loaded.

Details

From the project README

Appwrite Swift SDK

Installation

// Swift Package Manager — Package.swift
.package(url: "https://github.com/appwrite/sdk-for-swift", branch: "main")

Setting Up the Client

Client-side (Apple platforms)
import Appwrite

let client = Client()
    .setEndpoint("https://<REGION>.cloud.appwrite.io/v1")
    .setProject("[PROJECT_ID]")
Server-side (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)
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)
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.

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.

TypeMax charactersIndexingStorage
varchar16,383Full index (if size ≤ 768)Inline in row
text16,383Prefix onlyOff-page
mediumtext4,194,303Prefix onlyOff-page
longtext1,073,741,823Prefix onlyOff-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.
// 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
// 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
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
InputFile.fromPath("/path/to/file.png")                    // from filesystem path
InputFile.fromData(data, filename: "file.png", mimeType: "image/png")  // from Data
Teams
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)
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:

ChannelDescription
accountChanges to the authenticated user's account
tablesdb.[DB_ID].tables.[TABLE_ID].rowsAll rows in a table
tablesdb.[DB_ID].tables.[TABLE_ID].rows.[ROW_ID]A specific row
buckets.[BUCKET_ID].filesAll files in a bucket
buckets.[BUCKET_ID].files.[FILE_ID]A specific file
teamsChanges to teams the user belongs to
teams.[TEAM_ID]A specific team
membershipsThe user's team memberships
functions.[FUNCTION_ID].executionsFunction execution updates

Response fields: events (array), payload (resource), channels (matched), timestamp (ISO 8601).

Serverless Functions (server-side)
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)
// 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)
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)
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
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
// 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

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:

CodeMeaning
401Unauthorized — missing or invalid session/API key
403Forbidden — insufficient permissions
404Not found — resource does not exist
409Conflict — duplicate ID or unique constraint
429Rate 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.

import Appwrite
// Permission and Role are included in the main module import
Database Row with Permissions
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
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

Comments

Markdown supported