> ## Documentation Index
> Fetch the complete documentation index at: https://mintlify.com/trailbaseio/trailbase/llms.txt
> Use this file to discover all available pages before exploring further.

# Kotlin SDK

> Official Kotlin client for TrailBase

The official Kotlin SDK for TrailBase provides a type-safe, coroutine-based client for accessing your TrailBase backend from JVM, Android, and multiplatform Kotlin applications.

## Installation

### Gradle (Kotlin DSL)

Add to your `build.gradle.kts`:

```kotlin theme={null}
dependencies {
    implementation("io.trailbase:trailbase-client:0.2.1")
}
```

### Gradle (Groovy)

Add to your `build.gradle`:

```groovy theme={null}
dependencies {
    implementation 'io.trailbase:trailbase-client:0.2.1'
}
```

### Maven

```xml theme={null}
<dependency>
    <groupId>io.trailbase</groupId>
    <artifactId>trailbase-client</artifactId>
    <version>0.2.1</version>
</dependency>
```

## Requirements

* Kotlin 2.1+
* Ktor client (automatically included)
* kotlinx.serialization (automatically included)

## Initialization

### Basic Client

```kotlin theme={null}
import io.trailbase.client.Client

val client = Client("https://your-server.trailbase.io")
```

### Client with Tokens

```kotlin theme={null}
import io.trailbase.client.Client
import io.trailbase.client.Tokens

suspend fun main() {
    val tokens = Tokens(
        auth_token = "your-auth-token",
        refresh_token = "your-refresh-token",
        csrf_token = "your-csrf-token"
    )
    
    val client = Client.withTokens(
        "https://your-server.trailbase.io",
        tokens
    )
}
```

## Authentication

### Login

```kotlin theme={null}
suspend fun login() {
    try {
        val tokens = client.login("user@example.com", "password")
        println("Auth token: ${tokens.auth_token}")
        
        val user = client.user()
        user?.let {
            println("Logged in as: ${it.email}")
        }
    } catch (e: Exception) {
        println("Login failed: ${e.message}")
    }
}
```

### Logout

```kotlin theme={null}
suspend fun logout() {
    client.logout()
}
```

### Current User

```kotlin theme={null}
val user = client.user()
user?.let {
    println("User ID: ${it.id}")
    println("Email: ${it.email}")
}
```

### Access Tokens

```kotlin theme={null}
val tokens = client.tokens()
tokens?.let {
    // Persist tokens for later use
    saveTokens(it)
}
```

### Refresh Token

```kotlin theme={null}
suspend fun refresh() {
    client.refreshAuthToken()
}
```

## Record API

### Define Your Record Types

```kotlin theme={null}
import kotlinx.serialization.Serializable

@Serializable
data class Post(
    val id: String,
    val title: String,
    val content: String,
    val author_id: String,
    val created_at: Long
)

@Serializable
data class NewPost(
    val title: String,
    val content: String
)
```

### List Records

```kotlin theme={null}
import io.trailbase.client.Pagination
import io.trailbase.client.ListResponse

suspend fun listPosts() {
    val posts = client.records("posts")
    
    val response: ListResponse<Post> = posts.list(
        pagination = Pagination(
            cursor = null,
            limit = 10,
            offset = 0
        ),
        order = listOf("-created_at"),
        count = true
    )
    
    println("Records: ${response.records.size}")
    println("Total count: ${response.total_count}")
    println("Next cursor: ${response.cursor}")
}
```

### Read a Record

```kotlin theme={null}
import io.trailbase.client.RecordId

suspend fun readPost() {
    val posts = client.records("posts")
    
    // String ID
    val post: Post = posts.read(RecordId.string("post-id"))
    
    // Integer ID
    val post: Post = posts.read(RecordId.int(123))
    
    // With expanded relationships
    val postWithAuthor: Post = posts.read(
        RecordId.string("post-id"),
        expand = listOf("author")
    )
    
    println("Title: ${post.title}")
}
```

