← AppwriteCONTENT HISTORYWHAT CHANGED · RULE-BASED ANALYSIS
Update to Appwrite
Snapshot Sep 30, 2026 · 23:07 UTC · version 1.0.0
Collection source: not recorded for this historical snapshot.
First saved snapshot
No earlier snapshot is available to establish a change.
Compare saved observations
Download comparison JSONFull technical diff · 0 changed fields
Full snapshot data
{
"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.",
"included_files": [],
"skill_md_contents": "---\r\nname: appwrite-kotlin\r\ndescription: 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.\r\n---\r\n\r\n\r\n# Appwrite Kotlin SDK\r\n\r\n## Installation\r\n\r\n```kotlin\r\n// build.gradle.kts — Android\r\nimplementation(\"io.appwrite:sdk-for-android:+\")\r\n\r\n// build.gradle.kts — Server (Kotlin JVM)\r\nimplementation(\"io.appwrite:sdk-for-kotlin:+\")\r\n```\r\n\r\n## Setting Up the Client\r\n\r\n### Client-side (Android)\r\n\r\n```kotlin\r\nimport io.appwrite.Client\r\nimport io.appwrite.ID\r\nimport io.appwrite.Query\r\nimport io.appwrite.enums.OAuthProvider\r\nimport io.appwrite.services.Account\r\nimport io.appwrite.services.Realtime\r\nimport io.appwrite.services.TablesDB\r\nimport io.appwrite.services.Storage\r\nimport io.appwrite.models.InputFile\r\n\r\nval client = Client(context)\r\n .setEndpoint(\"https://<REGION>.cloud.appwrite.io/v1\")\r\n .setProject(\"[PROJECT_ID]\")\r\n```\r\n\r\n### Server-side (Kotlin JVM)\r\n\r\n```kotlin\r\nimport io.appwrite.Client\r\nimport io.appwrite.ID\r\nimport io.appwrite.Query\r\nimport io.appwrite.services.Users\r\nimport io.appwrite.services.TablesDB\r\nimport io.appwrite.services.Storage\r\nimport io.appwrite.services.Functions\r\n\r\nval client = Client()\r\n .setEndpoint(\"https://<REGION>.cloud.appwrite.io/v1\")\r\n .setProject(System.getenv(\"APPWRITE_PROJECT_ID\"))\r\n .setKey(System.getenv(\"APPWRITE_API_KEY\"))\r\n```\r\n\r\n## Code Examples\r\n\r\n### Authentication (client-side)\r\n\r\n```kotlin\r\nval account = Account(client)\r\n\r\n// Signup\r\naccount.create(\r\n userId = ID.unique(),\r\n email = \"user@example.com\",\r\n password = \"password123\",\r\n name = \"User Name\"\r\n)\r\n\r\n// Login\r\nval session = account.createEmailPasswordSession(\r\n email = \"user@example.com\",\r\n password = \"password123\"\r\n)\r\n\r\n// OAuth\r\naccount.createOAuth2Session(activity = activity, provider = OAuthProvider.GOOGLE)\r\n\r\n// Get current user\r\nval user = account.get()\r\n\r\n// Logout\r\naccount.deleteSession(sessionId = \"current\")\r\n```\r\n\r\n### User Management (server-side)\r\n\r\n```kotlin\r\nval users = Users(client)\r\n\r\n// Create user\r\nval user = users.create(\r\n userId = ID.unique(),\r\n email = \"user@example.com\",\r\n password = \"password123\",\r\n name = \"User Name\"\r\n)\r\n\r\n// List users\r\nval list = users.list()\r\n\r\n// Get user\r\nval fetched = users.get(userId = \"[USER_ID]\")\r\n\r\n// Delete user\r\nusers.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 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.\r\n\r\n```kotlin\r\nval tablesDB = TablesDB(client)\r\n\r\n// Create database (server-side only)\r\nval db = tablesDB.create(databaseId = ID.unique(), name = \"My Database\")\r\n\r\n// Create row\r\nval doc = tablesDB.createRow(\r\n databaseId = \"[DATABASE_ID]\",\r\n tableId = \"[TABLE_ID]\",\r\n rowId = ID.unique(),\r\n data = mapOf(\"title\" to \"Hello\", \"done\" to false)\r\n)\r\n\r\n// Query rows\r\nval results = tablesDB.listRows(\r\n databaseId = \"[DATABASE_ID]\",\r\n tableId = \"[TABLE_ID]\",\r\n queries = listOf(Query.equal(\"done\", false), Query.limit(10))\r\n)\r\n\r\n// Get row\r\nval row = tablesDB.getRow(databaseId = \"[DATABASE_ID]\", tableId = \"[TABLE_ID]\", rowId = \"[ROW_ID]\")\r\n\r\n// Update row\r\ntablesDB.updateRow(\r\n databaseId = \"[DATABASE_ID]\",\r\n tableId = \"[TABLE_ID]\",\r\n rowId = \"[ROW_ID]\",\r\n data = mapOf(\"done\" to true)\r\n)\r\n\r\n// Delete row\r\ntablesDB.deleteRow(\r\n databaseId = \"[DATABASE_ID]\",\r\n tableId = \"[TABLE_ID]\",\r\n rowId = \"[ROW_ID]\"\r\n)\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```kotlin\r\n// Create table with explicit string column types\r\ntablesDB.createTable(\r\n databaseId = \"[DATABASE_ID]\",\r\n tableId = ID.unique(),\r\n name = \"articles\",\r\n columns = listOf(\r\n mapOf(\"key\" to \"title\", \"type\" to \"varchar\", \"size\" to 255, \"required\" to true),\r\n mapOf(\"key\" to \"summary\", \"type\" to \"text\", \"required\" to false),\r\n mapOf(\"key\" to \"body\", \"type\" to \"mediumtext\", \"required\" to false),\r\n mapOf(\"key\" to \"raw_data\", \"type\" to \"longtext\", \"required\" to false),\r\n )\r\n)\r\n```\r\n\r\n### Query Methods\r\n\r\n```kotlin\r\n// Filtering\r\nQuery.equal(\"field\", \"value\") // == (or pass list for IN)\r\nQuery.notEqual(\"field\", \"value\") // !=\r\nQuery.lessThan(\"field\", 100) // <\r\nQuery.lessThanEqual(\"field\", 100) // <=\r\nQuery.greaterThan(\"field\", 100) // >\r\nQuery.greaterThanEqual(\"field\", 100) // >=\r\nQuery.between(\"field\", 1, 100) // 1 <= field <= 100\r\nQuery.isNull(\"field\") // is null\r\nQuery.isNotNull(\"field\") // is not null\r\nQuery.startsWith(\"field\", \"prefix\") // starts with\r\nQuery.endsWith(\"field\", \"suffix\") // ends with\r\nQuery.contains(\"field\", \"sub\") // contains\r\nQuery.search(\"field\", \"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(listOf(\"field1\", \"field2\"))\r\nQuery.or(listOf(Query.equal(\"a\", 1), Query.equal(\"b\", 2))) // OR\r\nQuery.and(listOf(Query.greaterThan(\"age\", 18), Query.lessThan(\"age\", 65))) // AND (default)\r\n```\r\n\r\n### File Storage\r\n\r\n```kotlin\r\nval storage = Storage(client)\r\n\r\n// Upload file\r\nval file = storage.createFile(\r\n bucketId = \"[BUCKET_ID]\",\r\n fileId = ID.unique(),\r\n file = InputFile.fromPath(\"/path/to/file.png\")\r\n)\r\n\r\n// Get file preview\r\nval preview = storage.getFilePreview(\r\n bucketId = \"[BUCKET_ID]\",\r\n fileId = \"[FILE_ID]\",\r\n width = 300,\r\n height = 300\r\n)\r\n\r\n// List files\r\nval files = storage.listFiles(bucketId = \"[BUCKET_ID]\")\r\n\r\n// Delete file\r\nstorage.deleteFile(bucketId = \"[BUCKET_ID]\", fileId = \"[FILE_ID]\")\r\n```\r\n\r\n#### InputFile Factory Methods\r\n\r\n```kotlin\r\nimport io.appwrite.models.InputFile\r\n\r\nInputFile.fromPath(\"/path/to/file.png\") // from filesystem path\r\nInputFile.fromBytes(byteArray, \"file.png\") // from ByteArray\r\n```\r\n\r\n### Teams\r\n\r\n```kotlin\r\nval teams = Teams(client)\r\n\r\n// Create team\r\nval team = teams.create(teamId = ID.unique(), name = \"Engineering\")\r\n\r\n// List teams\r\nval list = teams.list()\r\n\r\n// Create membership (invite user by email)\r\nval membership = teams.createMembership(\r\n teamId = \"[TEAM_ID]\",\r\n roles = listOf(\"editor\"),\r\n email = \"user@example.com\"\r\n)\r\n\r\n// List memberships\r\nval members = teams.listMemberships(teamId = \"[TEAM_ID]\")\r\n\r\n// Update membership roles\r\nteams.updateMembership(teamId = \"[TEAM_ID]\", membershipId = \"[MEMBERSHIP_ID]\", roles = listOf(\"admin\"))\r\n\r\n// Delete team\r\nteams.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```kotlin\r\nimport io.appwrite.Channel\r\n\r\nval realtime = Realtime(client)\r\n\r\n// Subscribe to row changes\r\nval subscription = realtime.subscribe(\r\n Channel.tablesdb(\"[DATABASE_ID]\").table(\"[TABLE_ID]\").row()\r\n) { response ->\r\n println(response.events) // e.g. [\"tablesdb.*.tables.*.rows.*.create\"]\r\n println(response.payload) // the affected resource\r\n}\r\n\r\n// Subscribe to multiple channels\r\nval multi = realtime.subscribe(\r\n Channel.tablesdb(\"[DATABASE_ID]\").table(\"[TABLE_ID]\").row(),\r\n Channel.bucket(\"[BUCKET_ID]\").file()\r\n) { response -> /* ... */ }\r\n\r\n// Cleanup\r\nsubscription.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```kotlin\r\nval functions = Functions(client)\r\n\r\n// Execute function\r\nval execution = functions.createExecution(\r\n functionId = \"[FUNCTION_ID]\",\r\n body = \"\"\"{\"key\": \"value\"}\"\"\"\r\n)\r\n\r\n// List executions\r\nval executions = functions.listExecutions(functionId = \"[FUNCTION_ID]\")\r\n```\r\n\r\n#### Writing a Function Handler (Kotlin runtime)\r\n\r\n```kotlin\r\n// src/Main.kt — Appwrite Function entry point\r\nimport io.openruntimes.kotlin.RuntimeContext\r\nimport io.openruntimes.kotlin.RuntimeOutput\r\n\r\nfun main(context: RuntimeContext): RuntimeOutput {\r\n // context.req.body — raw body (String)\r\n // context.req.bodyJson — parsed JSON (Map)\r\n // context.req.headers — headers (Map)\r\n // context.req.method — HTTP method\r\n // context.req.path — URL path\r\n // context.req.query — query params (Map)\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(mapOf(\"message\" to \"Hello from Appwrite Function!\"))\r\n }\r\n\r\n return context.res.json(mapOf(\"success\" to 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 Kotlin server frameworks (Ktor, Spring Boot, 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```kotlin\r\nimport io.appwrite.Client\r\nimport io.appwrite.services.Account\r\nimport io.appwrite.enums.OAuthProvider\r\n\r\n// Admin client (reusable)\r\nval adminClient = Client()\r\n .setEndpoint(\"https://<REGION>.cloud.appwrite.io/v1\")\r\n .setProject(\"[PROJECT_ID]\")\r\n .setKey(System.getenv(\"APPWRITE_API_KEY\"))\r\n\r\n// Session client (create per-request)\r\nval sessionClient = Client()\r\n .setEndpoint(\"https://<REGION>.cloud.appwrite.io/v1\")\r\n .setProject(\"[PROJECT_ID]\")\r\n\r\nval session = call.request.cookies[\"a_session_[PROJECT_ID]\"]\r\nif (session != null) {\r\n sessionClient.setSession(session)\r\n}\r\n```\r\n\r\n#### Email/Password Login (Ktor)\r\n\r\n```kotlin\r\npost(\"/login\") {\r\n val body = call.receive<LoginRequest>()\r\n val account = Account(adminClient)\r\n val session = 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 call.response.cookies.append(Cookie(\r\n name = \"a_session_[PROJECT_ID]\",\r\n value = session.secret,\r\n httpOnly = true,\r\n secure = true,\r\n extensions = mapOf(\"SameSite\" to \"Strict\"),\r\n path = \"/\",\r\n ))\r\n call.respond(mapOf(\"success\" to true))\r\n}\r\n```\r\n\r\n#### Authenticated Requests\r\n\r\n```kotlin\r\nget(\"/user\") {\r\n val session = call.request.cookies[\"a_session_[PROJECT_ID]\"]\r\n ?: return@get call.respond(HttpStatusCode.Unauthorized)\r\n\r\n val sessionClient = Client()\r\n .setEndpoint(\"https://<REGION>.cloud.appwrite.io/v1\")\r\n .setProject(\"[PROJECT_ID]\")\r\n .setSession(session)\r\n\r\n val account = Account(sessionClient)\r\n val user = account.get()\r\n call.respond(user)\r\n}\r\n```\r\n\r\n#### OAuth2 SSR Flow\r\n\r\n```kotlin\r\n// Step 1: Redirect to OAuth provider\r\nget(\"/oauth\") {\r\n val account = Account(adminClient)\r\n val redirectUrl = account.createOAuth2Token(\r\n provider = OAuthProvider.GITHUB,\r\n success = \"https://example.com/oauth/success\",\r\n failure = \"https://example.com/oauth/failure\",\r\n )\r\n call.respondRedirect(redirectUrl)\r\n}\r\n\r\n// Step 2: Handle callback — exchange token for session\r\nget(\"/oauth/success\") {\r\n val account = Account(adminClient)\r\n val session = account.createSession(\r\n userId = call.parameters[\"userId\"]!!,\r\n secret = call.parameters[\"secret\"]!!,\r\n )\r\n\r\n call.response.cookies.append(Cookie(\r\n name = \"a_session_[PROJECT_ID]\", value = session.secret,\r\n httpOnly = true, secure = true,\r\n extensions = mapOf(\"SameSite\" to \"Strict\"), path = \"/\",\r\n ))\r\n call.respond(mapOf(\"success\" to true))\r\n}\r\n```\r\n\r\n> **Cookie security:** Always use `httpOnly`, `secure`, 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(call.request.headers[\"User-Agent\"])` to record the end-user's browser info for debugging and security.\r\n\r\n## Error Handling\r\n\r\n```kotlin\r\nimport io.appwrite.AppwriteException\r\n\r\ntry {\r\n val row = tablesDB.getRow(databaseId = \"[DATABASE_ID]\", tableId = \"[TABLE_ID]\", rowId = \"[ROW_ID]\")\r\n} catch (e: AppwriteException) {\r\n println(e.message) // human-readable message\r\n println(e.code) // HTTP status code (Int)\r\n println(e.type) // error type (e.g. \"document_not_found\")\r\n println(e.response) // full response body (Map)\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```kotlin\r\nimport io.appwrite.Permission\r\nimport io.appwrite.Role\r\n```\r\n\r\n### Database Row with Permissions\r\n\r\n```kotlin\r\nval doc = tablesDB.createRow(\r\n databaseId = \"[DATABASE_ID]\",\r\n tableId = \"[TABLE_ID]\",\r\n rowId = ID.unique(),\r\n data = mapOf(\"title\" to \"Hello World\"),\r\n permissions = listOf(\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```kotlin\r\nval file = storage.createFile(\r\n bucketId = \"[BUCKET_ID]\",\r\n fileId = ID.unique(),\r\n file = InputFile.fromPath(\"/path/to/file.png\"),\r\n permissions = listOf(\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: 9c596f92fcdd5f38cadfd268fb9ff98ede871c02a7e557cdec636c13c6088d64