Skip to content

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

ProcessLanguageBuild targetRuntime imageFramework / library
ControllerJava 25 (--enable-preview)JDK 25eclipse-temurin:25-jdk builder → debian:bookworm-slimJavalin 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
DaemonJava 25 (--enable-preview)JDK 25eclipse-temurin:25-jdk builder → jlink JRE on debian:bookworm-slimgrpc-java (streaming client), ProcessBuilder for instance processes, OSHI (host metrics), Logback + SLF4J
DashboardTypeScriptNode 22 + browsernode:22-alpine builder → nginx:alpineNuxt 4, Vue 3, Pinia, generated OpenAPI SDK, ESLint + Vitest + Playwright
CLI (prexorctl)Go 1.24static binarynone (single binary)Cobra (commands), Charmbracelet bubbletea/huh/lipgloss (TUI), goreleaser (cross-build + sign)
Server plugins (Paper / Spigot / Folia / Fabric / NeoForge)Javahost server JVMhost MC serverPaper / Bukkit API, Fabric Loom, NeoForge ModDevGradle
Proxy plugins (Velocity / BungeeCord / Geyser)Javahost proxy JVMhost proxyVelocity 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:

Terminal window
./gradlew :cloud-protocol:generateProto

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

ToolRole
cosignKeyless signing of release artefacts and module bundles; operator-side verification
FulcioShort-lived signing certificates from OIDC identity
RekorTransparency log; offline SET enforcement via modules.signing.rekor.policy=REQUIRE_SET
TrivyVulnerability scan against built images
SyftCycloneDX 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

ConcernToolNotes
MetricsMicrometer + Prometheus registryMetricsCollector 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).
TracingOpenTelemetry (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.
LogsLogback + SLF4JHUMAN 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

ToolPurpose
Gradle (Kotlin DSL)Multi-project Java build. Convention plugins in build-logic/ pin Java 25 (control plane) or 21 (API jars) per module.
pnpmDashboard, module SDK, and auxiliary Node workspaces.
Go modulesCLI build.
protobuf-gradle-plugin + protocgRPC/protobuf code generation for cloud-protocol.
GitHub ActionsTests, builds, releases. release.yml ships cosign-signed prexorctl binaries; release-images.yml ships cosign-signed multi-arch GHCR images on v*.
goreleaserCLI cross-compile, sign, and publish.
SpotlessFormat/license enforcement (needs JDK 25).
Trivy / SyftImage 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 ReloadableServerSslContext for 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 v1Why
Spring / Guice / DaggerDI frameworks hide the dependency graph. Hand-wired PrexorCloudBootstrap keeps the component graph readable in one file.
Helm / Kubernetes operatorCompose-first install fits MC operator teams. Wrapping per-instance JVMs in K8s pods is awkward and slow.
Grafana dashboard packMaintaining dashboards as code is a real burden. Metrics are stable and well-named; build the panels you need.
OIDC / SAML / SCIM / MFASingle-tenant local-user plus JWT. The audience is 1–10-operator teams; SSO complexity outweighs the benefit at that scale.
WASM modulesModules ship as JVM jars. Operators already have JVM expertise, and the threat model is signed-bundle integrity, not hostile-module sandboxing.
GitOps reconciliation loopTemplates 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 contractcloud-protocol bumps the protocol version only on incompatible changes; the controller and daemon negotiate it at handshake. Additive oneof variants do not bump it.
  • Module SDKdashboard/packages/module-sdk ships its own version; the compat matrix lives at dashboard/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

PickOverWhy
Java 25 control planeJava 21Records, pattern matching, virtual threads. API jars stay on 21 for plugin-side compatibility.
Hand-wired PrexorCloudBootstrapSpring / GuiceReadable graph, faster startup, no annotation magic.
Javalin 7Spring BootMinimal HTTP layer, native SSE, no auto-config.
protoc via GradlebufCode generation already lives in the Gradle build; no extra toolchain to install.
MongoDBPostgreSQLDocument model fits composition plans, workflow intent, and module storage.
ValkeyRedisBSD-3 license; same protocol.
Apache RatisExternal consensus serviceJava-native Raft embedded in the controller.
cosign keylessCustom signing schemeNo private key to maintain.
MicrometerPrometheus simpleclientVendor-neutral metric facade with a Prometheus registry.
OpenTelemetry, opt-inAlways-on tracingTracing earns its keep at scale; default-off keeps the common case free.
Argon2idbcryptMemory-hard hashing tuned for the threat model.
SSEWebSocketServer-to-client only; Last-Event-ID resumption built in (ADR 11).
ComposeHelm chartOperator audience already has Docker; K8s around MC processes is awkward.

Runtime requirements (bare metal)

ComponentMinimumNotes
Controller hostLinux x86_64 (Debian/Ubuntu, RHEL/Fedora, openSUSE, Arch)macOS / Windows are not supported as controllers.
Daemon hostLinux x86_64Same.
JavaOpenJDK 25 for controller and daemon, 21+ for plugin/API jarsprexorctl setup installs via the distro package manager when missing.
MongoDB6.0+ (Compose pins mongo:8.0)Self-hosted; replica set recommended for HA.
Valkey / RedisValkey 7.2+ / Redis 7+ (Compose pins valkey/valkey:8-alpine)Required in the production profile.

Next up