A Kotlin application platform for building extension-driven web and desktop products. The platform provides configuration, database migrations, authentication, session management, routing, and template rendering — extensions provide the product UI and business logic.
If you are building an extension, start with the Extension Author Guide and the copyable starter-extension-app scaffold. If you are upgrading an existing extension, see MIGRATION.md for the 1.6.x -> 3.6.4 migration path.
- Extension Composition Model: Extensions control what platform UI to include via
PlatformMode(FullPlatform,ExtensionHost,Headless). Route ownership, conflict detection, and startup diagnostics come built-in. - Multi-Module Architecture:
outerstellar-i18n,platform-core,platform-extension-api,platform-persistence-jdbi,platform-sync-client,platform-security,platform-web,platform-desktop, andplatform-seederfor clear separation of concerns. - Transactional Outbox Pattern: Ensures atomicity and reliability for background tasks and data synchronization.
- Observability: Integrated with OpenTelemetry for distributed tracing and Micrometer for real-time metrics.
- Type-Safe Configuration: Uses Hoplite for multi-environment configuration from YAML + env vars.
- Contract-First API: Synchronization API defined using http4k-contract with automatic OpenAPI documentation.
- Caching Layer: Caffeine-based caching with integrated metrics.
- Swing Desktop MVVM: Desktop client uses Model-View-ViewModel with FlatLaf theming.
- Managed Database Migrations: Flyway for schema versioning with extension-isolated migration history tables.
platform-core: Domain models, service interfaces, shared business logic, composition model types.outerstellar-i18n: ResourceBundle-backed runtime translation service.platform-extension-api: Extension SPI and extension-facing DTOs for the composition model.platform-persistence-jdbi: Database implementation using JDBI and Flyway migrations.platform-sync-client: Shared DTOs and client logic for synchronization between components.platform-security: Authentication models, role-based access control, fine-grained permissions, multi-realm auth, and security filters.platform-web: The main http4k server, JTE templates, route registry, and web-specific infrastructure.platform-jte-extensions: Shared JTE template registry and rendering helpers for extensions.platform-desktop: A Swing-based desktop application implementing the MVVM pattern.platform-seeder: Database seeding utility.platform-desktop-javafx: JavaFX desktop module (scaffolded, not production-ready).platform-testkit: Shared test fixtures and helpers (JDBI test harness, mock builders).outerstellar-i18n-validator/outerstellar-i18n-validator-maven-plugin: Build-time i18n key validation and the wrapping Maven plugin.outerstellar-platform-extension-archetype: Maven archetype for scaffolding a new extension host.outerstellar-platform-extension-parent: Parent POM extensions extend to inherit plugin/dependency configuration.
- JDK 21
- Maven 3.9+
- Node.js (for Tailwind CSS)
mvn clean installUse these root-level profiles to separate concerns:
coverage: enables coverage collection/report generation for verification runs.tests-headless: runs tests with desktop UI in headless mode.tests-headful: runs tests with desktop UI in non-headless mode.runtime-dev: optimized runtime profile for local development launches.runtime-prod: optimized runtime profile for production-like launches.
Examples:
# Coverage + tests
mvn -Pcoverage verify
# Desktop tests in headless mode (CI friendly)
mvn -pl platform-desktop -Ptests-headless test# Desktop tests with actual UI
mvn -pl platform-desktop -Ptests-headful test# Run web in dev runtime mode
mvn -pl platform-web -Pruntime-dev compile exec:java./start-web.ps1./start-swing.ps1The project includes a comprehensive test suite:
- Unit Tests: Business logic verification with MockK.
- Integration Tests: Database and service-level integration.
- Architecture Tests: Enforcing modular boundaries with ArchUnit.
- End-to-End Tests: Full system verification through the web layer.
To run all tests:
mvn testReleases are now manual and version-confirmed to avoid publishing the wrong version.
- Merge the release commit to
mainwith the exact target version inpom.xmland a matchingCHANGELOG.mdsection like## [1.6.4]. - Run Release and Publish from
main, enterrelease_version, then type the exact same version again inconfirm_release_version. - After that succeeds, run Publish to Maven Central from
mainwith the same two inputs.
Both workflows now fail unless they run from main, the entered version exactly matches the root Maven version, the version is not a -SNAPSHOT, the changelog contains that exact release heading, and CI has already succeeded on that exact commit. Maven Central also refuses to run until the matching GitHub release tag already exists.
The web application uses http4k with JTE templates and HTMX for interactivity. Routes are organized by ownership through the RouteRegistry:
- Kernel routes: Always present — auth, static assets, health, metrics.
- Platform UI routes: Opt-in via
includePlatformPages()— home, contacts, settings, search, notifications, profile, admin, dev-dashboard. - Extension routes: Registered by the extension via
routeRegistrations()in any route group. - API routes: JSON-based synchronization and bearer-token API.
enumclassPlatformMode {
FullPlatform, // Default — all platform UI routes mounted, zero configExtensionHost, // Extension opts into specific platform pages via includePlatformPages()Headless// API-only, no HTML UI at all
}classMyPlatformExtension : PlatformExtension {
overrideval id ="my-app"overrideval mode =PlatformMode.ExtensionHostoverridefuncontribute(context:ExtensionContributionContext) {
context.platformPages.include(PlatformPageSets.SETTINGS, PlatformPageSets.SEARCH)
context.routes.publicUi(myHomeRoute, "Home page", "/")
context.navigation.item("Home", "/", "home-line")
}
}
classMyExtensionContractTest {
@Test
fun`contribution is valid`() {
val diagnostics =ExtensionContract.diagnostics(MyPlatformExtension(), testExtensionHostContext())
assertEquals(listOf("/"), diagnostics.routes.map { it.pathPattern })
}
}
// Start the serverval components = createServerComponents(extension =MyPlatformExtension())At startup, the route registry logs a table showing all routes, their owners, and any conflicts. If two owners claim the same path, the server fails fast with a descriptive error.
- Create a ViewModel: Define a Kotlin
data classimplementingViewModelalongside the domain-specific page factory that owns it. - Create a Template: Add a corresponding
.ktefile inplatform-web/src/main/jte. Wrap your content using thePage<T>wrapper to inherit the global layout:@import io.github.rygel.outerstellar.platform.web.MyPage @import io.github.rygel.outerstellar.platform.web.Page @param model: Page<MyPage> @template.io.github.rygel.outerstellar.platform.web.LayoutRouter(shell = model.shell, content = @` <h1>${model.data.title}</h1> `)
- Update the Factory: Add a
buildMyPagemethod to the appropriate domain factory (e.g.,HomePageFactory,SettingsPageFactory,AdminPageFactory). - Register the Route: Add the route to the appropriate route class (e.g.,
HomeRoutes.kt) and register it in theRouteRegistryinApp.kt. - Access State: Use
request.shellRendererto buildShellViewwith nav links, CSRF token, theme, and user info.
Beyond role-based access (USER/ADMIN), routes can require specific permissions using the wildcard domain:action:instance model:
// Require "report:export" permission to access this routeSecurityRules.hasPermission(Permission("report", "export"), permissionResolver, next)The default RoleBasedPermissionResolver maps roles to permission sets (admins get *:*). For per-user permissions, implement a custom PermissionResolver backed by a database table — the interface is a single method.
Bearer token authentication is resolved through a chain of AuthRealm instances. The default chain tries session tokens first, then API keys. To add a custom auth source:
classLdapRealm(privatevalldapClient:LdapClient) : AuthRealm {
overrideval name ="ldap"overridefunauthenticate(token:String): AuthResult {
val user = ldapClient.validateToken(token) ?:returnAuthResult.SkippedreturnAuthResult.Authenticated(user)
}
}The web application is ready for Ahead-of-Time (AOT) compilation using GraalVM. This produces a standalone native binary with extremely fast startup times and low memory footprint.
To build the native image (requires GraalVM installed and JAVA_HOME set correctly):
mvn package -Pnative -pl platform-web -DskipTestsThe resulting binary will be located in platform-web/target/outerstellar-web.
To prevent common Kotlin type-inference issues and library conflicts, follow these rules:
- Explicit Request Typing: In route handlers (
bindContract), always explicitly type the request parameter. This preventsClassCastExceptionwhere the compiler might confuse aRequestwith aViewModel.// ALWAYS DO THIS: bindContract GET to { request:Request-> renderer.render(pageFactory.buildAuthPage(request.webContext)) }
- Fully Qualified Template Types: JTE and http4k both have a
ContentTypeclass. Always use the fully qualified namegg.jte.ContentType.Htmlwhen configuring the template engine to avoid import ambiguity. - Use the Render Extension: Never manually construct HTML responses. Use
renderer.render(viewModel)which automatically handles content-type headers and UTF-8 encoding. - Contextual Helpers: Prefer
request.webContextover manually creating aWebContextinstance. This ensures you are using the state already extracted by the global filters.
- Discourage OOB Swaps: The use of HTMX Out-of-Band (OOB) swaps is discouraged by default. They should only be implemented if strictly necessary and only after careful consideration.
- Reasons for avoidance:
- Increased Server Complexity: It requires route handlers to wrap multiple unrelated fragments in a single response, bloating template logic.
- Risk of State Desync: Updating elements far away from the trigger can lead to unpredictable UI states across different tabs or sessions.
- Breaks Locality of Behavior: It violates the core HTMX principle by spreading the consequences of an action across disparate parts of the DOM.
- Initialization Logic Duplication: OOB updates only happen on specific actions; ensuring the "Initial Load" logic matches the "Action Result" logic requires repetitive code.
Prefer standard
hx-targetswaps to keep the flow predictable and maintainable.
- Reasons for avoidance:
The desktop application is built with Swing following the MVVM pattern and uses FlatLaf for a modern, themed look and feel.
Theme Consistency is Mandatory: Every Swing UI surface (main windows, dialogs, popups, and transient overlays) must use centralized theming. Do not ship unthemed or partially themed screens.
FlatLaf as Source of Truth: Use FlatLaf/UI defaults (
UIManagerkeys and shared theme tokens) for colors, typography, borders, and component states. Avoid hardcoded colors, fonts, and ad-hoc styling in individual windows.MigLayout as Standard Layout System: Use MigLayout for all new/updated Swing windows and dialogs to keep spacing, alignment, and responsiveness consistent across the app.
Themed Dialog Rule: Authentication and settings dialogs must apply the same background, foreground, and component style rules as primary windows so there is no visual drift.
Localization Rule: All user-visible Swing text (window titles, labels, buttons, menu items, and dialog messages) must come from i18n keys. Avoid hardcoded UI strings in production code.
Runtime Language Switch Rule: Changing language at runtime must refresh currently mounted UI text in-place, not only newly opened windows/dialogs.
Language Regression Test Rule: Any change touching Swing text or settings flow must include/update an automated language-switch test that verifies key UI labels and menus update correctly.
Design Review Check: UI work is incomplete until all affected screens are verified for theme parity (light/dark if supported), readable contrast, and consistent spacing.
No WebSockets for Desktop: The desktop application explicitly does not use WebSockets for synchronization.
- Reasons: To keep the desktop client's architecture lean and focused on its primary role as a standalone synchronization tool. Standard HTTP-based sync provides a reliable, firewall-friendly connection model that is easier to debug and maintain for a desktop environment. Real-time updates are prioritized for the Web UI, while the desktop app maintains a robust manual or background polling-based sync model.