Getting started
Five minutes: install, one entity, one query, against a real (local) ScyllaDB instance. This page
intentionally skips the reasoning behind every decision — for that, go build the full app in
/tutorial, which explains every line as it's written. If you haven't read
/foundations yet, do that first; nothing below will feel arbitrary once you have.
This page assumes a Ktor server project with Kotlin 2.1+, KSP configured, and Docker available for
a local ScyllaDB instance. The full project scaffold, every dependency explained, is
/tutorial/02-project-setup.
1. Run a local ScyllaDB
services:
scylla:
image: scylladb/scylla:latest
ports:
- "9042:9042"
command: --smp 1 --memory 750M --overprovisioned 1 --api-address 0.0.0.0docker compose up -d2. Add the dependencies
dependencies {
implementation(platform("ke.co.coinx.kandra:kandra-bom:0.4.6"))
implementation("ke.co.coinx.kandra:kandra-ktor")
implementation("ke.co.coinx.kandra:kandra-koin")
ksp("ke.co.coinx.kandra:kandra-codegen")
}The BOM aligns every Kandra module's version so you never repeat a version string; kandra-ktor is
the plugin; kandra-koin auto-binds repositories into Koin; kandra-codegen (via KSP) generates
type-safe *Table objects for the query DSL. See
/tutorial/02-project-setup for why each of these, in depth,
including the alternatives (kandra-kodein instead of kandra-koin).
3. Define an entity
import io.kandra.core.annotations.*
import java.time.Instant
import java.util.UUID
@ScyllaTable("users")
data class User(
@PartitionKey val userId: UUID,
@LookupIndex(tableSuffix = "by_email", consistency = LookupConsistency.BATCH)
val email: String,
val name: String,
@CreatedAt val createdAt: Instant = Instant.EPOCH,
)@PartitionKey decides which node(s) this row lives on — see
/foundations if "why this column"
isn't obvious yet. @LookupIndex generates and maintains a second table, users_by_email, so
"find by email" doesn't need a full scan.
4. Install the plugin
import io.kandra.ktor.*
fun Application.configureDatabase() {
install(Kandra) {
contactPoints = "localhost:9042"
keyspace = "quickstart"
localDatacenter = "datacenter1"
autoCreateKeyspace = true
schemaMode = SchemaMode.AUTO_CREATE
register(User::class)
auth { provider = KandraAuth.static("cassandra", "cassandra") } // local dev only, see below
}
kandraKoin()
}schemaMode = SchemaMode.AUTO_CREATE runs CREATE TABLE IF NOT EXISTS for every registered
entity at startup — right for a quickstart, not what you'd run in a production deploy pipeline (see
/philosophy/schema-safety).
AuthConfig.provider defaults to KandraAuth.fromEnv(), and install(Kandra) calls it
unconditionally while building the driver session — there's no "skip auth for local dev" default.
Without the override above, this app throws KandraAuthException at startup unless
SCYLLA_USERNAME/SCYLLA_PASSWORD are set in the environment, even though the docker-compose
ScyllaDB above has no authentication enabled. KandraAuth.static(...) is fine for local dev;
KandraAuth.fromEnv() (the default) is the right choice once there's a real cluster with real
credentials — see /tutorial/04-plugin-install.
5. Save and query
import io.kandra.runtime.repository.KandraSuspendRepository
import org.koin.core.qualifier.named
import org.koin.ktor.ext.inject
fun Route.userRoutes() {
val userRepo by inject<KandraSuspendRepository<*>>(named("UserSuspendRepo"))
post("/users") {
val user = call.receive<User>()
userRepo.save(user)
call.respond(HttpStatusCode.Created)
}
get("/users/by-email/{email}") {
val email = call.parameters["email"]!!
val user = userRepo.find { UserTable.email eq email }
call.respond(user ?: HttpStatusCode.NotFound)
}
}UserTable is generated by kandra-codegen from the User entity — a compile-time-checked
reference to the email column, not a magic string. userRepo.find { UserTable.email eq email }
resolves through the users_by_email lookup table Kandra generated in step 3, not a full scan of
users.
That's the whole loop: an annotated entity, a plugin install, a type-safe query. Nothing here explains why each piece is shaped this way — that's what the rest of the site is for.
Next: /tutorial builds a real multi-user app — a todo list — from data modeling
through shipping, with every decision explained as it's made. It's deliberately more thorough than
this page; that's the point.