universe docs source
browse docs
docs /extensions /overview

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:

  1. JAR discovery

    LoaderUtils.loadDirectory() adds every *.jar in ./extensions/ to the runtime classloader.

  2. Class scanning

    ExtensionClassUtils.extensions() scans the loaded classes for any that implement the Extension interface.

  3. Guice injection

    Each extension is instantiated through its no-argument constructor, then receives its @Inject dependencies (registries, services) before any lifecycle method runs.

  4. Lifecycle start

    onLoad() is called on each extension. An extension that returns true from masterOnly() 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
i
note

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() and version() identify the extension.
  • masterOnly() returns true to load only on Master nodes (default false).
  • reloadable() returns false to refuse runtime reloads (default true).
  • onLoad(), onReload(), and onUnload() are the lifecycle hooks. Providers are registered in onLoad() and torn down in onUnload().

What an extension can add

Extensions register against the registries in extension-api. The bundled extensions fall into six categories:

CategoryWhat it contributes
StorageRemote template backends (S3, MinIO) via TemplateStorageProvider.
RuntimesExecution environments (Docker, Kubernetes) via RuntimeProvider.
DatabasesPersistence backends (PostgreSQL, MongoDB, Redis) via DatabaseProvider.
MetricsObservability exporters (Prometheus, InfluxDB) via MetricsProvider.
DevOpsGitOps sync and Kubernetes manifest export.
IntegrationsThird-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
!
warning

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