--- name: appwrite-kotlin description: 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. --- # Appwrite Kotlin SDK ## Installation ```kotlin // build.gradle.kts — Android implementation("io.appwrite:sdk-for-android:+") // build.gradle.kts — Server (Kotlin JVM) implementation("io.appwrite:sdk-for-kotlin:+") ``` ## Setting Up the Client ### Client-side (Android) ```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://.cloud.appwrite.io/v1") .setProject("[PROJECT_ID]") ``` ### Server-side (Kotlin JVM) ```kotlin 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://.cloud.appwrite.io/v1") .setProject(System.getenv("APPWRITE_PROJECT_ID")) .setKey(System.getenv("APPWRITE_API_KEY")) ``` ## Code Examples ### Authentication (client-side) ```kotlin 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") ``` ### User Management (server-side) ```kotlin 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]") ``` ### 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 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. ```kotlin 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]" ) ``` #### 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. ```kotlin // 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), ) ) ``` ### Query Methods ```kotlin // 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) ``` ### File Storage ```kotlin 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]") ``` #### InputFile Factory Methods ```kotlin import io.appwrite.models.InputFile InputFile.fromPath("/path/to/file.png") // from filesystem path InputFile.fromBytes(byteArray, "file.png") // from ByteArray ``` ### Teams ```kotlin 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 or `Role.team("[TEAM_ID]", "editor")` for a specific team role when setting permissions. ### Real-time Subscriptions (client-side) ```kotlin val realtime = Realtime(client) val subscription = realtime.subscribe("databases.[DATABASE_ID].tables.[TABLE_ID].rows") { response -> println(response.events) // e.g. ["databases.*.tables.*.rows.*.create"] println(response.payload) // the affected resource } // Subscribe to multiple channels val multi = realtime.subscribe( "databases.[DATABASE_ID].tables.[TABLE_ID].rows", "buckets.[BUCKET_ID].files" ) { response -> /* ... */ } // Cleanup subscription.close() ``` **Available channels:** | Channel | Description | |---------|-------------| | `account` | Changes to the authenticated user's account | | `databases.[DB_ID].tables.[TABLE_ID].rows` | All rows in a table | | `databases.[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) ```kotlin 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]") ``` #### Writing a Function Handler (Kotlin runtime) ```kotlin // 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 } ``` ### Server-Side Rendering (SSR) Authentication SSR apps using Kotlin server frameworks (Ktor, Spring Boot, 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) ```kotlin import io.appwrite.Client import io.appwrite.services.Account import io.appwrite.enums.OAuthProvider // Admin client (reusable) val adminClient = Client() .setEndpoint("https://.cloud.appwrite.io/v1") .setProject("[PROJECT_ID]") .setKey(System.getenv("APPWRITE_API_KEY")) // Session client (create per-request) val sessionClient = Client() .setEndpoint("https://.cloud.appwrite.io/v1") .setProject("[PROJECT_ID]") val session = call.request.cookies["a_session_[PROJECT_ID]"] if (session != null) { sessionClient.setSession(session) } ``` #### Email/Password Login (Ktor) ```kotlin post("/login") { val body = call.receive() val account = Account(adminClient) val session = account.createEmailPasswordSession( email = body.email, password = body.password, ) // Cookie name must be a_session_ 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)) } ``` #### Authenticated Requests ```kotlin get("/user") { val session = call.request.cookies["a_session_[PROJECT_ID]"] ?: return@get call.respond(HttpStatusCode.Unauthorized) val sessionClient = Client() .setEndpoint("https://.cloud.appwrite.io/v1") .setProject("[PROJECT_ID]") .setSession(session) val account = Account(sessionClient) val user = account.get() call.respond(user) } ``` #### OAuth2 SSR Flow ```kotlin // 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`, and `SameSite=Strict` to prevent XSS. The cookie name must be `a_session_`. > **Forwarding user agent:** Call `sessionClient.setForwardedUserAgent(call.request.headers["User-Agent"])` to record the end-user's browser info for debugging and security. ## Error Handling ```kotlin 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 | ## 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 document/file level or inherited from the collection/bucket settings. Permissions are arrays of strings built with the `Permission` and `Role` helpers. ```kotlin import io.appwrite.Permission import io.appwrite.Role ``` ### Database Row with Permissions ```kotlin 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 ) ) ``` ### File Upload with Permissions ```kotlin 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()` 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