Appwrite Kotlin SDK skill. Use when building native Android apps or server-side Kotlin/JVM backends with Appwrite. Covers client-side auth (email, OAuth with Activity integration), database queries, file uploads, real-time subscriptions with coroutine support, and server-side admin via API keys for user management, database administration, storage, and functions.
npx claudepluginhub joshuarweaver/cascade-data-storage --plugin appwrite-agent-skillsThis skill uses the workspace's default tool permissions.
```kotlin
Conducts multi-round deep research on GitHub repos via API and web searches, generating markdown reports with executive summaries, timelines, metrics, and Mermaid diagrams.
Dynamically discovers and combines enabled skills into cohesive, unexpected delightful experiences like interactive HTML or themed artifacts. Activates on 'surprise me', inspiration, or boredom cues.
Generates images from structured JSON prompts via Python script execution. Supports reference images and aspect ratios for characters, scenes, products, visuals.
// build.gradle.kts — Android
implementation("io.appwrite:sdk-for-android:+")
// build.gradle.kts — Server (Kotlin JVM)
implementation("io.appwrite:sdk-for-kotlin:+")
import io.appwrite.Client
import io.appwrite.ID
import io.appwrite.Query
import io.appwrite.enums.OAuthProvider
import io.appwrite.services.Account
import io.appwrite.services.Realtime
import io.appwrite.services.TablesDB
import io.appwrite.services.Storage
import io.appwrite.models.InputFile
val client = Client(context)
.setEndpoint("https://<REGION>.cloud.appwrite.io/v1")
.setProject("[PROJECT_ID]")
import io.appwrite.Client
import io.appwrite.ID
import io.appwrite.Query
import io.appwrite.services.Users
import io.appwrite.services.TablesDB
import io.appwrite.services.Storage
import io.appwrite.services.Functions
val client = Client()
.setEndpoint("https://<REGION>.cloud.appwrite.io/v1")
.setProject(System.getenv("APPWRITE_PROJECT_ID"))
.setKey(System.getenv("APPWRITE_API_KEY"))
val account = Account(client)
// Signup
account.create(
userId = ID.unique(),
email = "user@example.com",
password = "password123",
name = "User Name"
)
// Login
val session = account.createEmailPasswordSession(
email = "user@example.com",
password = "password123"
)
// OAuth
account.createOAuth2Session(activity = activity, provider = OAuthProvider.GOOGLE)
// Get current user
val user = account.get()
// Logout
account.deleteSession(sessionId = "current")
val users = Users(client)
// Create user
val user = users.create(
userId = ID.unique(),
email = "user@example.com",
password = "password123",
name = "User Name"
)
// List users
val list = users.list()
// Get user
val fetched = users.get(userId = "[USER_ID]")
// Delete user
users.delete(userId = "[USER_ID]")
Note: Use
TablesDB(not the deprecatedDatabasesclass) for all new code. Only useDatabasesif the existing codebase already relies on it or the user explicitly requests it.Tip: Prefer named arguments (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.
val tablesDB = TablesDB(client)
// Create database (server-side only)
val db = tablesDB.create(databaseId = ID.unique(), name = "My Database")
// Create row
val doc = tablesDB.createRow(
databaseId = "[DATABASE_ID]",
tableId = "[TABLE_ID]",
rowId = ID.unique(),
data = mapOf("title" to "Hello", "done" to false)
)
// Query rows
val results = tablesDB.listRows(
databaseId = "[DATABASE_ID]",
tableId = "[TABLE_ID]",
queries = listOf(Query.equal("done", false), Query.limit(10))
)
// Get row
val row = tablesDB.getRow(databaseId = "[DATABASE_ID]", tableId = "[TABLE_ID]", rowId = "[ROW_ID]")
// Update row
tablesDB.updateRow(
databaseId = "[DATABASE_ID]",
tableId = "[TABLE_ID]",
rowId = "[ROW_ID]",
data = mapOf("done" to true)
)
// Delete row
tablesDB.deleteRow(
databaseId = "[DATABASE_ID]",
tableId = "[TABLE_ID]",
rowId = "[ROW_ID]"
)
Note: The legacy
stringtype 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.// Create table with explicit string column types
tablesDB.createTable(
databaseId = "[DATABASE_ID]",
tableId = ID.unique(),
name = "articles",
columns = listOf(
mapOf("key" to "title", "type" to "varchar", "size" to 255, "required" to true),
mapOf("key" to "summary", "type" to "text", "required" to false),
mapOf("key" to "body", "type" to "mediumtext", "required" to false),
mapOf("key" to "raw_data", "type" to "longtext", "required" to false),
)
)
// Filtering
Query.equal("field", "value") // == (or pass list for IN)
Query.notEqual("field", "value") // !=
Query.lessThan("field", 100) // <
Query.lessThanEqual("field", 100) // <=
Query.greaterThan("field", 100) // >
Query.greaterThanEqual("field", 100) // >=
Query.between("field", 1, 100) // 1 <= field <= 100
Query.isNull("field") // is null
Query.isNotNull("field") // is not null
Query.startsWith("field", "prefix") // starts with
Query.endsWith("field", "suffix") // ends with
Query.contains("field", "sub") // contains
Query.search("field", "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(listOf("field1", "field2"))
Query.or(listOf(Query.equal("a", 1), Query.equal("b", 2))) // OR
Query.and(listOf(Query.greaterThan("age", 18), Query.lessThan("age", 65))) // AND (default)
val storage = Storage(client)
// Upload file
val file = storage.createFile(
bucketId = "[BUCKET_ID]",
fileId = ID.unique(),
file = InputFile.fromPath("/path/to/file.png")
)
// Get file preview
val preview = storage.getFilePreview(
bucketId = "[BUCKET_ID]",
fileId = "[FILE_ID]",
width = 300,
height = 300
)
// List files
val files = storage.listFiles(bucketId = "[BUCKET_ID]")
// Delete file
storage.deleteFile(bucketId = "[BUCKET_ID]", fileId = "[FILE_ID]")
import io.appwrite.models.InputFile
InputFile.fromPath("/path/to/file.png") // from filesystem path
InputFile.fromBytes(byteArray, "file.png") // from ByteArray
val teams = Teams(client)
// Create team
val team = teams.create(teamId = ID.unique(), name = "Engineering")
// List teams
val list = teams.list()
// Create membership (invite user by email)
val membership = teams.createMembership(
teamId = "[TEAM_ID]",
roles = listOf("editor"),
email = "user@example.com"
)
// List memberships
val members = teams.listMemberships(teamId = "[TEAM_ID]")
// Update membership roles
teams.updateMembership(teamId = "[TEAM_ID]", membershipId = "[MEMBERSHIP_ID]", roles = listOf("admin"))
// Delete team
teams.delete(teamId = "[TEAM_ID]")
Role-based access: Use
Role.team("[TEAM_ID]")for all team members orRole.team("[TEAM_ID]", "editor")for a specific team role when setting permissions.
import io.appwrite.Channel
val realtime = Realtime(client)
// Subscribe to row changes
val subscription = realtime.subscribe(
Channel.tablesdb("[DATABASE_ID]").table("[TABLE_ID]").row()
) { response ->
println(response.events) // e.g. ["tablesdb.*.tables.*.rows.*.create"]
println(response.payload) // the affected resource
}
// Subscribe to multiple channels
val multi = realtime.subscribe(
Channel.tablesdb("[DATABASE_ID]").table("[TABLE_ID]").row(),
Channel.bucket("[BUCKET_ID]").file()
) { response -> /* ... */ }
// Cleanup
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).
val functions = Functions(client)
// Execute function
val execution = functions.createExecution(
functionId = "[FUNCTION_ID]",
body = """{"key": "value"}"""
)
// List executions
val executions = functions.listExecutions(functionId = "[FUNCTION_ID]")
// src/Main.kt — Appwrite Function entry point
import io.openruntimes.kotlin.RuntimeContext
import io.openruntimes.kotlin.RuntimeOutput
fun main(context: RuntimeContext): RuntimeOutput {
// context.req.body — raw body (String)
// context.req.bodyJson — parsed JSON (Map)
// context.req.headers — headers (Map)
// context.req.method — HTTP method
// context.req.path — URL path
// context.req.query — query params (Map)
context.log("Processing: ${context.req.method} ${context.req.path}")
if (context.req.method == "GET") {
return context.res.json(mapOf("message" to "Hello from Appwrite Function!"))
}
return context.res.json(mapOf("success" to true)) // JSON
// context.res.text("Hello") // plain text
// context.res.empty() // 204
// context.res.redirect("https://...") // 302
}
SSR apps using Kotlin server frameworks (Ktor, Spring Boot, etc.) use the server SDK to handle auth. You need two clients:
import io.appwrite.Client
import io.appwrite.services.Account
import io.appwrite.enums.OAuthProvider
// Admin client (reusable)
val adminClient = Client()
.setEndpoint("https://<REGION>.cloud.appwrite.io/v1")
.setProject("[PROJECT_ID]")
.setKey(System.getenv("APPWRITE_API_KEY"))
// Session client (create per-request)
val sessionClient = Client()
.setEndpoint("https://<REGION>.cloud.appwrite.io/v1")
.setProject("[PROJECT_ID]")
val session = call.request.cookies["a_session_[PROJECT_ID]"]
if (session != null) {
sessionClient.setSession(session)
}
post("/login") {
val body = call.receive<LoginRequest>()
val account = Account(adminClient)
val session = account.createEmailPasswordSession(
email = body.email,
password = body.password,
)
// Cookie name must be a_session_<PROJECT_ID>
call.response.cookies.append(Cookie(
name = "a_session_[PROJECT_ID]",
value = session.secret,
httpOnly = true,
secure = true,
extensions = mapOf("SameSite" to "Strict"),
path = "/",
))
call.respond(mapOf("success" to true))
}
get("/user") {
val session = call.request.cookies["a_session_[PROJECT_ID]"]
?: return@get call.respond(HttpStatusCode.Unauthorized)
val sessionClient = Client()
.setEndpoint("https://<REGION>.cloud.appwrite.io/v1")
.setProject("[PROJECT_ID]")
.setSession(session)
val account = Account(sessionClient)
val user = account.get()
call.respond(user)
}
// Step 1: Redirect to OAuth provider
get("/oauth") {
val account = Account(adminClient)
val redirectUrl = account.createOAuth2Token(
provider = OAuthProvider.GITHUB,
success = "https://example.com/oauth/success",
failure = "https://example.com/oauth/failure",
)
call.respondRedirect(redirectUrl)
}
// Step 2: Handle callback — exchange token for session
get("/oauth/success") {
val account = Account(adminClient)
val session = account.createSession(
userId = call.parameters["userId"]!!,
secret = call.parameters["secret"]!!,
)
call.response.cookies.append(Cookie(
name = "a_session_[PROJECT_ID]", value = session.secret,
httpOnly = true, secure = true,
extensions = mapOf("SameSite" to "Strict"), path = "/",
))
call.respond(mapOf("success" to true))
}
Cookie security: Always use
httpOnly,secure, andSameSite=Strictto prevent XSS. The cookie name must bea_session_<PROJECT_ID>.
Forwarding user agent: Call
sessionClient.setForwardedUserAgent(call.request.headers["User-Agent"])to record the end-user's browser info for debugging and security.
import io.appwrite.AppwriteException
try {
val row = tablesDB.getRow(databaseId = "[DATABASE_ID]", tableId = "[TABLE_ID]", rowId = "[ROW_ID]")
} catch (e: AppwriteException) {
println(e.message) // human-readable message
println(e.code) // HTTP status code (Int)
println(e.type) // error type (e.g. "document_not_found")
println(e.response) // full response body (Map)
}
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 |
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 document/file level or inherited from the collection/bucket settings. Permissions are arrays of strings built with the Permission and Role helpers.
import io.appwrite.Permission
import io.appwrite.Role
val doc = tablesDB.createRow(
databaseId = "[DATABASE_ID]",
tableId = "[TABLE_ID]",
rowId = ID.unique(),
data = mapOf("title" to "Hello World"),
permissions = listOf(
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
)
)
val file = storage.createFile(
bucketId = "[BUCKET_ID]",
fileId = ID.unique(),
file = InputFile.fromPath("/path/to/file.png"),
permissions = listOf(
Permission.read(Role.any()),
Permission.update(Role.user("[USER_ID]")),
Permission.delete(Role.user("[USER_ID]")),
)
)
When to set permissions: Set document/file-level permissions when you need per-resource access control. If all documents in a collection share the same rules, configure permissions at the collection/bucket level and leave document permissions empty.
Common mistakes:
- Forgetting permissions — the resource becomes inaccessible to all users (including the creator)
Role.any()withwrite/update/delete— allows any user, including unauthenticated guests, to modify or remove the resourcePermission.read(Role.any())on sensitive data — makes the resource publicly readable