| 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
implementation("io.appwrite:sdk-for-android:+")
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)
account.create(
userId = ID.unique(),
email = "user@example.com",
password = "password123",
name = "User Name"
)
val session = account.createEmailPasswordSession(
email = "user@example.com",
password = "password123"
)
account.createOAuth2Session(activity = activity, provider = OAuthProvider.GOOGLE)
val user = account.get()
account.deleteSession(sessionId = "current")
User Management (server-side)
val users = Users(client)
val user = users.create(
userId = ID.unique(),
email = "user@example.com",
password = "password123",
name = "User Name"
)
val list = users.list()
val fetched = users.get(userId = "[USER_ID]")
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.
val tablesDB = TablesDB(client)
val db = tablesDB.create(databaseId = ID.unique(), name = "My Database")
val doc = tablesDB.createRow(
databaseId = "[DATABASE_ID]",
tableId = "[TABLE_ID]",
rowId = ID.unique(),
data = mapOf("title" to "Hello", "done" to false)
)
val results = tablesDB.listRows(
databaseId = "[DATABASE_ID]",
tableId = "[TABLE_ID]",
queries = listOf(Query.equal("done", false), Query.limit(10))
)
val row = tablesDB.getRow(databaseId = "[DATABASE_ID]", tableId = "[TABLE_ID]", rowId = "[ROW_ID]")
tablesDB.updateRow(
databaseId = "[DATABASE_ID]",
tableId = "[TABLE_ID]",
rowId = "[ROW_ID]",
data = mapOf("done" to true)
)
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.
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
Query.equal("field", "value")
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)
Query.isNull("field")
Query.isNotNull("field")
Query.startsWith("field", "prefix")
Query.endsWith("field", "suffix")
Query.contains("field", "sub")
Query.search("field", "keywords")
Query.orderAsc("field")
Query.orderDesc("field")
Query.limit(25)
Query.offset(0)
Query.cursorAfter("[ROW_ID]")
Query.cursorBefore("[ROW_ID]")
Query.select(listOf("field1", "field2"))
Query.or(listOf(Query.equal("a", 1), Query.equal("b", 2)))
Query.and(listOf(Query.greaterThan("age", 18), Query.lessThan("age", 65)))
File Storage
val storage = Storage(client)
val file = storage.createFile(
bucketId = "[BUCKET_ID]",
fileId = ID.unique(),
file = InputFile.fromPath("/path/to/file.png")
)
val preview = storage.getFilePreview(
bucketId = "[BUCKET_ID]",
fileId = "[FILE_ID]",
width = 300,
height = 300
)
val files = storage.listFiles(bucketId = "[BUCKET_ID]")
storage.deleteFile(bucketId = "[BUCKET_ID]", fileId = "[FILE_ID]")
InputFile Factory Methods
import io.appwrite.models.InputFile
InputFile.fromPath("/path/to/file.png")
InputFile.fromBytes(byteArray, "file.png")
Teams
val teams = Teams(client)
val team = teams.create(teamId = ID.unique(), name = "Engineering")
val list = teams.list()
val membership = teams.createMembership(
teamId = "[TEAM_ID]",
roles = listOf("editor"),
email = "user@example.com"
)
val members = teams.listMemberships(teamId = "[TEAM_ID]")
teams.updateMembership(teamId = "[TEAM_ID]", membershipId = "[MEMBERSHIP_ID]", roles = listOf("admin"))
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)
import io.appwrite.Channel
val realtime = Realtime(client)
val subscription = realtime.subscribe(
Channel.tablesdb("[DATABASE_ID]").table("[TABLE_ID]").row()
) { response ->
println(response.events)
println(response.payload)
}
val multi = realtime.subscribe(
Channel.tablesdb("[DATABASE_ID]").table("[TABLE_ID]").row(),
Channel.bucket("[BUCKET_ID]").file()
) { response -> }
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)
val execution = functions.createExecution(
functionId = "[FUNCTION_ID]",
body = """{"key": "value"}"""
)
val executions = functions.listExecutions(functionId = "[FUNCTION_ID]")
Writing a Function Handler (Kotlin runtime)
import io.openruntimes.kotlin.RuntimeContext
import io.openruntimes.kotlin.RuntimeOutput
fun main(context: RuntimeContext): RuntimeOutput {
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))
}
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
val adminClient = Client()
.setEndpoint("https://<REGION>.cloud.appwrite.io/v1")
.setProject("[PROJECT_ID]")
.setKey(System.getenv("APPWRITE_API_KEY"))
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,
)
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
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)
}
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_<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)
println(e.code)
println(e.type)
println(e.response)
}
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.Role
Database 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]")),
Permission.update(Role.user("[USER_ID]")),
Permission.read(Role.team("[TEAM_ID]")),
Permission.read(Role.any()),
)
)
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