### Create a Record

```kotlin theme={null}
suspend fun createPost() {
    val posts = client.records("posts")
    
    val newPost = NewPost(
        title = "Hello World",
        content = "My first post from Kotlin"
    )
    
    val postId = posts.create(newPost)
    println("Created post with ID: $postId")
}
```

### Update a Record

```kotlin theme={null}
suspend fun updatePost() {
    val posts = client.records("posts")
    
    val update = mapOf(
        "title" to "Updated Title"
    )
    
    posts.update(RecordId.string("post-id"), update)
}
```

### Delete a Record

```kotlin theme={null}
suspend fun deletePost() {
    val posts = client.records("posts")
    posts.delete(RecordId.string("post-id"))
}
```

## Filtering

```kotlin theme={null}
import io.trailbase.client.*

suspend fun filterPosts() {
    val posts = client.records("posts")
    
    // Simple equality filter
    val response: ListResponse<Post> = posts.list(
        filters = listOf(
            Filter(column = "author_id", value = userId)
        )
    )
    
    // With comparison operators
    val weekAgo = System.currentTimeMillis() - (7 * 24 * 60 * 60 * 1000)
    val recentPosts: ListResponse<Post> = posts.list(
        filters = listOf(
            Filter(
                column = "created_at",
                op = CompareOp.greaterThan,
                value = weekAgo.toString()
            )
        )
    )
    
    // LIKE operator for text search
    val searchResults: ListResponse<Post> = posts.list(
        filters = listOf(
            Filter(
                column = "title",
                op = CompareOp.like,
                value = "%search%"
            )
        )
    )
    
    // AND composite filter
    val filtered: ListResponse<Post> = posts.list(
        filters = listOf(
            And(listOf(
                Filter(column = "status", value = "published"),
                Filter(column = "author_id", value = userId)
            ))
        )
    )
    
    // OR composite filter
    val filtered: ListResponse<Post> = posts.list(
        filters = listOf(
            Or(listOf(
                Filter(column = "category", value = "tech"),
                Filter(column = "category", value = "science")
            ))
        )
    )
}
```

### Available Comparison Operators

```kotlin theme={null}
enum class CompareOp {
    equal,
    notEqual,
    lessThan,
    lessThanEqual,
    greaterThan,
    greaterThanEqual,
    like,
    regexp,
    stWithin,      // Geospatial
    stIntersects,  // Geospatial
    stContains,    // Geospatial
}
```

## Error Handling

```kotlin theme={null}
try {
    val post: Post = posts.read(RecordId.string("post-id"))
} catch (e: HttpException) {
    println("HTTP ${e.status}: ${e.message}")
} catch (e: Exception) {
    println("Error: ${e.message}")
}
```

## Type Definitions

### User

```kotlin theme={null}
@Serializable
data class User(
    val id: String,
    val email: String
)
```

### Tokens

```kotlin theme={null}
@Serializable
data class Tokens(
    val auth_token: String,
    val refresh_token: String?,
    val csrf_token: String?
)
```

### ListResponse

```kotlin theme={null}
@Serializable
data class ListResponse<T>(
    val records: List<T>,
    val cursor: String? = null,
    val total_count: Int? = null
)
```

### Pagination

```kotlin theme={null}
class Pagination(
    val cursor: String? = null,
    val limit: Int? = null,
    val offset: Int? = null
)
```

### RecordId

```kotlin theme={null}
sealed class RecordId {
    abstract fun id(): String
    
    companion object {
        fun uuid(id: String): RecordId = StringRecordId(id)
        fun string(id: String): RecordId = StringRecordId(id)
        fun int(id: Int): RecordId = IntegerRecordId(id)
    }
}
```

## Android Integration

