| 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
.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)
let user = try await account.create(userId: ID.unique(), email: "user@example.com", password: "password123", name: "User Name")
let session = try await account.createEmailPasswordSession(email: "user@example.com", password: "password123")
try await account.createOAuth2Session(provider: .google)
let me = try await account.get()
try await account.deleteSession(sessionId: "current")
User Management (server-side)
let users = Users(client)
let user = try await users.create(userId: ID.unique(), email: "user@example.com", password: "password123", name: "User Name")
let list = try await users.list(queries: [Query.limit(25)])
let fetched = try await users.get(userId: "[USER_ID]")
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)
let db = try await tablesDB.create(databaseId: ID.unique(), name: "My Database")
let doc = try await tablesDB.createRow(databaseId: "[DATABASE_ID]", tableId: "[TABLE_ID]", rowId: ID.unique(), data: [
"title": "Hello",
"done": false
])
let results = try await tablesDB.listRows(databaseId: "[DATABASE_ID]", tableId: "[TABLE_ID]", queries: [
Query.equal("done", value: false),
Query.limit(10)
])
let row = try await tablesDB.getRow(databaseId: "[DATABASE_ID]", tableId: "[TABLE_ID]", rowId: "[ROW_ID]")
try await tablesDB.updateRow(databaseId: "[DATABASE_ID]", tableId: "[TABLE_ID]", rowId: "[ROW_ID]", data: ["done": true])
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.
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
Query.equal("field", value: "value")
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)
Query.isNull("field")
Query.isNotNull("field")
Query.startsWith("field", value: "prefix")
Query.endsWith("field", value: "suffix")
Query.contains("field", value: "sub")
Query.search("field", value: "keywords")
Query.orderAsc("field")
Query.orderDesc("field")
Query.limit(25)
Query.offset(0)
Query.cursorAfter("[ROW_ID]")
Query.cursorBefore("[ROW_ID]")
Query.select(["field1", "field2"])
Query.or([Query.equal("a", value: 1), Query.equal("b", value: 2)])
Query.and([Query.greaterThan("age", value: 18), Query.lessThan("age", value: 65)])
File Storage
let storage = Storage(client)
let file = try await storage.createFile(bucketId: "[BUCKET_ID]", fileId: ID.unique(), file: InputFile.fromPath("/path/to/file.png"))
let files = try await storage.listFiles(bucketId: "[BUCKET_ID]")
try await storage.deleteFile(bucketId: "[BUCKET_ID]", fileId: "[FILE_ID]")
InputFile Factory Methods
InputFile.fromPath("/path/to/file.png")
InputFile.fromData(data, filename: "file.png", mimeType: "image/png")
Teams
let teams = Teams(client)
let team = try await teams.create(teamId: ID.unique(), name: "Engineering")
let list = try await teams.list()
let membership = try await teams.createMembership(
teamId: "[TEAM_ID]",
roles: ["editor"],
email: "user@example.com"
)
let members = try await teams.listMemberships(teamId: "[TEAM_ID]")
try await teams.updateMembership(teamId: "[TEAM_ID]", membershipId: "[MEMBERSHIP_ID]", roles: ["admin"])
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)
let subscription = try await realtime.subscribe(channels: [
Channel.tablesdb("[DATABASE_ID]").table("[TABLE_ID]").row()
]) { response in
print(response.events)
print(response.payload)
}
let multi = try await realtime.subscribe(channels: [
Channel.tablesdb("[DATABASE_ID]").table("[TABLE_ID]").row(),
Channel.files(),
]) { response in }
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)
let functions = Functions(client)
let execution = try await functions.createExecution(functionId: "[FUNCTION_ID]", body: "{\"key\": \"value\"}")
let executions = try await functions.listExecutions(functionId: "[FUNCTION_ID]")
Writing a Function Handler (Swift runtime)
func main(context: RuntimeContext) async throws -> RuntimeOutput {
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])
}
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
let adminClient = Client()
.setEndpoint("https://<REGION>.cloud.appwrite.io/v1")
.setProject("[PROJECT_ID]")
.setKey(Environment.get("APPWRITE_API_KEY")!)
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
)
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()
}
OAuth2 SSR Flow
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)
}
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
do {
let row = try await tablesDB.getRow(databaseId: "[DATABASE_ID]", tableId: "[TABLE_ID]", rowId: "[ROW_ID]")
} catch let error as AppwriteException {
print(error.message)
print(error.code)
print(error.type)
print(error.response)
}
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.
import Appwrite
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]")),
Permission.update(Role.user("[USER_ID]")),
Permission.read(Role.team("[TEAM_ID]")),
Permission.read(Role.any()),
]
)
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