
Full-stack CRUD toolkit with MongoDB/SQL backend-agnostic repository pattern, declarative view config, Tabulator grids, RBAC, change logs, file attachments, SSR, shared models, hooks, and API contract discovery.
Kotlin Multiplatform full-stack CRUD library for MongoDB and SQL backends with KVision frontend.
FSLib provides a backend-agnostic repository pattern, declarative view configuration, Tabulator-based data grids, role-based access control, change logging, and shared data models across JVM/JS targets. It eliminates repetitive CRUD boilerplate so you can focus on business logic.
IRepository interface with cross-engine dependency checking.ConfigViewList / ConfigViewItem. The framework handles routing, pagination, forms, and CRUD operations.simpleContainer() factories for data entities, simpleCommon() for non-data views (landing pages, dashboards), StandardCrudService for zero-boilerplate service delegation, and registerEntityViews() for declarative view wiring.TabulatorViewList.onQueryCreate, onBeforeUpdateAction, onAfterDeleteAction, onValidate, and many more hooks on the repository for validation, transformation, and side effects.IRolePermissionProvider.DataMedia support (via the :media module) for managing file uploads with thumbnails and metadata.OId (MongoDB ObjectId), IntId, LongId, StringId — all with custom serializers.Coll, and subtype-specific repositories. Shared lookups, hooks, and indexes are defined once in the abstract base.:memorydb module provides an InMemoryRepository for samples, tests, and prototyping without any database engine.@RpcBindingRoute annotation for human-readable route paths (/rpc/ITaskService.apiList). The RouteContract class exposes a /apiContract endpoint for third-party client (Android, etc.) route discovery.:ssr module provides SSR support using Ktor HTML builder.your-app ──> fullstack ──> core
mongodb ──> fullstack, core
sql ──> fullstack, core
memorydb ──> fullstack, core
media ──> fullstack, core, mongodb
ssr ──> fullstack, core, mongodb
| Module | Purpose |
|---|---|
:core |
Platform-independent foundation: BaseDoc<ID>, ID types, annotations, serializers, state management, user/role models, API framework, date/math utilities. |
:fullstack |
Core library. jvmMain: IRepository interface, IRolePermissionProvider, PermissionRegistry, permissions, change logging, RouteContract for API contract discovery, Ktor server stack. jsMain: View system, configuration, Tabulator wrappers, layout helpers, ViewRegistry. commonMain: Shared RPC interfaces via Kilua RPC. |
:mongodb |
MongoDB engine (JVM-only). Coll implementation with aggregation pipelines, lookups, filtering, change logging, and role-based access via KMongo coroutine driver. |
:sql |
SQL engine (JVM-only). SqlRepository implementation using Exposed for relational database access with type-aware filtering and identifier quoting. |
:memorydb |
In-memory database engine (JVM-only). InMemoryRepository using ConcurrentHashMap for storage. Designed for samples, tests, and prototyping — no database engine required. |
:media |
Extensions: DataMedia (file attachments) and ChangeLog views built on top of :fullstack. |
:ssr |
Server-side rendering with Ktor HTML builder. |
| Component | Technology | Version |
|---|---|---|
| Language | Kotlin (Multiplatform) | 2.3.x |
| Backend | Ktor (Netty) | 3.4.x |
| MongoDB | KMongo (coroutine) | 5.5.x |
| SQL | Exposed | 0.61.x |
| Frontend | KVision | 9.4.x |
| RPC | Kilua RPC | 0.0.42 |
| Serialization | kotlinx-serialization | 1.10.x |
| JVM | Toolchain 21 |
FSLib is available on Maven Central.
Add the dependency to your module's build.gradle.kts:
// Version catalog (gradle/libs.versions.toml)
[versions]
fslib = "3.1.2"
[libraries]
fslib-core = { module = "com.fonrouge.fslib:core", version.ref = "fslib" }
fslib-fullstack = { module = "com.fonrouge.fslib:fullstack", version.ref = "fslib" }
fslib-mongodb = { module = "com.fonrouge.fslib:mongodb", version.ref = "fslib" }
fslib-sql = { module = "com.fonrouge.fslib:sql", version.ref = "fslib" }
fslib-memorydb = { module = "com.fonrouge.fslib:memorydb", version.ref = "fslib" }
fslib-media = { module = "com.fonrouge.fslib:media", version.ref = "fslib" }
fslib-ssr = { module = "com.fonrouge.fslib:ssr", version.ref = "fslib" }// build.gradle.kts — In-memory (prototyping/samples, no database required)
kotlin {
sourceSets {
commonMain {
dependencies {
api("com.fonrouge.fslib:fullstack:3.1.2")
}
}
jvmMain {
dependencies {
implementation("com.fonrouge.fslib:memorydb:3.1.2")
}
}
}
}// build.gradle.kts — MongoDB application
kotlin {
sourceSets {
commonMain {
dependencies {
api("com.fonrouge.fslib:fullstack:3.1.2")
}
}
jvmMain {
dependencies {
implementation("com.fonrouge.fslib:mongodb:3.1.2")
}
}
}
}// build.gradle.kts — SQL application
kotlin {
sourceSets {
commonMain {
dependencies {
api("com.fonrouge.fslib:fullstack:3.1.2")
}
}
jvmMain {
dependencies {
implementation("com.fonrouge.fslib:sql:3.1.2")
}
}
}
}// build.gradle.kts — Hybrid (MongoDB + SQL)
kotlin {
sourceSets {
commonMain {
dependencies {
api("com.fonrouge.fslib:fullstack:3.1.2")
}
}
jvmMain {
dependencies {
implementation("com.fonrouge.fslib:mongodb:3.1.2")
implementation("com.fonrouge.fslib:sql:3.1.2")
}
}
}
}./gradlew publishToMavenLocalThis publishes :core, :fullstack, :mongodb, :sql, :memorydb, :media, and :ssr to your local Maven repository (~/.m2/repository).
@Serializable
@Collection("customers")
data class Customer(
override val _id: OId<Customer> = OId(),
val name: String = "",
val email: String = "",
val active: Boolean = true,
) : BaseDoc<OId<Customer>>object CommonCustomer : ICommonContainer<Customer, OId<Customer>, CustomerFilter>(
itemKClass = Customer::class,
filterKClass = CustomerFilter::class,
labelItem = "Customer",
labelList = "Customers",
labelId = { it?.name ?: "" },
)// Or use the simpleContainer factory (when using ApiFilter):
val CommonCustomer = simpleContainer<Customer, OId<Customer>>(
labelItem = "Customer",
labelList = "Customers",
labelId = { it?.name ?: "" },
)@KiluaRpcServiceName("ICustomerService")
interface ICustomerService {
suspend fun apiItem(iApiItem: IApiItem<Customer, OId<Customer>, CustomerFilter>): ItemState<Customer>
suspend fun apiList(apiList: ApiList<CustomerFilter>): ListState<Customer>
}class CustomerColl : Coll<Customer, OId<Customer>, CustomerFilter, OId<User>>(
commonContainer = CommonCustomer,
mongoDatabase = MongoDb.database,
) {
override fun findItemFilter(apiFilter: CustomerFilter): Bson? {
// Custom filtering logic
return apiFilter.nameSearch?.let {
Customer::name regex Regex(it, RegexOption.IGNORE_CASE)
}
}
}class CustomerSqlRepo : SqlRepository<Customer, OId<Customer>, CustomerFilter, OId<User>>(
commonContainer = CommonCustomer,
sqlDatabase = mySqlDatabase,
) {
override fun buildWhereFromApiFilter(
apiFilter: CustomerFilter,
whereClauses: MutableList<String>,
whereArgs: MutableList<Pair<IColumnType<*>, Any?>>,
) {
apiFilter.nameSearch?.let {
whereClauses += "name LIKE ?"
whereArgs += VarCharColumnType() to "%$it%"
}
}
}// List view configuration (in ViewListCustomer companion)
companion object {
val configViewList = configViewList(
viewKClass = ViewListCustomer::class,
commonContainer = CommonCustomer,
apiListFun = ICustomerService::apiList,
)
}
// Item view configuration (in ViewItemCustomer companion)
companion object {
val configViewItem = configViewItem(
viewKClass = ViewItemCustomer::class,
commonContainer = CommonCustomer,
apiItemFun = ICustomerService::apiItem,
)
}
// Register views in App.start() using the DSL:
val reg = registerEntityViews(getServiceManager<ICustomerService>()) {
list(ViewListCustomer.configViewList, isDefault = true)
item(ViewItemCustomer.configViewItem)
}
KVWebManager.initialize { defaultView = reg.defaultView }class ViewListCustomer : ViewList<Customer, OId<Customer>, CustomerFilter, Unit>() {
override val configView = ConfigViewListCustomer
override fun Container.displayPage() {
fsTabulator(viewList = this@ViewListCustomer) {
addColumn("Name") { it.name }
addColumn("Email") { it.email }
addColumn("Active") { if (it.active) "Yes" else "No" }
}
}
}
class ViewItemCustomer : ViewItem<Customer, OId<Customer>, CustomerFilter>() {
override val configView = ConfigViewItemCustomer
override fun Container.displayPage() {
formPanel = ViewFormPanel.xcreate(viewItem = this@ViewItemCustomer) {
formRow {
text(label = "Name", value = Customer::name)
text(label = "Email", value = Customer::email)
}
}
}
}The IRepository interface provides hooks at every stage of CRUD operations:
Query Phase (validation) Action Phase (mutation)
───────────────────── ──────────────────────
onQueryCreate onBeforeCreateAction → DB INSERT → onAfterCreateAction
onQueryRead
onQueryUpdate onBeforeUpdateAction → DB UPDATE → onAfterUpdateAction
onQueryDelete onBeforeDeleteAction → DB DELETE → onAfterDeleteAction
onQueryCreateItem onBeforeUpsertAction (shared create/update)
onQueryUpsert (shared) onAfterUpsertAction (shared create/update)
onValidate (content validation)
Override any hook in your repository class:
class CustomerColl : Coll<...>(...) {
override suspend fun onValidate(apiItem: ApiItem<...>, item: Customer): SimpleState {
if (item.email.isBlank()) return simpleErrorState("Email is required")
return SimpleState(true)
}
override suspend fun onBeforeCreateAction(apiItem: ApiItem.Action.Create<...>): ItemState<Customer> {
// Transform item before insert
return ItemState(item = apiItem.item.copy(name = apiItem.item.name.trim()))
}
override suspend fun onAfterCreateAction(apiItem: ApiItem.Action.Create<...>, itemState: ItemState<Customer>) {
// Side effects after insert (send email, update cache, etc.)
}
}Located in com.fonrouge.base.annotations:
| Annotation | Target | Purpose |
|---|---|---|
@Collection(name) |
Class | Maps class to MongoDB collection or SQL table name |
@Computed |
Property | Marks a body property as intentionally non-persisted (see Constructor-Only Persistence) |
@SqlField(name, compound) |
Property | Maps property to a specific SQL column name or marks it as a compound (nested) field |
@SqlIgnoreField |
Property | Excludes property from SQL INSERT/UPDATE statements |
@SqlOneToOne |
Property | Marks a one-to-one relationship for SQL mapping |
@PreLookupField |
Property | Indicates a pre-lookup field for initial filtering |
FSLib enforces a convention: only primary constructor parameters of BaseDoc subclasses are persisted to the database. Properties declared in the class body are automatically stripped before writes. This is handled by ConstructorCopier, a shared utility used by all repository engines (MongoDB, SQL, InMemory).
@Serializable
data class Product(
override val _id: String, // persisted (constructor parameter)
val name: String = "", // persisted
val price: Double = 0.0, // persisted
) : BaseDoc<String> {
@Computed
val displayPrice: String // NOT persisted (body property)
get() = "$$price"
}Use the @Computed annotation on body properties to make the non-persisted intent explicit and self-documenting.
FSLib includes a built-in RBAC system:
IAppRole — Defines available roles (per class, per CRUD task)IRoleInUser — Assigns roles to individual users (Allow / Deny / Default)IGroupOfUser — Groups users togetherIRoleInGroup — Assigns roles to groupsIUserGroup — Links users to groups with inherited rolesPermissions are checked automatically on every CRUD operation via getCrudPermission(). Roles are auto-created for new repository classes on first access.
The permission system is decoupled from the database engine through IRolePermissionProvider and PermissionRegistry. The MongoDB module registers its provider automatically; SQL repositories consume it without importing MongoDB types.
Enable audit trails by providing a changeLogCollFun on your repository:
class CustomerColl : Coll<...>(...) {
override val changeLogCollFun = { ChangeLogColl() }
}Every create, update, and delete operation automatically records:
View change logs with the IViewListChangeLog interface from the :media module.
The :media module provides file attachment support:
// Define your DataMedia model implementing IDataMedia<User, OId<User>>
// Use IDataMediaColl for the MongoDB collection
// Use IViewListDataMedia for the frontend view with upload, thumbnail preview, and downloadFeatures: file upload with type filtering, thumbnail generation, ordering, metadata tracking (size, content type, user, date).
FSLib supports module-scoped contextual help. See HELP-DOCS-GUIDE.md for the complete guide.
Directory structure:
help-docs/
ViewListCustomer/
tutorial.html # Step-by-step guide
context.html # Quick reference
ViewItemCustomer/
tutorial.html
context.html
Help buttons appear automatically when documentation files exist for a view.
FSLib includes a system for exposing RPC endpoints to third-party clients (Android, native, etc.) that don't use KSP-generated Kilua RPC proxies.
Annotate RPC service methods with @RpcBindingRoute to produce human-readable, order-independent route paths instead of counter-based defaults:
@RpcService
interface ITaskService {
@RpcBindingRoute("ITaskService.apiList")
suspend fun apiList(apiList: ApiList<TaskFilter>): ListState<Task>
@RpcBindingRoute("ITaskService.apiItem")
suspend fun apiItem(iApiItem: IApiItem<Task, String, TaskFilter>): ItemState<Task>
}This produces routes like /rpc/ITaskService.apiList instead of /rpc/routeTaskServiceManager0.
RouteContract reads actual routes from Kilua RPC's registry and serves them at /apiContract:
// Main.kt (jvmMain)
val contract = RouteContract(version = "3.1.2")
contract.register(TaskServiceManager, "ITaskService")
routing {
apiContractEndpoint(contract)
}Note: The
/apiContractendpoint is optional when using a shared contract library with@RpcBindingRoutenamed routes. Since routes follow the"/rpc/InterfaceName.methodName"pattern, clients that share the contract library can construct routes at compile time without runtime discovery.
Third-party clients fetch the contract at startup to discover available services:
{
"version": "3.1.2",
"protocol": {
"format": "json-rpc-2.0",
"contentType": "application/json",
"paramEncoding": "each parameter is individually JSON-serialized into a string element of the params array",
"resultEncoding": "the result field contains a JSON-serialized string that must be deserialized a second time"
},
"services": [
{
"service": "ITaskService",
"methods": {
"apiList": { "route": "/rpc/ITaskService.apiList", "method": "POST" },
"apiItem": { "route": "/rpc/ITaskService.apiItem", "method": "POST" }
}
}
]
}For compile-time type safety between server and client, split your models and service contract into a shared library module:
// showcase-lib (shared, no server/frontend dependencies)
interface ITaskServiceContract {
suspend fun apiList(apiList: ApiList<TaskFilter>): ListState<Task>
suspend fun apiItem(iApiItem: IApiItem<Task, String, TaskFilter>): ItemState<Task>
}
// showcase-app (server) — extends the contract with @RpcService
@RpcService
interface ITaskService : ITaskServiceContract { ... }
// Android client — implements the contract with HTTP calls
class ITaskService : ITaskServiceContract {
override suspend fun apiList(apiList: ApiList<TaskFilter>): ListState<Task> =
call("apiList", apiList)
}See samples/fullstack/showcase/ for a complete working example with showcase-lib and showcase-app.
A standalone Android client that consumes the showcase API contract is available at showcase-android. It demonstrates both approaches: runtime route discovery via /apiContract, and compile-time route construction using the shared showcase-lib contract with @RpcBindingRoute named routes.
./gradlew build # Build all modules
./gradlew :core:build # Build only the core module
./gradlew :fullstack:build # Build only the fullstack module
./gradlew :mongodb:build # Build only the mongodb module
./gradlew :sql:build # Build only the sql module
./gradlew :media:build # Build only the media module
./gradlew :ssr:build # Build only the ssr module
./gradlew publishToMavenLocal -PSNAPSHOT # Publish SNAPSHOT to local Maven (~/.m2/)To publish a SNAPSHOT version to your local Maven repository for development and testing:
./gradlew publishToMavenLocal -PSNAPSHOT # Publishes as 3.1.2-SNAPSHOT to ~/.m2/
./gradlew :core:publishToMavenLocal -PSNAPSHOT # Single module onlyThe -PSNAPSHOT flag automatically appends -SNAPSHOT to the version defined in libs.versions.toml — no manual version editing required. In your consuming project, add mavenLocal() and reference the snapshot:
repositories {
mavenLocal()
}
dependencies {
implementation("com.fonrouge.fsLib:fullstack:3.1.2-SNAPSHOT")
}Tip: Gradle caches SNAPSHOT dependencies. If you republish the same snapshot version, use
--refresh-dependenciesin the consuming project to pick up the latest artifacts.
Safety: Running
publishToMavenLocalwithout-PSNAPSHOTis blocked by default. Publishing a release version (e.g.,3.1.2) to~/.m2/would silently shadow the official Maven Central artifact for every project on the machine. If you need to override this check, use-PFORCE_LOCAL.
# Fullstack samples (KVision + Ktor)
./gradlew :samples:fullstack:rpc-demo:run # RPC demo
./gradlew :samples:fullstack:greeting:run # Simple greeting
./gradlew :samples:fullstack:contacts:run # Contacts grid
./gradlew :samples:fullstack:showcase:showcase-app:run # Showcase (InMemoryRepository + API contract)
# SSR samples (Ktor HTML builder)
./gradlew :samples:ssr:basic:run
./gradlew :samples:ssr:catalog:run
./gradlew :samples:ssr:advanced:runFSLib/
core/ # :core module (formerly :base)
src/
commonMain/ # BaseDoc, ID types, annotations, serializers, state, API
jvmMain/ # BSON serializers, JVM utilities
jsMain/ # Browser utilities, JS serializers
fullstack/ # :fullstack module (formerly :fullStack)
src/
commonMain/ # Shared RPC interfaces
jvmMain/ # IRepository, IRolePermissionProvider, PermissionRegistry
jsMain/ # Views, config, Tabulator, layout helpers
mongodb/ # :mongodb module (JVM-only)
src/main/kotlin/ # Coll, aggregation pipelines, BSON helpers
sql/ # :sql module (JVM-only)
src/main/kotlin/ # SqlRepository, SqlDatabase
memorydb/ # :memorydb module (JVM-only)
src/main/kotlin/ # InMemoryRepository
media/ # :media module (formerly :utils)
src/
commonMain/ # DataMedia, ChangeLog interfaces
jvmMain/ # DataMedia MongoDB collection
jsMain/ # DataMedia and ChangeLog views
ssr/ # :ssr module
src/main/kotlin/ # Server-side rendering with Ktor HTML builder
buildSrc/ # Gradle convention plugins
src/main/kotlin/
fslib-publishing.gradle.kts # Maven Central publishing
samples/ # Sample applications
fullstack/
rpc-demo/ # Full-stack KVision + Ktor sample
greeting/ # Simple greeting sample
contacts/ # Contacts sample
showcase/
showcase-lib/ # Shared models + contract (publishable)
showcase-app/ # Full-stack app with API contract endpoint
ssr/
basic/ # Basic SSR sample
catalog/ # Catalog SSR sample
advanced/ # Advanced SSR sample
CLAUDE.md # AI assistant instructions
HELP-DOCS-GUIDE.md # Help documentation guide
:mongodb module):sql module — MSSQL via jTDS or JDBC driver)./gradlew build to verifySee the project repository for license information.
Kotlin Multiplatform full-stack CRUD library for MongoDB and SQL backends with KVision frontend.
FSLib provides a backend-agnostic repository pattern, declarative view configuration, Tabulator-based data grids, role-based access control, change logging, and shared data models across JVM/JS targets. It eliminates repetitive CRUD boilerplate so you can focus on business logic.
IRepository interface with cross-engine dependency checking.ConfigViewList / ConfigViewItem. The framework handles routing, pagination, forms, and CRUD operations.simpleContainer() factories for data entities, simpleCommon() for non-data views (landing pages, dashboards), StandardCrudService for zero-boilerplate service delegation, and registerEntityViews() for declarative view wiring.TabulatorViewList.onQueryCreate, onBeforeUpdateAction, onAfterDeleteAction, onValidate, and many more hooks on the repository for validation, transformation, and side effects.IRolePermissionProvider.DataMedia support (via the :media module) for managing file uploads with thumbnails and metadata.OId (MongoDB ObjectId), IntId, LongId, StringId — all with custom serializers.Coll, and subtype-specific repositories. Shared lookups, hooks, and indexes are defined once in the abstract base.:memorydb module provides an InMemoryRepository for samples, tests, and prototyping without any database engine.@RpcBindingRoute annotation for human-readable route paths (/rpc/ITaskService.apiList). The RouteContract class exposes a /apiContract endpoint for third-party client (Android, etc.) route discovery.:ssr module provides SSR support using Ktor HTML builder.your-app ──> fullstack ──> core
mongodb ──> fullstack, core
sql ──> fullstack, core
memorydb ──> fullstack, core
media ──> fullstack, core, mongodb
ssr ──> fullstack, core, mongodb
| Module | Purpose |
|---|---|
:core |
Platform-independent foundation: BaseDoc<ID>, ID types, annotations, serializers, state management, user/role models, API framework, date/math utilities. |
:fullstack |
Core library. jvmMain: IRepository interface, IRolePermissionProvider, PermissionRegistry, permissions, change logging, RouteContract for API contract discovery, Ktor server stack. jsMain: View system, configuration, Tabulator wrappers, layout helpers, ViewRegistry. commonMain: Shared RPC interfaces via Kilua RPC. |
:mongodb |
MongoDB engine (JVM-only). Coll implementation with aggregation pipelines, lookups, filtering, change logging, and role-based access via KMongo coroutine driver. |
:sql |
SQL engine (JVM-only). SqlRepository implementation using Exposed for relational database access with type-aware filtering and identifier quoting. |
:memorydb |
In-memory database engine (JVM-only). InMemoryRepository using ConcurrentHashMap for storage. Designed for samples, tests, and prototyping — no database engine required. |
:media |
Extensions: DataMedia (file attachments) and ChangeLog views built on top of :fullstack. |
:ssr |
Server-side rendering with Ktor HTML builder. |
| Component | Technology | Version |
|---|---|---|
| Language | Kotlin (Multiplatform) | 2.3.x |
| Backend | Ktor (Netty) | 3.4.x |
| MongoDB | KMongo (coroutine) | 5.5.x |
| SQL | Exposed | 0.61.x |
| Frontend | KVision | 9.4.x |
| RPC | Kilua RPC | 0.0.42 |
| Serialization | kotlinx-serialization | 1.10.x |
| JVM | Toolchain 21 |
FSLib is available on Maven Central.
Add the dependency to your module's build.gradle.kts:
// Version catalog (gradle/libs.versions.toml)
[versions]
fslib = "3.1.2"
[libraries]
fslib-core = { module = "com.fonrouge.fslib:core", version.ref = "fslib" }
fslib-fullstack = { module = "com.fonrouge.fslib:fullstack", version.ref = "fslib" }
fslib-mongodb = { module = "com.fonrouge.fslib:mongodb", version.ref = "fslib" }
fslib-sql = { module = "com.fonrouge.fslib:sql", version.ref = "fslib" }
fslib-memorydb = { module = "com.fonrouge.fslib:memorydb", version.ref = "fslib" }
fslib-media = { module = "com.fonrouge.fslib:media", version.ref = "fslib" }
fslib-ssr = { module = "com.fonrouge.fslib:ssr", version.ref = "fslib" }// build.gradle.kts — In-memory (prototyping/samples, no database required)
kotlin {
sourceSets {
commonMain {
dependencies {
api("com.fonrouge.fslib:fullstack:3.1.2")
}
}
jvmMain {
dependencies {
implementation("com.fonrouge.fslib:memorydb:3.1.2")
}
}
}
}// build.gradle.kts — MongoDB application
kotlin {
sourceSets {
commonMain {
dependencies {
api("com.fonrouge.fslib:fullstack:3.1.2")
}
}
jvmMain {
dependencies {
implementation("com.fonrouge.fslib:mongodb:3.1.2")
}
}
}
}// build.gradle.kts — SQL application
kotlin {
sourceSets {
commonMain {
dependencies {
api("com.fonrouge.fslib:fullstack:3.1.2")
}
}
jvmMain {
dependencies {
implementation("com.fonrouge.fslib:sql:3.1.2")
}
}
}
}// build.gradle.kts — Hybrid (MongoDB + SQL)
kotlin {
sourceSets {
commonMain {
dependencies {
api("com.fonrouge.fslib:fullstack:3.1.2")
}
}
jvmMain {
dependencies {
implementation("com.fonrouge.fslib:mongodb:3.1.2")
implementation("com.fonrouge.fslib:sql:3.1.2")
}
}
}
}./gradlew publishToMavenLocalThis publishes :core, :fullstack, :mongodb, :sql, :memorydb, :media, and :ssr to your local Maven repository (~/.m2/repository).
@Serializable
@Collection("customers")
data class Customer(
override val _id: OId<Customer> = OId(),
val name: String = "",
val email: String = "",
val active: Boolean = true,
) : BaseDoc<OId<Customer>>object CommonCustomer : ICommonContainer<Customer, OId<Customer>, CustomerFilter>(
itemKClass = Customer::class,
filterKClass = CustomerFilter::class,
labelItem = "Customer",
labelList = "Customers",
labelId = { it?.name ?: "" },
)// Or use the simpleContainer factory (when using ApiFilter):
val CommonCustomer = simpleContainer<Customer, OId<Customer>>(
labelItem = "Customer",
labelList = "Customers",
labelId = { it?.name ?: "" },
)@KiluaRpcServiceName("ICustomerService")
interface ICustomerService {
suspend fun apiItem(iApiItem: IApiItem<Customer, OId<Customer>, CustomerFilter>): ItemState<Customer>
suspend fun apiList(apiList: ApiList<CustomerFilter>): ListState<Customer>
}class CustomerColl : Coll<Customer, OId<Customer>, CustomerFilter, OId<User>>(
commonContainer = CommonCustomer,
mongoDatabase = MongoDb.database,
) {
override fun findItemFilter(apiFilter: CustomerFilter): Bson? {
// Custom filtering logic
return apiFilter.nameSearch?.let {
Customer::name regex Regex(it, RegexOption.IGNORE_CASE)
}
}
}class CustomerSqlRepo : SqlRepository<Customer, OId<Customer>, CustomerFilter, OId<User>>(
commonContainer = CommonCustomer,
sqlDatabase = mySqlDatabase,
) {
override fun buildWhereFromApiFilter(
apiFilter: CustomerFilter,
whereClauses: MutableList<String>,
whereArgs: MutableList<Pair<IColumnType<*>, Any?>>,
) {
apiFilter.nameSearch?.let {
whereClauses += "name LIKE ?"
whereArgs += VarCharColumnType() to "%$it%"
}
}
}// List view configuration (in ViewListCustomer companion)
companion object {
val configViewList = configViewList(
viewKClass = ViewListCustomer::class,
commonContainer = CommonCustomer,
apiListFun = ICustomerService::apiList,
)
}
// Item view configuration (in ViewItemCustomer companion)
companion object {
val configViewItem = configViewItem(
viewKClass = ViewItemCustomer::class,
commonContainer = CommonCustomer,
apiItemFun = ICustomerService::apiItem,
)
}
// Register views in App.start() using the DSL:
val reg = registerEntityViews(getServiceManager<ICustomerService>()) {
list(ViewListCustomer.configViewList, isDefault = true)
item(ViewItemCustomer.configViewItem)
}
KVWebManager.initialize { defaultView = reg.defaultView }class ViewListCustomer : ViewList<Customer, OId<Customer>, CustomerFilter, Unit>() {
override val configView = ConfigViewListCustomer
override fun Container.displayPage() {
fsTabulator(viewList = this@ViewListCustomer) {
addColumn("Name") { it.name }
addColumn("Email") { it.email }
addColumn("Active") { if (it.active) "Yes" else "No" }
}
}
}
class ViewItemCustomer : ViewItem<Customer, OId<Customer>, CustomerFilter>() {
override val configView = ConfigViewItemCustomer
override fun Container.displayPage() {
formPanel = ViewFormPanel.xcreate(viewItem = this@ViewItemCustomer) {
formRow {
text(label = "Name", value = Customer::name)
text(label = "Email", value = Customer::email)
}
}
}
}The IRepository interface provides hooks at every stage of CRUD operations:
Query Phase (validation) Action Phase (mutation)
───────────────────── ──────────────────────
onQueryCreate onBeforeCreateAction → DB INSERT → onAfterCreateAction
onQueryRead
onQueryUpdate onBeforeUpdateAction → DB UPDATE → onAfterUpdateAction
onQueryDelete onBeforeDeleteAction → DB DELETE → onAfterDeleteAction
onQueryCreateItem onBeforeUpsertAction (shared create/update)
onQueryUpsert (shared) onAfterUpsertAction (shared create/update)
onValidate (content validation)
Override any hook in your repository class:
class CustomerColl : Coll<...>(...) {
override suspend fun onValidate(apiItem: ApiItem<...>, item: Customer): SimpleState {
if (item.email.isBlank()) return simpleErrorState("Email is required")
return SimpleState(true)
}
override suspend fun onBeforeCreateAction(apiItem: ApiItem.Action.Create<...>): ItemState<Customer> {
// Transform item before insert
return ItemState(item = apiItem.item.copy(name = apiItem.item.name.trim()))
}
override suspend fun onAfterCreateAction(apiItem: ApiItem.Action.Create<...>, itemState: ItemState<Customer>) {
// Side effects after insert (send email, update cache, etc.)
}
}Located in com.fonrouge.base.annotations:
| Annotation | Target | Purpose |
|---|---|---|
@Collection(name) |
Class | Maps class to MongoDB collection or SQL table name |
@Computed |
Property | Marks a body property as intentionally non-persisted (see Constructor-Only Persistence) |
@SqlField(name, compound) |
Property | Maps property to a specific SQL column name or marks it as a compound (nested) field |
@SqlIgnoreField |
Property | Excludes property from SQL INSERT/UPDATE statements |
@SqlOneToOne |
Property | Marks a one-to-one relationship for SQL mapping |
@PreLookupField |
Property | Indicates a pre-lookup field for initial filtering |
FSLib enforces a convention: only primary constructor parameters of BaseDoc subclasses are persisted to the database. Properties declared in the class body are automatically stripped before writes. This is handled by ConstructorCopier, a shared utility used by all repository engines (MongoDB, SQL, InMemory).
@Serializable
data class Product(
override val _id: String, // persisted (constructor parameter)
val name: String = "", // persisted
val price: Double = 0.0, // persisted
) : BaseDoc<String> {
@Computed
val displayPrice: String // NOT persisted (body property)
get() = "$$price"
}Use the @Computed annotation on body properties to make the non-persisted intent explicit and self-documenting.
FSLib includes a built-in RBAC system:
IAppRole — Defines available roles (per class, per CRUD task)IRoleInUser — Assigns roles to individual users (Allow / Deny / Default)IGroupOfUser — Groups users togetherIRoleInGroup — Assigns roles to groupsIUserGroup — Links users to groups with inherited rolesPermissions are checked automatically on every CRUD operation via getCrudPermission(). Roles are auto-created for new repository classes on first access.
The permission system is decoupled from the database engine through IRolePermissionProvider and PermissionRegistry. The MongoDB module registers its provider automatically; SQL repositories consume it without importing MongoDB types.
Enable audit trails by providing a changeLogCollFun on your repository:
class CustomerColl : Coll<...>(...) {
override val changeLogCollFun = { ChangeLogColl() }
}Every create, update, and delete operation automatically records:
View change logs with the IViewListChangeLog interface from the :media module.
The :media module provides file attachment support:
// Define your DataMedia model implementing IDataMedia<User, OId<User>>
// Use IDataMediaColl for the MongoDB collection
// Use IViewListDataMedia for the frontend view with upload, thumbnail preview, and downloadFeatures: file upload with type filtering, thumbnail generation, ordering, metadata tracking (size, content type, user, date).
FSLib supports module-scoped contextual help. See HELP-DOCS-GUIDE.md for the complete guide.
Directory structure:
help-docs/
ViewListCustomer/
tutorial.html # Step-by-step guide
context.html # Quick reference
ViewItemCustomer/
tutorial.html
context.html
Help buttons appear automatically when documentation files exist for a view.
FSLib includes a system for exposing RPC endpoints to third-party clients (Android, native, etc.) that don't use KSP-generated Kilua RPC proxies.
Annotate RPC service methods with @RpcBindingRoute to produce human-readable, order-independent route paths instead of counter-based defaults:
@RpcService
interface ITaskService {
@RpcBindingRoute("ITaskService.apiList")
suspend fun apiList(apiList: ApiList<TaskFilter>): ListState<Task>
@RpcBindingRoute("ITaskService.apiItem")
suspend fun apiItem(iApiItem: IApiItem<Task, String, TaskFilter>): ItemState<Task>
}This produces routes like /rpc/ITaskService.apiList instead of /rpc/routeTaskServiceManager0.
RouteContract reads actual routes from Kilua RPC's registry and serves them at /apiContract:
// Main.kt (jvmMain)
val contract = RouteContract(version = "3.1.2")
contract.register(TaskServiceManager, "ITaskService")
routing {
apiContractEndpoint(contract)
}Note: The
/apiContractendpoint is optional when using a shared contract library with@RpcBindingRoutenamed routes. Since routes follow the"/rpc/InterfaceName.methodName"pattern, clients that share the contract library can construct routes at compile time without runtime discovery.
Third-party clients fetch the contract at startup to discover available services:
{
"version": "3.1.2",
"protocol": {
"format": "json-rpc-2.0",
"contentType": "application/json",
"paramEncoding": "each parameter is individually JSON-serialized into a string element of the params array",
"resultEncoding": "the result field contains a JSON-serialized string that must be deserialized a second time"
},
"services": [
{
"service": "ITaskService",
"methods": {
"apiList": { "route": "/rpc/ITaskService.apiList", "method": "POST" },
"apiItem": { "route": "/rpc/ITaskService.apiItem", "method": "POST" }
}
}
]
}For compile-time type safety between server and client, split your models and service contract into a shared library module:
// showcase-lib (shared, no server/frontend dependencies)
interface ITaskServiceContract {
suspend fun apiList(apiList: ApiList<TaskFilter>): ListState<Task>
suspend fun apiItem(iApiItem: IApiItem<Task, String, TaskFilter>): ItemState<Task>
}
// showcase-app (server) — extends the contract with @RpcService
@RpcService
interface ITaskService : ITaskServiceContract { ... }
// Android client — implements the contract with HTTP calls
class ITaskService : ITaskServiceContract {
override suspend fun apiList(apiList: ApiList<TaskFilter>): ListState<Task> =
call("apiList", apiList)
}See samples/fullstack/showcase/ for a complete working example with showcase-lib and showcase-app.
A standalone Android client that consumes the showcase API contract is available at showcase-android. It demonstrates both approaches: runtime route discovery via /apiContract, and compile-time route construction using the shared showcase-lib contract with @RpcBindingRoute named routes.
./gradlew build # Build all modules
./gradlew :core:build # Build only the core module
./gradlew :fullstack:build # Build only the fullstack module
./gradlew :mongodb:build # Build only the mongodb module
./gradlew :sql:build # Build only the sql module
./gradlew :media:build # Build only the media module
./gradlew :ssr:build # Build only the ssr module
./gradlew publishToMavenLocal -PSNAPSHOT # Publish SNAPSHOT to local Maven (~/.m2/)To publish a SNAPSHOT version to your local Maven repository for development and testing:
./gradlew publishToMavenLocal -PSNAPSHOT # Publishes as 3.1.2-SNAPSHOT to ~/.m2/
./gradlew :core:publishToMavenLocal -PSNAPSHOT # Single module onlyThe -PSNAPSHOT flag automatically appends -SNAPSHOT to the version defined in libs.versions.toml — no manual version editing required. In your consuming project, add mavenLocal() and reference the snapshot:
repositories {
mavenLocal()
}
dependencies {
implementation("com.fonrouge.fsLib:fullstack:3.1.2-SNAPSHOT")
}Tip: Gradle caches SNAPSHOT dependencies. If you republish the same snapshot version, use
--refresh-dependenciesin the consuming project to pick up the latest artifacts.
Safety: Running
publishToMavenLocalwithout-PSNAPSHOTis blocked by default. Publishing a release version (e.g.,3.1.2) to~/.m2/would silently shadow the official Maven Central artifact for every project on the machine. If you need to override this check, use-PFORCE_LOCAL.
# Fullstack samples (KVision + Ktor)
./gradlew :samples:fullstack:rpc-demo:run # RPC demo
./gradlew :samples:fullstack:greeting:run # Simple greeting
./gradlew :samples:fullstack:contacts:run # Contacts grid
./gradlew :samples:fullstack:showcase:showcase-app:run # Showcase (InMemoryRepository + API contract)
# SSR samples (Ktor HTML builder)
./gradlew :samples:ssr:basic:run
./gradlew :samples:ssr:catalog:run
./gradlew :samples:ssr:advanced:runFSLib/
core/ # :core module (formerly :base)
src/
commonMain/ # BaseDoc, ID types, annotations, serializers, state, API
jvmMain/ # BSON serializers, JVM utilities
jsMain/ # Browser utilities, JS serializers
fullstack/ # :fullstack module (formerly :fullStack)
src/
commonMain/ # Shared RPC interfaces
jvmMain/ # IRepository, IRolePermissionProvider, PermissionRegistry
jsMain/ # Views, config, Tabulator, layout helpers
mongodb/ # :mongodb module (JVM-only)
src/main/kotlin/ # Coll, aggregation pipelines, BSON helpers
sql/ # :sql module (JVM-only)
src/main/kotlin/ # SqlRepository, SqlDatabase
memorydb/ # :memorydb module (JVM-only)
src/main/kotlin/ # InMemoryRepository
media/ # :media module (formerly :utils)
src/
commonMain/ # DataMedia, ChangeLog interfaces
jvmMain/ # DataMedia MongoDB collection
jsMain/ # DataMedia and ChangeLog views
ssr/ # :ssr module
src/main/kotlin/ # Server-side rendering with Ktor HTML builder
buildSrc/ # Gradle convention plugins
src/main/kotlin/
fslib-publishing.gradle.kts # Maven Central publishing
samples/ # Sample applications
fullstack/
rpc-demo/ # Full-stack KVision + Ktor sample
greeting/ # Simple greeting sample
contacts/ # Contacts sample
showcase/
showcase-lib/ # Shared models + contract (publishable)
showcase-app/ # Full-stack app with API contract endpoint
ssr/
basic/ # Basic SSR sample
catalog/ # Catalog SSR sample
advanced/ # Advanced SSR sample
CLAUDE.md # AI assistant instructions
HELP-DOCS-GUIDE.md # Help documentation guide
:mongodb module):sql module — MSSQL via jTDS or JDBC driver)./gradlew build to verifySee the project repository for license information.