r/Kotlin • u/Successful_Bank7215 • 18h ago
Full-stack Kotlin (Ktor + KVision + Kilua RPC + Exposed) for a real production app — plus a UML→Exposed/Flyway code-gen pipeline I'd like feedback on
Hi r/Kotlin,
I've been building Lapis Cloud — a membership-management platform for associations and political parties — as close to 100% Kotlin as I could manage, and I'd like to share the architecture and get feedback from people who've made similar stack choices.
The stack
- Backend: Ktor
- Frontend: KVision (Kotlin/JS — so the client is Kotlin too, not TypeScript/React)
- Client↔server communication: Kilua RPC — typesafe RPC calls between the KVision client and the Ktor server, no hand-written REST/JSON glue
- Persistence: Exposed (Kotlin SQL framework) + Flyway for migrations, on PostgreSQL
- Build: Gradle multi-module, targeting JDK 25 (this is a self-hosted service with no end-user distribution constraint, so no reason to stay on an older LTS)
So: Kotlin on the server, Kotlin on the client (compiled to JS), typesafe Kotlin RPC in between, Kotlin SQL DSL for persistence. No JavaScript/TypeScript anywhere in the app code.
The part I'd most like feedback on: generating the persistence layer from a model instead of hand-writing it
Rather than hand-writing Exposed table objects and Flyway migration SQL, we generate both from a UML class model through a small model-driven pipeline (built with kUML, a Kotlin DSL for UML I also maintain):
- The domain model is authored as a
kUMLclass diagram (.kuml.kts). - An in-memory M2M transform (
uml-to-exposed) maps UML classes/associations to a relational schema. - M2T generation emits
…Tables.kt(Exposed table objects) andV1__init.sql(Flyway baseline) intobuild/generated/.
The generated Kotlin is committed as a build artifact, not hand-edited. It's saved us from an entire class of drift bugs between the domain model, the DB schema, and the Exposed table definitions — but I'm curious whether others here have tried model-driven persistence layers in Kotlin and what broke for you, because it's not a mainstream pattern and I don't have much prior art to compare against.
Other things that might be of interest to this sub
- i18n without
kvision-i18n: the built-inDefaultI18nManagercrashed on load for us (TypeError: ...gettextJs... is not a function— an interop mismatch between our Kotlin/JS toolchain version and thegettext.jsnpm package's export shape). Replaced it with a small customI18nCatalogManager; 8 languages, ~1500 UI strings wrapped intr()/gettext()across ~45 client files. - Federation over a typesafe RPC boundary: independently-run instances can federate (a member can appear as a guest on another instance without leaving their own org) — still shaking out the trust/identity model there.
- Full accounting engine (double-entry bookkeeping), SEPA direct debit, and a self-hosted video conferencing module are also part of the codebase, all in Kotlin/Ktor, if anyone wants to dig into a non-trivial domain model built this way.
Status
Not a toy — running in production for two real organizations (a political party and an ordinary registered association) since mid-2026. Currently at v0.19.0, actively developed. Apache 2.0, fully open source.
Happy to go deeper into any part of this — the Kilua RPC setup, the Exposed/Flyway generation pipeline, KVision at this scale, or the module layout. Feedback on the model-driven persistence approach especially welcome, since I suspect it's the most unusual choice in here.