```kotlin theme={null}
import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.material3.*
import androidx.compose.runtime.*
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import io.trailbase.client.Client
import kotlinx.coroutines.launch
import kotlinx.serialization.Serializable

@Serializable
data class Post(
    val id: String,
    val title: String,
    val content: String
)

class MainActivity : ComponentActivity() {
    private val client = Client("https://your-server.trailbase.io")
    
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContent {
            PostsScreen(client)
        }
    }
}

@Composable
fun PostsScreen(client: Client) {
    var posts by remember { mutableStateOf<List<Post>>(emptyList()) }
    var loading by remember { mutableStateOf(true) }
    var error by remember { mutableStateOf<String?>(null) }
    
    val scope = rememberCoroutineScope()
    
    LaunchedEffect(Unit) {
        scope.launch {
            try {
                val response = client.records("posts")
                    .list<Post>(
                        order = listOf("-created_at"),
                        pagination = io.trailbase.client.Pagination(limit = 20)
                    )
                posts = response.records
                loading = false
            } catch (e: Exception) {
                error = e.message
                loading = false
            }
        }
    }
    
    Column(modifier = Modifier.fillMaxSize().padding(16.dp)) {
        when {
            loading -> CircularProgressIndicator()
            error != null -> Text("Error: $error")
            else -> LazyColumn {
                items(posts) { post ->
                    PostItem(post)
                }
            }
        }
    }
}

@Composable
fun PostItem(post: Post) {
    Card(modifier = Modifier.fillMaxWidth().padding(vertical = 4.dp)) {
        Column(modifier = Modifier.padding(16.dp)) {
            Text(text = post.title, style = MaterialTheme.typography.titleMedium)
            Spacer(modifier = Modifier.height(4.dp))
            Text(text = post.content, style = MaterialTheme.typography.bodyMedium)
        }
    }
}
```

## Best Practices

<Note>
  Use Kotlin coroutines for all async operations. The SDK is built with coroutines in mind.
</Note>

<Warning>
  Store tokens securely using Android's EncryptedSharedPreferences or KeyStore on Android, and appropriate secure storage on other platforms.
</Warning>

<Info>
  The client automatically refreshes auth tokens before they expire.
</Info>

## Example Application

```kotlin theme={null}
import io.trailbase.client.*
import kotlinx.coroutines.runBlocking
import kotlinx.serialization.Serializable

@Serializable
data class Post(
    val id: String,
    val title: String,
    val content: String,
    val published: Boolean
)

fun main() = runBlocking {
    // Initialize client
    val url = System.getenv("TRAILBASE_URL") ?: "http://localhost:4000"
    val client = Client(url)
    
    // Login
    try {
        client.login(
            System.getenv("TRAILBASE_EMAIL") ?: error("Email required"),
            System.getenv("TRAILBASE_PASSWORD") ?: error("Password required")
        )
        
        client.user()?.let { user ->
            println("Logged in as: ${user.email}")
        }
    } catch (e: Exception) {
        println("Login failed: ${e.message}")
        return@runBlocking
    }
    
    // List posts
    val posts = client.records("posts")
    val response: ListResponse<Post> = posts.list(
        order = listOf("-created_at"),
        pagination = Pagination(limit = 10),
        filters = listOf(
            Filter(column = "published", value = "true")
        )
    )
    
    println("\nFound ${response.records.size} posts:")
    response.records.forEach { post ->
        println("- ${post.title}")
    }
    
    // Create a new post
    @Serializable
    data class NewPost(
        val title: String,
        val content: String,
        val published: Boolean
    )
    
    val newPost = NewPost(
        title = "Hello from Kotlin",
        content = "This post was created using the TrailBase Kotlin SDK",
        published = true
    )
    
    val newPostId = posts.create(newPost)
    println("\nCreated new post with ID: $newPostId")
    
    // Read the post
    val post: Post = posts.read(newPostId)
    println("Post title: ${post.title}")
    
    // Logout
    client.logout()
    println("\nLogged out")
}
```
