Tech stack
Every load-bearing technology in PrexorCloud, the role it plays, and
the reasoning behind the choice. Versions below are read from the build
files — java/gradle/libs.versions.toml, cli/go.mod,
dashboard/package.json, and the per-process Dockerfiles — not from
memory. Where a version is a stated minimum rather than the pinned
build version, the text says so.
What you’ll learn
- The language and runtime for each process, with the version the build pins
- The libraries that show up in the dependency catalogues and how they’re used
- Operational dependencies: MongoDB, Valkey, Prometheus, cosign, Ratis
- The reasoning for each pick over the obvious alternative
Process matrix
| Process | Language | Build target | Runtime image | Framework / library |
|---|---|---|---|---|
| Controller | Java 25 (--enable-preview) | JDK 25 | eclipse-temurin:25-jdk builder → debian:bookworm-slim | Javalin 7 (HTTP/SSE), grpc-java 1.80 + Netty (gRPC server), Mongo sync driver 5.6, Lettuce 7.5 (Valkey/Redis), Apache Ratis 3.1 (Raft), Micrometer + Prometheus registry, Logback + SLF4J, Jackson |
| Daemon | Java 25 (--enable-preview) | JDK 25 | eclipse-temurin:25-jdk builder → jlink JRE on debian:bookworm-slim | grpc-java (streaming client), ProcessBuilder for instance processes, OSHI (host metrics), Logback + SLF4J |
| Dashboard | TypeScript | Node 22 + browser | node:22-alpine builder → nginx:alpine | Nuxt 4, Vue 3, Pinia, generated OpenAPI SDK, ESLint + Vitest + Playwright |
CLI (prexorctl) | Go 1.24 | static binary | none (single binary) | Cobra (commands), Charmbracelet bubbletea/huh/lipgloss (TUI), goreleaser (cross-build + sign) |
| Server plugins (Paper / Spigot / Folia / Fabric / NeoForge) | Java | host server JVM | host MC server | Paper / Bukkit API, Fabric Loom, NeoForge ModDevGradle |
| Proxy plugins (Velocity / BungeeCord / Geyser) | Java | host proxy JVM | host proxy | Velocity API 3.4, BungeeCord API, Geyser extension API |
Both the controller and the daemon target Java 25 and compile with
--enable-preview (see prexorcloud.java25-preview.gradle.kts). The
daemon’s runtime image is a jlink-trimmed JRE — java.base,
java.management, java.naming, java.logging, and a few more — not a
full JDK, so the deployed footprint is small. The shared API jars
cloud-api and cloud-common target Java 21 (prexorcloud.java21-api)
so plugin-side code running on long-tail server JDKs can consume them.
Languages and runtimes
Java 25 for the control plane
The controller and daemon both run on OpenJDK 25 and opt into preview
features. The build pins the toolchain to language version 25 and adds
--enable-preview to every JavaCompile and Test task. The
control-plane code base uses records, pattern matching, and virtual
threads; the daemon runs a streaming gRPC client and shells out to
instance processes with ProcessBuilder, with no HTTP framework of its
own.
The plugin-facing API (cloud-api, cloud-common) is held back to
Java 21 with options.release.set(21) so a jar built here loads on the
JDKs that real Paper/Velocity hosts run. Mixing a Java 25 control plane
with Java 21 API jars is deliberate, not an oversight.
Go 1.24 for the CLI
prexorctl is a single static Go binary (cli/go.mod, go 1.24). The
command surface is Cobra; the interactive setup wizard and status views
use the Charmbracelet stack — bubbletea, bubbles, huh, and
lipgloss. A single binary with no runtime dependency is the right
shape for a tool an operator copies onto a fresh host before anything
else exists.
TypeScript on Node 22 for the dashboard
The dashboard is Nuxt 4 / Vue 3 with Pinia stores (dashboard/package.json:
nuxt ^4.4, vue ^3.5, pinia ^3.0). It talks to the controller through
an SDK generated from the controller’s OpenAPI spec — pnpm sdk:check
validates docs/openapi.json and regenerates @prexorcloud/api-sdk
before every build. The production image builds on node:22-alpine and
serves the static output from nginx:alpine.
gRPC and protobuf
The daemon-to-controller contract lives in java/cloud-protocol and is
generated by the protobuf-gradle-plugin driving protoc 4.34 plus the
protoc-gen-grpc-java plugin at the same version as grpc-java (1.80).
There is no buf.yaml or buf.gen.yaml in the tree — code generation
runs through Gradle, not buf. The wire layer is grpc-java over
grpc-netty-shaded, with bidirectional streaming for the daemon
connection and ReloadableServerSslContext for hot CA rotation.
The protocol’s compatibility is enforced two ways: a checked-in
contracts/proto-contracts.sha256 guards the generated surface, and the
controller and daemon negotiate a protocol version at handshake.
Additive oneof variants do not bump the protocol version.
To regenerate after editing a .proto:
./gradlew :cloud-protocol:generateProtoPersistence and coordination
MongoDB — durable state
PrexorCloud keeps its platform state in MongoDB through the synchronous
Mongo Java driver (mongodb-driver-sync 5.6). The reference Compose
stack pins mongo:8.0; the documented minimum is MongoDB 6.0, and a
replica set is recommended for HA. The driver is used directly — the
connection pool is tuned through the URI, with no ORM or wrapper.
The document model is the reason for the pick. Platform state is deeply-nested, per-feature variable data: composition plans, module manifests, template layers, workflow intent. A relational schema would mean either a column of JSON (a relational anti-pattern) or a rigid schema that fights every new feature.
Valkey — coordination
Coordination — leases, TTLs, pub/sub fan-out — runs over the Redis
protocol via Lettuce (lettuce-core 7.5), a non-blocking client used
through its synchronous primitives at the call sites; connection
multiplexing keeps that cheap. The reference Compose stack pins
valkey/valkey:8-alpine; the documented minimum is Valkey 7.2 or
Redis 7.
Valkey is the default for the licensing question — Valkey is BSD-3, Redis moved to a source-available license — but the controller speaks the Redis protocol, so an operator already running Redis can point at it unchanged.
Apache Ratis — Raft consensus
The embedded cluster control plane (v1.1) runs on Apache Ratis 3.1
(ratis-server, ratis-grpc, and friends in libs.versions.toml). The
state machine and bootstrap live under
cloud-controller/.../cluster/raft/. Ratis gives a Java-native Raft
implementation with InstallSnapshot and dynamic membership, which is what
a multi-controller deployment needs to elect a leader and replicate the
control log without bolting on an external consensus service.
Signing and supply chain
Release artefacts and module bundles are signed with Sigstore cosign,
keyless. The CLI release config (cli/.goreleaser.yaml) signs the
checksums.txt blob with cosign sign-blob under keyless OIDC, so one
signature covers the whole release; verification is
cosign verify-blob. There are no long-lived signing keys to rotate or
leak.
| Tool | Role |
|---|---|
| cosign | Keyless signing of release artefacts and module bundles; operator-side verification |
| Fulcio | Short-lived signing certificates from OIDC identity |
| Rekor | Transparency log; offline SET enforcement via modules.signing.rekor.policy=REQUIRE_SET |
| Trivy | Vulnerability scan against built images |
| Syft | CycloneDX SBOM per image |
Cosign and Rekor signature verification on the controller side uses
Bouncy Castle (bcpkix/bcprov 1.83), the same library that backs the
mTLS CA operations.
Observability
| Concern | Tool | Notes |
|---|---|---|
| Metrics | Micrometer + Prometheus registry | MetricsCollector registers into a PrometheusMeterRegistry; the controller exposes a standard Prometheus scrape endpoint. Module metrics are hand-rendered exposition to keep module dependencies minimal (ADR 16). |
| Tracing | OpenTelemetry (opt-in) | The controller ships an OTLP/gRPC tracing pipeline (observability/telemetry/Telemetry.java). Off by default — TelemetryConfig.enabled() false means a no-op tracer and no SDK — so spans at instrumentation sites cost nothing. When enabled, spans batch-export over OTLP/gRPC to any compatible collector (Jaeger, Tempo, …) at otlpEndpoint (default http://localhost:4317), with W3C trace-context propagation. Instrumented across auth, scheduler, placement, Raft, Redis, and HTTP. |
| Logs | Logback + SLF4J | HUMAN and JSON formats; RequestIdMiddleware plumbs requestId through MDC. |
Metrics are registered through Micrometer’s PrometheusMeterRegistry,
not the legacy Prometheus simpleclient. Tracing is a real, wired
subsystem (delivered under northstar Track D) that defaults to off — the
right shape for a two-service control plane where most operators only
need Prometheus plus structured logs.
Build and CI
| Tool | Purpose |
|---|---|
| Gradle (Kotlin DSL) | Multi-project Java build. Convention plugins in build-logic/ pin Java 25 (control plane) or 21 (API jars) per module. |
| pnpm | Dashboard, module SDK, and auxiliary Node workspaces. |
| Go modules | CLI build. |
| protobuf-gradle-plugin + protoc | gRPC/protobuf code generation for cloud-protocol. |
| GitHub Actions | Tests, builds, releases. release.yml ships cosign-signed prexorctl binaries; release-images.yml ships cosign-signed multi-arch GHCR images on v*. |
| goreleaser | CLI cross-compile, sign, and publish. |
| Spotless | Format/license enforcement (needs JDK 25). |
| Trivy / Syft | Image vulnerability scan and SBOM. |
Library highlights
A non-exhaustive list of dependencies that shape behaviour:
- Javalin 7 — small Java HTTP framework with first-class SSE. WebSocket is disabled in favour of SSE (ADR 11).
- grpc-java 1.80 + grpc-netty-shaded — bidirectional streaming for
daemon connections, with
ReloadableServerSslContextfor hot CA rotation. - Lettuce 7.5 — non-blocking Redis-protocol client, used through its synchronous primitives.
- Mongo sync driver 5.6 — synchronous flavour, tuned via the URI, no wrapper.
- Apache Ratis 3.1 — Raft consensus for the embedded cluster control plane.
- Micrometer + Prometheus registry — metrics; the registration site is
MetricsCollector. - OpenTelemetry SDK + OTLP gRPC exporter — opt-in controller tracing.
- Jackson 2.21 — the only JSON/YAML/TOML binding; records map cleanly with no runtime type-magic.
- argon2-jvm — password hashing with Argon2id (64 MB memory,
3 iterations) in
cloud-security/.../PasswordHasher.java. Not bcrypt. - jjwt 0.13 — JWT signing and verification for session tokens.
- Bouncy Castle 1.83 — cosign/Rekor signature verification and mTLS CA operations.
- OSHI — host metrics collection in the daemon.
- Cobra + Charmbracelet — CLI command surface and TUI.
- Nuxt 4 / Vue 3 / Pinia — dashboard; SDK generated from OpenAPI.
What we deliberately don’t ship
| Not in v1 | Why |
|---|---|
| Spring / Guice / Dagger | DI frameworks hide the dependency graph. Hand-wired PrexorCloudBootstrap keeps the component graph readable in one file. |
| Helm / Kubernetes operator | Compose-first install fits MC operator teams. Wrapping per-instance JVMs in K8s pods is awkward and slow. |
| Grafana dashboard pack | Maintaining dashboards as code is a real burden. Metrics are stable and well-named; build the panels you need. |
| OIDC / SAML / SCIM / MFA | Single-tenant local-user plus JWT. The audience is 1–10-operator teams; SSO complexity outweighs the benefit at that scale. |
| WASM modules | Modules ship as JVM jars. Operators already have JVM expertise, and the threat model is signed-bundle integrity, not hostile-module sandboxing. |
| GitOps reconciliation loop | Templates and groups go through REST, CLI, and the dashboard imperatively. |
Each is an explicit architectural decision, not an accident of timeline. Tracing is no longer on this list — the controller ships an opt-in OpenTelemetry pipeline (see Observability).
Versioning
- Release versions — semver; minor versions are additive, major versions can break.
- gRPC contract —
cloud-protocolbumps the protocol version only on incompatible changes; the controller and daemon negotiate it at handshake. Additiveoneofvariants do not bump it. - Module SDK —
dashboard/packages/module-sdkships its own version; the compat matrix lives atdashboard/packages/module-sdk/COMPAT.md. - Schema — Mongo schema migrations run on startup and log
migration applied: <name>. Release notes call out migrations that need a data backfill.
Why we chose each thing — short list
| Pick | Over | Why |
|---|---|---|
| Java 25 control plane | Java 21 | Records, pattern matching, virtual threads. API jars stay on 21 for plugin-side compatibility. |
Hand-wired PrexorCloudBootstrap | Spring / Guice | Readable graph, faster startup, no annotation magic. |
| Javalin 7 | Spring Boot | Minimal HTTP layer, native SSE, no auto-config. |
| protoc via Gradle | buf | Code generation already lives in the Gradle build; no extra toolchain to install. |
| MongoDB | PostgreSQL | Document model fits composition plans, workflow intent, and module storage. |
| Valkey | Redis | BSD-3 license; same protocol. |
| Apache Ratis | External consensus service | Java-native Raft embedded in the controller. |
| cosign keyless | Custom signing scheme | No private key to maintain. |
| Micrometer | Prometheus simpleclient | Vendor-neutral metric facade with a Prometheus registry. |
| OpenTelemetry, opt-in | Always-on tracing | Tracing earns its keep at scale; default-off keeps the common case free. |
| Argon2id | bcrypt | Memory-hard hashing tuned for the threat model. |
| SSE | WebSocket | Server-to-client only; Last-Event-ID resumption built in (ADR 11). |
| Compose | Helm chart | Operator audience already has Docker; K8s around MC processes is awkward. |
Runtime requirements (bare metal)
| Component | Minimum | Notes |
|---|---|---|
| Controller host | Linux x86_64 (Debian/Ubuntu, RHEL/Fedora, openSUSE, Arch) | macOS / Windows are not supported as controllers. |
| Daemon host | Linux x86_64 | Same. |
| Java | OpenJDK 25 for controller and daemon, 21+ for plugin/API jars | prexorctl setup installs via the distro package manager when missing. |
| MongoDB | 6.0+ (Compose pins mongo:8.0) | Self-hosted; replica set recommended for HA. |
| Valkey / Redis | Valkey 7.2+ / Redis 7+ (Compose pins valkey/valkey:8-alpine) | Required in the production profile. |
Next up
- Architecture — process boundaries and how the stack fits together
- Storage schema — what each store holds
- Cosign pipeline — release and module signing