Agent Skill · Appwrite

appwrite-kotlin

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.

Provider: Appwrite Path in repo: SKILL.md

Skill body

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: 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)

// 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
// 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 ByteArray

Teams

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)

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:

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 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)     // 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.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]")),     // 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

Work with this as data

Every skill here is available over the APIs.io API and to AI agents over MCP.

MCP server

One button, every client — Claude, Cursor, VS Code and the rest.

https://apis.io/mcp

Tools for agent skills

4 MCP tools reach this
  • find_skillsBrowse and filter every skill in the catalog.
  • apis_io_searchSTART HERE — APIs, providers and tags for one query, each with its total.
  • resolveTurn a domain, URL or GitHub org into the provider it belongs to.
  • find_cohortsEvery scored population of providers in the catalog.
All 92 tools →

Call it yourself

curl for this page
This skill
curl "https://apis.io/api/v1/skills/appwrite-kotlin"
All agent skills
curl "https://apis.io/api/v1/skills?limit=25"

Discovery needs no key. Ratings and market analysis are Pro.

Get an API key

Free tier, no form to fill in. Signing in shares your email address with us — we store it to create your key and to recognise you if you sign in with another provider. See our Privacy Policy and Terms.

A second provider on the same verified email joins the account you already have.