Installation
Gradle (Kotlin DSL)
Add to yourbuild.gradle.kts:
dependencies {
implementation("io.trailbase:trailbase-client:0.2.1")
}
Gradle (Groovy)
Add to yourbuild.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")
}