
Appwrite Kotlin
- 277 installs
- 25 repo stars
- Updated May 6, 2026
- appwrite/agent-skills
appwrite-kotlin is an Appwrite agent skill that teaches Kotlin and Android SDK patterns for auth, databases, storage, functions, and realtime subscriptions for developers who build native or JVM backends on Appwrite.
About
appwrite-kotlin is an official Appwrite agent skill covering the Kotlin Android SDK (`io.appwrite:sdk-for-android`) and server Kotlin JVM SDK (`io.appwrite:sdk-for-kotlin`) from build.gradle.kts installation through client setup, email and OAuth auth with Activity integration, database queries, file uploads, coroutine-based realtime subscriptions, and server-side admin operations via API keys for user, database, storage, and function management. The skill provides guided code patterns agents follow when scaffolding or extending Appwrite-backed Android apps or Kotlin services. Reach for appwrite-kotlin when implementing Appwrite authentication flows, document queries, bucket uploads, or live subscriptions in Kotlin rather than reading scattered SDK docs manually. It assumes an existing Appwrite project and focuses on SDK integration correctness across client and admin contexts.
- Appwrite Kotlin SDK patterns
- Auth and session flows
- Database and storage hooks
- Android integration guidance
- Official agent skill recipes
Appwrite Kotlin by the numbers
- 277 all-time installs (skills.sh)
- Ranked #381 of 1,039 Mobile Development skills by installs in the Skillselion catalog
- Data as of Jul 31, 2026 (Skillselion catalog sync)
npx skills add https://github.com/appwrite/agent-skills --skill appwrite-kotlinAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 277 |
|---|---|
| repo stars | ★ 25 |
| Last updated | May 6, 2026 |
| Repository | appwrite/agent-skills ↗ |
How do you integrate Appwrite auth in Kotlin Android?
Integrate Appwrite auth, database, storage, and functions into Kotlin Android apps using guided patterns from official agent skills.
Who is it for?
Android or Kotlin JVM developers adding Appwrite authentication, databases, storage, functions, or realtime to a native app with guided SDK patterns.
Skip if: Skip appwrite-kotlin for Flutter, React Native, or web Appwrite SDKs, or when the backend is not Appwrite and needs a different BaaS integration skill.
When should I use this skill?
User builds Kotlin Android or JVM apps needing Appwrite auth, database, storage, functions, or realtime SDK integration patterns
What you get
Kotlin client setup, auth flows, database queries, storage uploads, realtime subscriptions, and admin API-key management code
- kotlin integration code
- gradle dependencies
By the numbers
- Covers 2 SDK artifacts: sdk-for-android and sdk-for-kotlin
- Documents 5 service areas: auth, database, storage, functions, and realtime
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 row/file level or inherited from the table/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 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
Related skills
How it compares
Choose appwrite-kotlin for native Android or JVM Appwrite SDK work; pick web or cross-platform Appwrite skills when the client is not Kotlin.
FAQ
Which Appwrite SDKs does appwrite-kotlin cover?
appwrite-kotlin covers the Android client SDK via `implementation("io.appwrite:sdk-for-android:+")` and the server Kotlin JVM SDK via `implementation("io.appwrite:sdk-for-kotlin:+")`, including client setup for both platforms.
Does appwrite-kotlin support OAuth on Android?
appwrite-kotlin documents client-side email and OAuth authentication with Android Activity integration, plus database queries, storage uploads, coroutine realtime subscriptions, and server admin operations through API keys.
When should agents load appwrite-kotlin?
Agents should load appwrite-kotlin when building native Android apps or Kotlin/JVM backends that need Appwrite auth, databases, storage, functions, or realtime—especially during initial SDK wiring or debugging integration issues.