Files
summercms/STACK.md
2026-02-06 11:59:58 +01:00

12 KiB

SummerCMS Technology Stack

Philosophy

Direct-style Scala 3 on JDK 21 virtual threads. No monadic effect systems (no ZIO, no Cats Effect). Virtual threads make blocking IO cheap, so you write normal imperative-looking code that scales. This is the simplest possible modern Scala stack.


Core Stack

Layer Technology Why
Language Scala 3.3+ LTS VirtusLab maintains the compiler. Scala 3 has cleaner syntax, given/using for DI, extension methods, enums, union types
Runtime JDK 21+ with Project Loom Virtual threads = millions of lightweight threads, no async/callback hell
HTTP Server Tapir + JDK HttpServer Tapir gives type-safe endpoint definitions with auto-generated OpenAPI docs. JDK HttpServer with Executors.newVirtualThreadPerTaskExecutor() is the lightest possible server
Concurrency Ox Structured concurrency, Go-like channels, error supervision, retries, rate limiting. Built on virtual threads
Database Magnum Scala 3 native, typesafe SQL interpolator, auto-generated CRUD, no dependencies, works with any JDBC database
Migrations Flyway Industry standard, JVM-native, works with any JDBC database

Supporting Libraries

Concern Library Notes
JSON jsoniter-scala Fastest JSON lib on JVM — author hand-tunes CPU instructions emitted by JVM. Only non-broken Scala JSON lib. May need a custom wrapper (~75% done)
Config Jig (HOCON) Supports reading & writing including comments. Better discoverability for LLMs. Maintained by VL engineer with full control — we can add features as needed
Logging Scribe + Scribe SLF4J binding Excellent performance, configured in code (no XML). SLF4J binding for Java lib compat. Avoid direct SLF4J — its config resolution rules are a nightmare
Caching Caffeine (in-memory) + Jedis/Lettuce (Redis) Caffeine is the fastest JVM cache; Redis for distributed
Hashing jBCrypt + JDK crypto Password hashing + general encryption. JDK is good enough; Bouncy Castle via JNI if performance-critical
Email Custom summer-postcard with driver trait Jakarta Mail is legacy with Java failure modes (random NPEs). Emails are too critical for that. Need clean interface + multiple bindings: SMTP, AWS SES, etc.
HTTP Client sttp Best HTTP client on JVM. Pairs with Tapir (same ecosystem). Handles sessions, cookies, streaming, HTTP auth
Admin Frontend Vue.js 3 + TypeScript SPA admin panel consuming Tapir API
OpenAPI codegen Tapir -> OpenAPI -> openapi-typescript Auto-generated TS types from Tapir endpoint definitions. VL has working template for this. Enables 1:1 API bindings + drift detection
Template Engine Pebble Twig-like syntax (preserves WinterCMS familiarity). Scalate is abandonware — avoid. Needs investigation: file-based loading + hot-reload in dev (file watcher fallback if needed)
CLI case-app By Alex Archambault (original scala-cli author). Auto-generated help, parsed case classes. Scopt has bad assumptions; Decline is too complex for LLMs to use effectively
Testing MUnit + testcontainers-scala + Tapir test utils MUnit only — no ScalaTest (build issues, unreliable cross-platform publishing). testcontainers-scala for integration tests (Postgres etc.)
Build (project) sbt Multi-module build for the subprojects
Build (scripts) scala-cli Quick scripts, prototyping, dev tooling. VirtusLab maintains it

Module Mapping: Laravel Illuminate -> SummerCMS

Each module = separate sbt subproject, publishable independently (like Illuminate packages).

Illuminate Package SummerCMS Module Implementation Approach
Container summer-backpack Scala 3 given/using for compile-time DI + lightweight runtime registry for plugins
Contracts summer-pact Scala traits — all public APIs defined here
Support summer-towel Extension methods, utility types, base classes
Config summer-compass Jig-based HOCON config, supports reading & writing with comments, standardized access across all modules
Events summer-festival Simple event bus with typed events + Ox channels for async
Http summer-surf Tapir endpoint definitions + sttp client, request/response wrappers, routing (Tapir endpoints are type-safe routes)
Database summer-lagoon Magnum repos + immutable case class models + query builder + pagination. Key challenge: Scala immutability vs Eloquent/Doctrine mutation patterns — use copy() + repo.save() for single entities. Relations are the hardest part to model (see open questions)
Auth summer-bouncer JWT tokens (for API) + session cookies (for admin), guards as traits, session management. JWT needs separate investigation — no good Scala-native JWT lib yet, may use Spring's JWT implementation
Validation summer-lifeguard Compile-time via Scala types + runtime rules engine (inspired by fields.yaml)
Cache summer-cooler Caffeine + Redis, driver-based via trait
Queue summer-conga Ox channels + virtual threads for workers, persistent queue via DB or Redis
Mail summer-postcard Clean email interface with driver trait + template support. Drivers: SMTP, AWS SES, etc. No Jakarta Mail dependency
Console summer-bonfire CLI command framework via case-app for scaffolding (summer create:plugin, etc.)
Filesystem summer-sandcastle java.nio.file + pluggable storage providers (S3, etc.) via driver trait
Translation summer-phrasebook i18n with HOCON/JSON locale files
View summer-sunset Template engine for admin panel rendering

Illuminate Packages Not Ported (and why)

