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 |
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 |
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.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
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