Extensions overview
Extensions are self-contained JARs dropped into ./extensions. On startup Universe scans the directory, finds classes that implement the Extension interface, injects their dependencies, and calls onLoad so each can register a runtime, storage backend, template variable, database, metrics exporter, or integration.
Universe keeps its core small and pushes optional behavior into extensions. An extension is an ordinary JAR placed in ./extensions/. There are no service-loader files to maintain and no registration manifest: you place the JAR, restart the node, and the extension wires itself in. Each extension contributes capability through registries exposed by the extension-api module, so the orchestrator never has to know about a backend ahead of time.
How loading works
ExtensionService runs a four-step initialization during startup, before the node begins accepting deploy tasks:
- JAR discovery
LoaderUtils.loadDirectory()adds every*.jarin./extensions/to the runtime classloader. - Class scanning
ExtensionClassUtils.extensions()scans the loaded classes for any that implement theExtensioninterface. - Guice injection
Each extension is instantiated through its no-argument constructor, then receives its
@Injectdependencies (registries, services) before any lifecycle method runs. - Lifecycle start
onLoad()is called on each extension. An extension that returnstruefrommasterOnly()is skipped silently on Wrapper nodes.
A typical working directory after copying a few JARs in looks like this:
./extensions/
extension-storage-s3.jar
extension-db-postgres.jar
extension-metrics-prometheus.jar
extension-discord.jar
s3/config.json ← per-extension config lives in its own folder
discord/config.json
metrics-influxdb/config.json
Per-extension configuration lives in its own folder under ./extensions/, named after the extension (for example ./extensions/s3/config.json). The JAR and its config folder sit side by side.
The Extension interface
Every extension implements a small contract:
id()andversion()identify the extension.masterOnly()returnstrueto load only on Master nodes (defaultfalse).reloadable()returnsfalseto refuse runtime reloads (defaulttrue).onLoad(),onReload(), andonUnload()are the lifecycle hooks. Providers are registered inonLoad()and torn down inonUnload().
What an extension can add
Extensions register against the registries in extension-api. The bundled extensions fall into six categories:
| Category | What it contributes |
|---|---|
| Storage | Remote template backends (S3, MinIO) via TemplateStorageProvider. |
| Runtimes | Execution environments (Docker, Kubernetes) via RuntimeProvider. |
| Databases | Persistence backends (PostgreSQL, MongoDB, Redis) via DatabaseProvider. |
| Metrics | Observability exporters (Prometheus, InfluxDB) via MetricsProvider. |
| DevOps | GitOps sync and Kubernetes manifest export. |
| Integrations | Third-party services such as the Discord bot. |
Installing an extension
Extension JARs are produced by the Gradle build alongside the core, but they are not bundled into the fat JAR. Copy the ones you want from .built/ into your working directory’s ./extensions/ folder, then restart the node:
# After ./gradlew build, the extension JARs land in .built/
# Copy the ones you want into the working directory's ./extensions/
cp .built/extension-storage-s3-*.jar ./extensions/
cp .built/extension-discord-*.jar ./extensions/
Managing extensions at runtime
The console exposes a small command group, also reachable over POST /api/commands/execute:
extension list
id version status scope
storage-s3 0.0.1 loaded all
db-postgres 0.0.1 loaded master
metrics-prometheus 0.0.1 loaded master
discord 0.0.1 loaded master
# Reload every reloadable extension
extension reload
# Reload one extension by id
extension reload gitops
extension reload only affects extensions whose reloadable() returns true. A reload closes the extension’s clients, re-reads its config, and re-registers its providers without restarting the cluster.
Bundled extensions
Centralized multi-node template storage on an S3 or MinIO bucket.
PostgreSQL, MongoDB, and Redis persistence backends.
Prometheus scrape endpoint and InfluxDB push export.
Inject a node’s mesh-network address into template variables.
Sync templates and configurations from a Git repository.
Export Universe state as Kubernetes manifests for ArgoCD.
Manage the cluster from Discord slash commands.
Author your own extension against the registry interfaces.