Illuminate Package Why not needed Where it lives instead
Pipeline Function composition is native to Scala (f andThen g). Laravel needs a Pipeline class because PHP lacks first-class functions. In Scala this is a one-liner, not a module. Inline wherever needed
Routing Tapir endpoint definitions ARE routes — defining an endpoint and defining a route is the same thing. No separate routing layer needed. Merged into summer-surf (http)
Session For an API-first CMS with JWT, sessions are thin — only needed for admin panel cookies. Not enough to justify a standalone module. Merged into summer-bouncer (auth)
Log Scribe handles logging directly with code-based configuration. No XML config hell, no SLF4J resolution rules. SLF4J binding provided for Java library compatibility. No need for a separate module. Direct Scribe usage
Collections Scala stdlib collections are already excellent — List, Map, Seq, Vector with map, filter, fold, etc. No wrapper needed. Scala stdlib
Pagination A paginator is a case class + a few helper methods. Too small for a standalone module. Merged into summer-lagoon (database)

Plugin & Theme System

Plugins

Each plugin = a directory with a Plugin.scala descriptor (same lifecycle as WinterCMS).

  • Descriptor declares: models, controllers, components, console commands, event listeners, navigation items
  • Plugins discovered at boot via classpath scanning or manifest
  • Plugins can extend other plugins' models via Scala 3 extension methods + event hooks
  • Plugin lifecycle: register() -> boot()

Components

  • Components are defined in plugins and placed in theme templates (like WinterCMS)
  • Each component = a class with onRun(), properties, and a partial template
  • Components handle their own data fetching and expose variables to templates
  • Registered via plugin descriptor, auto-discovered by the theme engine

Themes

  • Theme = directory of templates + assets + config
  • Templates rendered server-side (Pebble) or served as SPA shell
  • Components (from plugins) can be placed in theme templates

Admin Backend

  • Tapir endpoints serve as the admin API
  • YAML/JSON-driven forms — same concept as WinterCMS fields.yaml / columns.yaml:
    • Plugin defines fields.yaml for model forms
    • Backend returns form schema as JSON, Vue frontend renders it
    • Field types: text, textarea, dropdown, relation, repeater, etc.
  • Admin frontend: Full SPA in Vue.js 3 + TypeScript — consumes Tapir-generated typed API
    • TypeScript types auto-generated from Tapir OpenAPI spec
    • Vue components for form builder, list columns, relation managers, etc.

Frontend-Backend Communication

  • Tapir-generated typed API — all communication is via typed JSON endpoints
  • OpenAPI spec auto-generated from Tapir endpoint definitions
  • TypeScript client types generated from OpenAPI for the Vue admin SPA
  • Public-facing sites can use the same API (headless mode) or server-rendered templates
  • Headless mode: admin panel + API controllers serve as a superuser data presentation layer, frontend is fully decoupled

Scaffolding Commands

summer create:plugin vendor.pluginname
summer create:model vendor.pluginname ModelName
summer create:controller vendor.pluginname ControllerName
summer create:component vendor.pluginname ComponentName
summer create:command vendor.pluginname CommandName
summer create:job vendor.pluginname JobName
summer create:migration vendor.pluginname description

Project Structure

summercms/
  build.sbt                    # Multi-project sbt build
  modules/
    summer-backpack/           # DI container + plugin registry
    summer-pact/               # Contracts (traits)
    summer-towel/              # Support utilities
    summer-compass/            # Config
    summer-festival/           # Events
    summer-surf/               # HTTP + routing (Tapir)
    summer-lagoon/             # Database + pagination (Magnum)
    summer-bouncer/            # Auth + sessions
    summer-lifeguard/          # Validation
    summer-cooler/             # Cache
    summer-conga/              # Queue (Ox)
    summer-postcard/           # Mail
    summer-bonfire/            # Console / CLI
    summer-sandcastle/         # Filesystem + storage providers
    summer-phrasebook/         # Translation / i18n
    summer-sunset/             # View / templates
  app/                         # Full CMS application (depends on all modules)
    summer-cms/                # Assembled CMS with admin panel
  plugins/                     # Example/core plugins
    summer-plugin-user/
    summer-plugin-blog/
    summer-plugin-pages/

Open Questions & Research Needed

summer-lagoon ORM design (showstopper)

In PHP/Eloquent, models are mutable and carry their own persistence:

$user = User::findByName("name");
$user->firstName = "Raziel";
$user->save();

In Scala, case classes are immutable. The equivalent pattern:

val user = UserRepo.findByName("name")
UserRepo.save(user.copy(firstName = "Raziel"))

Single-entity CRUD via copy() + repo is straightforward. Relations are the hard part — in Eloquent, relations are dynamic mutable fields on the model. Modeling this idiomatically in Scala with immutability needs deep design work. This is the biggest architectural risk in the project.

Other open items

  • Pebble hot-reload: Can Pebble load templates from filesystem (not classpath) and hot-reload on change? If not, implement file watcher that reloads the template engine on file changes. See Pebble docs
  • JWT library: No good Scala-native JWT solution. Evaluate Spring's JWT implementation or write a thin wrapper. sttp/Tapir handle HTTP auth but not JWT token generation/validation
  • jsoniter-scala wrapper: ~75% complete wrapper exists. Evaluate if it needs finishing or if raw jsoniter-scala is sufficient
  • API drift detection: VL has know-how for static verification that services connect correctly post-deployment (from Yaga/Besom project). Integrate into dev loop and CI