Cies
06/10/2025, 11:37 PMauthenticated
role, you simply need the service_role
for writes); this to keep RLS dead simple and still useful (as an extra safety against data leaks and to allow Supabase goodness like GraphQL and realtime to be used later on). Both JWT-from-cookie and db-connection-with-supabase-auth are implemented as http4k `Filter`s.
The idea is: start SSR, because it is just waaay simpler, then add little browser apps on top later where it adds maximum value (and for that Supabase is really nice with GraphQL and realtime).
It used kotlinx.html for template rendering (with a layouting system) and Konform for FormDto validation.
Jdbi was chosen as I want to be able to write "just SQL". I might one day convert it to terpal-sql (the basis of ExoQuery) but it did not work out of the box, so I went with time-tested Jdbi.
Feel free to AMA, and if you have a look at the code and see any room for improvement: please let me know! I'm really hungry for feedback.
https://github.com/cies/supabase-http4k-ssr-jdbidave
06/11/2025, 8:37 AMCies
06/11/2025, 8:51 AMCies
06/11/2025, 9:24 AMkotlin-reflect
. Would like to use terpal-sql instead (which uses kotlinx.serialization), but I stumbled on errors trying it out, and it is sooo young that I dont feel very confident depending on it.dave
06/11/2025, 9:25 AMCies
06/11/2025, 9:35 AMdave
06/11/2025, 9:38 AMCies
06/11/2025, 9:52 AMCies
06/11/2025, 12:22 PMterpal-sql
to work, so no need for Jdbi (and jdbi3-kotlin
which ropes in kotlin-reflect
). Now see if I can make a deserializer for form data (with a naming scheme for the "names" or "keys" of the key-value pairs) to data objects with an @Serializable annotation.dave
06/11/2025, 1:49 PMdata class Foo(val anInt: Int)
val formLens = Body.autoWebForm().toLens()
val foo = formLens(request)
although that wouldn't give you the fully featured validations. we could do something with delegates though 🤔Cies
06/11/2025, 2:33 PMname
fields. We have standardized on the naming scheme of Konform (a validation lib for Kotlin that's quite nice and we also wanted to use).
The naming scheme gives structure to the fields that are otherwise just a flat k/v list.
The convention is simple: property names start with a dot (`.`) and array elements are
specified with a number between brackets (`[]`).
So this:
.x.y[0].v.w[0]=1
would deserialize to this (if JSON):
{
"x": {
"y": [
{
"v": {
"w": [
"1"
]
}
}
]
}
}
Cies
06/11/2025, 2:40 PMdata class RootFormDto(val x: YFormDto)
data class YFormDto(val y: List<VFormDto>)
data class VFormDto(val v: WFormDto)
data class WFormDto(val w: List<Int>)
We have quite complex forms at times. Being able to express them in www-form-urlencoded strings made a big difference.Cies
06/11/2025, 3:01 PM@JsonClass(generateAdapter = true)
data class BookModel(
val title: String,
@Json(name = "page_count") val pageCount: Int,
val genre: Genre,
val author: Author
) {
enum class Genre {
FICTION,
NONFICTION,
}
@JsonClass(generateAdapter = true)
data class Author(val firstName: String?, val lastName: String?)
}
private val moshi = Moshi.Builder().build()
@OptIn(ExperimentalStdlibApi::class)
private val adapter = moshi.adapter<BookModel>()
fun main() {
val book = adapter.fromJsonValue(
mapOf(
"title" to "a",
"page_count" to "123",
"genre" to "FICTION",
"author" to mapOf("firstName" to "a", "lastName" to "b")
)
)
println(book)
}
Cies
06/11/2025, 3:05 PMCies
06/13/2025, 8:00 PMCies
06/13/2025, 8:00 PMCies
06/13/2025, 8:02 PM```
Configuration name: "implementationDependenciesMetadata"
Total dependencies size: 4.92 Mb
postgresql-42.7.3.jar 1,063.78 kb
kotlinx-html-jvm-0.11.0.jar 865.22 kb
http4k-core-6.9.2.0.jar 836.43 kb
okhttp-4.12.0.jar 771.03 kb
kotlinx-serialization-json-jvm-1.8.1.jar 270.48 kb
http4k-realtime-core-6.9.2.0.jar 198.30 kb
kotlin-stdlib-2.1.20-all.jar 188.35 kb
HikariCP-5.1.0.jar 158.05 kb
kotlin-logging-jvm-7.0.7.jar 106.46 kb
http4k-format-core-6.9.2.0.jar 96.04 kb
slf4j-api-2.0.17.jar 68.27 kb
kotlinx-html-metadata-0.11.0.jar 68.15 kb
http4k-config-6.9.2.0.jar 59.66 kb
http4k-format-kotlinx-serialization-6.9.2.0.jar 56.52 kb
kotlinx-serialization-core-metadata-1.8.1.jar 53.57 kb
kotlinx-serialization-json-metadata-1.8.1.jar 42.05 kb
okio-metadata-3.6.0-all.jar 36.46 kb
http4k-client-okhttp-6.9.2.0.jar 28.90 kb
result4k-2.22.3.0.jar 27.39 kb
konform-metadata-0.11.0.jar 19.92 kb
slf4j-simple-2.0.17.jar 15.35 kb
terpal-runtime-metadata-2.1.0-2.0.0.PL.jar 6.83 kb
kotlin-stdlib-jdk8-1.8.21.jar 0.95 kb
kotlin-stdlib-jdk7-1.8.21.jar 0.94 kb
terpal-sql-jdbc-metadata-2.0.0.PL-1.2.0.jar 0.63 kb
```
Cies
06/13/2025, 8:03 PMCies
06/14/2025, 12:31 AMfun signUpPostHandler(req: Request): Response {
val formDto: SignUpForm = req.form().decodeOrFailWith { reason ->
log.warn { "Failed to decode SignUpForm: $reason" }
return Response(BAD_REQUEST)
}
when (val validationResult = formDto.validate()) {
is Invalid -> return signUpPage(formDto, validationResult)
is Valid<SignUpForm> -> {
// ...
Cies
06/14/2025, 12:32 AMinline fun <reified T> List<Pair<String, String?>>.decodeOrFailWith(block: (String) -> Nothing): T {
val jsonElement = formToJsonElement(this).onFailure { block(it.reason) }
try {
// Maybe build in leniency + heavy warnings in some cases
return strictJsonDecoder.decodeFromJsonElement<T>(jsonElement)
} catch (e: Exception) {
block(e.message ?: "Unknown deserialization error")
}
}
Sweet.Cies
06/14/2025, 12:34 AMfun formToJsonElement(pairs: List<Pair<String, String?>>): Result<JsonElement, String> { ... }
, but i still have to make all tests pass there.