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 |
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 |
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-partydefines 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.yamlfor model forms - Backend returns form schema as JSON, Vue frontend renders it
- Field types: text, textarea, dropdown, relation, repeater, etc.
- Plugin defines
- 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