Skip to main content
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:
dependencies {
    implementation("io.trailbase:trailbase-client:0.2.1")
}

Gradle (Groovy)

Add to your build.gradle:
dependencies {
    implementation 'io.trailbase:trailbase-client:0.2.1'
}

Maven

<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

import io.trailbase.client.Client

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

Client with Tokens

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

suspend fun login() {
    try {
        val tokens = client.login("[email protected]", "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

suspend fun logout() {
    client.logout()
}

Current User

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

Access Tokens

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

Refresh Token

suspend fun refresh() {
    client.refreshAuthToken()
}

Record API

Define Your Record Types

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

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

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

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

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

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

Filtering

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

enum class CompareOp {
    equal,
    notEqual,
    lessThan,
    lessThanEqual,
    greaterThan,
    greaterThanEqual,
    like,
    regexp,
    stWithin,      // Geospatial
    stIntersects,  // Geospatial
    stContains,    // Geospatial
}

Error Handling

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

@Serializable
data class User(
    val id: String,
    val email: String
)

Tokens

@Serializable
data class Tokens(
    val auth_token: String,
    val refresh_token: String?,
    val csrf_token: String?
)

ListResponse

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

Pagination

class Pagination(
    val cursor: String? = null,
    val limit: Int? = null,
    val offset: Int? = null
)

RecordId

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

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

Use Kotlin coroutines for all async operations. The SDK is built with coroutines in mind.
Store tokens securely using Android’s EncryptedSharedPreferences or KeyStore on Android, and appropriate secure storage on other platforms.
The client automatically refreshes auth tokens before they expire.

Example Application

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")
}

Build docs developers (and LLMs) love