appwrite-kotlin について
appwrite-kotlin は、Android と Kotlin JVM アプリ向けの Appwrite SDK です。クライアントとサーバーの開発者が、認証、データベース、ストレージ、リアルタイム購読、関数実行を利用でき、SSR 認証の例も含みます。
主な機能
- 01ユーザー認証と管理
- 02TablesDB の操作とクエリ
- 03ファイルストレージと権限設定
- 04チーム管理とリアルタイム購読
- 05Appwrite 関数の実行
活用シーン
- 01Android アプリへの Appwrite 認証の追加
- 02Kotlin サーバーでのユーザーとデータの管理
- 03行やファイルのリアルタイム変更の購読
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ appwrite/ skills --skill appwrite-kotlinインストール後、Claude Code で/appwrite-kotlinを入力して読み込まれたか確認します。
詳細
Appwrite Kotlin SDK
インストール
// build.gradle.kts — Android
implementation("io.appwrite:sdk-for-android:+")
// build.gradle.kts — Server (Kotlin JVM)
implementation("io.appwrite:sdk-for-kotlin:+")
クライアントのセットアップ
クライアントサイド(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]")
サーバーサイド(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"))
コード例
認証(クライアントサイド)
val account = Account(client)
// Signup
account.create(
userId = ID.unique(),
email = "[email protected]",
password = "password123",
name = "User Name"
)
// Login
val session = account.createEmailPasswordSession(
email = "[email protected]",
password = "password123"
)
// OAuth
account.createOAuth2Session(activity = activity, provider = OAuthProvider.GOOGLE)
// Get current user
val user = account.get()
// Logout
account.deleteSession(sessionId = "current")
ユーザー管理(サーバーサイド)
val users = Users(client)
// Create user
val user = users.create(
userId = ID.unique(),
email = "[email protected]",
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]")
データベース操作
Note: すべての新しいコードでは、
TablesDB(非推奨のDatabasesクラスではない)を使用してください。既存のコードベースがすでにDatabasesに依存している場合、またはユーザーが明示的に要求した場合にのみDatabasesを使用してください。Tip: すべての SDK メソッド呼び出しでは、名前付き引数(例:
databaseId = "...")を優先してください。既存のコードベースがすでに位置引数を使用している場合、またはユーザーが明示的に要求した場合にのみ、位置引数を使用してください。
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]"
)
文字列カラム型
Note: 従来の
string型は非推奨です。すべての新しいカラムには明示的なカラム型を使用してください。
| 型 | 最大文字数 | インデックス | ストレージ |
|---|---|---|---|
varchar | 16,383 | 完全インデックス(サイズが 768 以下の場合) | 行内にインライン保存 |
text | 16,383 | プレフィックスのみ | オフページ |
mediumtext | 4,194,303 | プレフィックスのみ | オフページ |
longtext | 1,073,741,823 | プレフィックスのみ | オフページ |
varcharはインラインで保存され、64 KB の行サイズ制限にカウントされます。名前、スラッグ、識別子などの短くインデックスされたフィールドに適しています。text、mediumtext、longtextはオフページで保存されるため(行内には 20 バイトのポインタのみが存在します)、行サイズの予算を消費しません。これらの型にはsizeは必要ありません。
// 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),
)
)
クエリメソッド
// 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)
Query.relationship("comments", listOf(Query.equal("approved", true), Query.limit(25))) // related documents
// Aggregations (optional alias)
Query.count("*", "total")
Query.sum("amount", "revenue")
Query.avg("score", "avgScore")
Query.min("price", "minPrice")
Query.max("price", "maxPrice")
// Grouping
Query.groupBy(listOf("status", "year"))
Query.having(listOf(Query.greaterThan("total", 1)))
Query.distinct()
// Joins (table id, left key, right key, operator, alias)
Query.join("orders", "\$id", "customerId")
Query.leftJoin("orders", "\$id", "customerId", "=", "ord")
Query.leftJoin("orders", "ord", listOf(
Query.on("\$id", "customerId"),
Query.equal("ord.status", "paid"),
))
// Fluent builder. Pass build() into list methods.
val queries = Query.builder()
.equal("status", "published")
.leftJoin("orders", "\$id", "customerId", "=", "ord")
.count("*", "total")
.groupBy(listOf("status"))
.limit(25)
.build()
tablesDB.listRows(
databaseId = "[DATABASE_ID]",
tableId = "[TABLE_ID]",
queries = queries,
)
ファイルストレージ
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 ファクトリメソッド
import io.appwrite.models.InputFile
InputFile.fromPath("/path/to/file.png") // from filesystem path
InputFile.fromBytes(byteArray, "file.png") // from ByteArray
チーム
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 = "[email protected]"
)
// 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.team("[TEAM_ID]")を、特定のチームロールにはRole.team("[TEAM_ID]", "editor")を使用してください。
リアルタイムサブスクリプション(クライアントサイド)
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()
利用可能なチャンネル:
| チャンネル | 説明 |
|---|---|
account | 認証済みユーザーのアカウントへの変更 |
tablesdb.[DB_ID].tables.[TABLE_ID].rows | テーブル内のすべての行 |
tablesdb.[DB_ID].tables.[TABLE_ID].rows.[ROW_ID] | 特定の行 |
buckets.[BUCKET_ID].files | バケット内のすべてのファイル |
buckets.[BUCKET_ID].files.[FILE_ID] | 特定のファイル |
teams | ユーザーが所属するチームへの変更 |
teams.[TEAM_ID] | 特定のチーム |
memberships | ユーザーのチームメンバーシップ |
functions.[FUNCTION_ID].executions | 関数実行の更新 |
レスポンスフィールド: events(配列)、payload(リソース)、channels(一致したチャンネル)、timestamp(ISO 8601)。
サーバーレス関数(サーバーサイド)
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]")
関数ハンドラーの記述(Kotlin ランタイム)
// 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
}
サーバーサイドレンダリング(SSR)認証
Kotlin サーバーフレームワーク(Ktor、Spring Boot など)を使用する SSR アプリは、認証を処理するために サーバー SDK を使用します。2 つのクライアントが必要です:
- 管理者クライアント — API キーを使用し、セッションを作成し、レート制限をバイパスします(再利用可能なシングルトン)
- セッションクライアント — セッション Cookie を使用し、ユーザーの代理として動作します(リクエストごとに作成し、決して共有しないでください)
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)
}
メール/パスワードログイン(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))
}
認証済みリクエスト
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 フロー
// 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 のセキュリティ: XSS を防ぐために、常に
httpOnly、secure、SameSite=Strictを使用してください。Cookie 名はa_session_<PROJECT_ID>である必要があります。
ユーザーエージェントの転送: デバッグとセキュリティのためにエンドユーザーのブラウザ情報を記録するには、
sessionClient.setForwardedUserAgent(call.request.headers["User-Agent"])を呼び出してください。
エラーハンドリング
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)
}
一般的なエラーコード:
| コード | 意味 |
|---|---|
401 | 認証されていません — セッション/API キーが欠落しているか無効です |
403 | 禁止されています — 権限が不十分です |
404 | 見つかりません — リソースが存在しません |
409 | 競合 — ID の重複または一意制約違反 |
429 | レート制限 — リクエストが多すぎます |
権限とロール (重要)
Appwrite は権限文字列を使用してリソースへのアクセスを制御します。各権限は、アクション (read、update、delete、create、または create + update + delete を許可する write) とロールターゲットを組み合わせたものです。デフォルトでは、行/ファイルレベルで権限を明示的に設定するか、テーブル/バケットの設定から継承しない限り、どのユーザーもアクセスできません。権限は Permission および Role ヘルパーで構築された文字列の配列です。
import io.appwrite.Permission
import io.appwrite.Role
権限付きのデータベース行
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
)
)
権限付きのファイルアップロード
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]")),
)
)
権限を設定するタイミング: リソースごとのアクセス制御が必要な場合は、行/ファイルレベルの権限を設定します。テーブル内のすべての行が同じルールを共有する場合は、テーブル/バケットレベルで権限を設定し、行の権限は空のままにします。
よくある間違い:
- 権限の設定忘れ — リソースがすべてのユーザー (作成者を含む) からアクセスできなくなります
write/update/deleteとRole.any()の併用 — 未認証のゲストを含む任意のユーザーがリソースを変更または削除できるようになります- 機密データに対する
Permission.read(Role.any())— リソースが公開で読み取り可能になります