
Kotlin
- 3 installs
- 10 repo stars
- Updated July 29, 2026
- appwrite/claude-plugin
Appwrite Kotlin SDK skill is a Claude Code skill that guides building native Android apps and server-side Kotlin/JVM backends against the Appwrite backend-as-a-service.
About
This skill documents the Appwrite Kotlin SDK for both native Android apps and server-side Kotlin/JVM backends. A developer uses it to set up the Appwrite client, handle email and OAuth authentication, run TablesDB database and row operations, upload files, and manage users. It gives Claude the correct current SDK calling conventions, including the newer TablesDB class over the deprecated Databases class.
- Client-side Android auth (email, OAuth via Activity) plus server-side Kotlin/JVM admin via API keys
- TablesDB database CRUD, queries, file storage, and Functions coverage
- Real-time subscriptions with Kotlin coroutine support
Kotlin by the numbers
- 3 all-time installs (skills.sh)
- Ranked #3,739 of 4,347 Backend & APIs skills by installs in the Skillselion catalog
- Data as of Jul 30, 2026 (Skillselion catalog sync)
kotlin capabilities & compatibility
Requires an Appwrite project and API key; Appwrite Cloud has a free tier.
- Capabilities
- appwrite sdk php · appwrite sdk swift · backend integration
- Works with
- github
- Use cases
- api development · database
- Pricing
- Bring your own API key
What kotlin says it does
Appwrite Kotlin SDK skill. Use when building native Android apps or server-side Kotlin/JVM backends with Appwrite.
**Note:** Use `TablesDB` (not the deprecated `Databases` class) for all new code.
npx skills add https://github.com/appwrite/claude-plugin --skill kotlinAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 3 |
|---|---|
| repo stars | ★ 10 |
| Last updated | July 29, 2026 |
| Repository | appwrite/claude-plugin ↗ |
What it does
Build Android or Kotlin JVM auth, database, and storage features against an Appwrite backend.
Who is it for?
Android or Kotlin JVM developers adding Appwrite auth, database, storage, and functions.
Skip if: Projects not using Appwrite, or SDKs in other languages.
When should I use this skill?
Building native Android apps or server-side Kotlin/JVM backends with Appwrite.
What you get
- Configured Appwrite Kotlin client
- auth flows
- TablesDB CRUD code
By the numbers
- 4 string column types documented (varchar, text, mediumtext, longtext)
Files
Appwrite Kotlin SDK
Installation
// 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)
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]")Server-side (Kotlin JVM)
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"))Code Examples
Authentication (client-side)
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)
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: UseTablesDB(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]"
)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 |
varcharis stored inline and counts towards the 64 KB row size limit. Prefer for short, indexed fields like names, slugs, or identifiers.text,mediumtext, andlongtextare stored off-page (only a 20-byte pointer lives in the row), so they don't consume the row size budget.sizeis 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),
)
)Query Methods
// 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
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
import io.appwrite.models.InputFile
InputFile.fromPath("/path/to/file.png") // from filesystem path
InputFile.fromBytes(byteArray, "file.png") // from ByteArrayTeams
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: UseRole.team("[TEAM_ID]")for all team members orRole.team("[TEAM_ID]", "editor")for a specific team role when setting permissions.
Real-time Subscriptions (client-side)
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).
Serverless Functions (server-side)
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)
// 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)
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)
}Email/Password Login (Ktor)
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))
}Authenticated Requests
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)
}OAuth2 SSR Flow
// 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 usehttpOnly,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.Error Handling
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.
import io.appwrite.Permission
import io.appwrite.RoleDatabase Row with Permissions
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
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
Related skills
FAQ
Does this cover Android client auth?
Yes, it covers client-side email and OAuth auth with Activity integration, plus server-side admin via API keys.
Which database class should I use?
Use TablesDB for new code; the Databases class is deprecated.