Files
summercms/STACK.md

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 + Netty Tapir gives type-safe endpoint definitions with auto-generated OpenAPI docs. Netty-based server for production use
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 Liquibase Supports rollbacks in free version (Flyway does not). Rollbacks are essential for dev workflow — drop last migration and fix, like in WinterCMS

Supporting Libraries

Concern Library Notes
JSON jsoniter-scala Fastest JSON lib on JVM. May need a custom wrapper (~75% done)
Config Jig (HOCON) Supports reading & writing including comments. Better discoverability for LLMs. Maintained in-house — we can add features as needed
Logging Scribe + Scribe SLF4J binding Excellent performance, configured in code (not XML). SLF4J binding for Java library compatibility
Caching Caffeine (in-memory) + Jedis/Lettuce (Redis) Caffeine is the fastest JVM cache; Redis for distributed
Hashing jBCrypt + JDK crypto Password hashing + general encryption. Bouncy Castle via JNI if performance-critical
Email summer-postcard with driver trait Clean interface + multiple drivers: 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. Enables 1:1 API bindings + drift detection
Template Engine Pebble Twig-like syntax (preserves WinterCMS familiarity). Needs investigation: file-based loading + hot-reload in dev
CLI case-app Auto-generated help, parsed case classes, simple and LLM-friendly
Testing MUnit + testcontainers-scala + Tapir test utils MUnit for unit tests, 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 via proven Java JWT library
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 Persistent queue for offloading heavy work from the HTTP hot path. Driver-based: Postgres table, ActiveMQ, Redis, etc. Guarantees that enqueued jobs are not lost and are eventually executed. Virtual threads for workers
Mail summer-postcard Clean email interface with driver trait + template support. Drivers: SMTP, AWS SES, etc.
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
Plugins summer-party Plugin system: versioned plugin interfaces, resolution system, ServiceLoader-based runtime discovery. Private plugin registry with version compatibility extracted from published artifacts

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. SLF4J binding provided for Java library compatibility. 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 Two aspects: DB-level query pagination (in summer-lagoon) and template/API pagination helpers (in summer-surf / summer-sunset). Not enough for a standalone module. Split across summer-lagoon + summer-surf

Plugin & Theme System

Plugins (summer-party)

Plugins are published JVM artifacts (JARs), discovered at runtime via ServiceLoader.

  • summer-party defines versioned plugin interfaces — a common contract all plugins implement
  • Plugins discovered at boot via ServiceLoader (runtime-based, not classpath scanning)
  • Version compatibility: registry extracts interface versions from published artifacts to know which plugins work with which CMS version
  • Plugin descriptor declares: models, controllers, components, console commands, event listeners, navigation items
  • 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
    summer-party/              # Plugin system + registry
  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: Evaluate proven Java JWT libraries for 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: Static verification that services connect correctly post-deployment. Integrate into dev loop and CI
  • Plugin registry: Design private plugin registry — extract interface versions from published artifacts to determine CMS version compatibility