AgentStack
SKILL verified BSD-3-Clause Self-run

Appwrite Kotlin

skill-appwrite-agent-skills-appwrite-kotlin · by appwrite

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.

No reviews yet
0 installs
10 views
0.0% view→install

Install

$ agentstack add skill-appwrite-agent-skills-appwrite-kotlin

✓ scanned · ✓ verified — works with Claude Code, Cursor, and more.

Security review

✓ Passed

No issues found. Passed automated security review. · v0.1.0 How review works →

  • Prompt-injection patterns
  • Secret / credential exfiltration
  • Dangerous shell & filesystem operations
  • Untrusted network calls
  • Known-malicious package signatures

What it can access

  • Network access No
  • Filesystem access No
  • Shell / process execution No
  • Environment & secrets Used
  • Dynamic code execution No

From automated source analysis of v0.1.0. “Used” means the capability is present in the source — more access means more to trust, not that it’s unsafe.

Are you the author of Appwrite Kotlin? Claim this listing to set pricing, connect Stripe payouts, and keep 70% of every sale.
Sign up to claim

About

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://.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://.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 |

  • 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.
// 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.greaterThanEqual("field", 100)      // >=
Query.between("field", 1, 100)            // 1  **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)

```kotlin
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://.cloud.appwrite.io/v1")
    .setProject("[PROJECT_ID]")
    .setKey(System.getenv("APPWRITE_API_KEY"))

// Session client (create per-request)
val sessionClient = Client()
    .setEndpoint("https://.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()
    val account = Account(adminClient)
    val session = account.createEmailPasswordSession(
        email = body.email,
        password = body.password,
    )

    // Cookie name must be a_session_
    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://.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_.

> 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

Source & license

This open-source skill is cataloged on AgentStack and links to its original source — we do not rehost the code.

Install and usage instructions live in the source repository linked above.

Reviews

No reviews yet — be the first.

Versions

  • v0.1.0 Imported from the upstream source.