Configuration
Builder API
Section titled “Builder API”Create a PlugwerkConfig using the builder. The server URL and namespace are required — all other parameters are optional:
var config = new PlugwerkConfig.Builder( "https://myplugwerk.host", "acme-corp") // optional .apiKey("pwk_...") .pluginDirectory(Path.of("./plugins")) .connectionTimeoutMs(5000) .readTimeoutMs(20000) .build();val config = PlugwerkConfig.Builder( "https://myplugwerk.host", "acme-corp") // optional .apiKey("pwk_...") .pluginDirectory(Path.of("./plugins")) .connectionTimeoutMs(5000) .readTimeoutMs(20000) .build()Parameters
Section titled “Parameters”| Parameter | Type | Default | Required | Description |
| -------------------- | ------ | ---------- | -------- | ----------------------------------------------------------------- |
| serverUrl | String | — | Yes | Plugwerk Server base URL (e.g., https://myplugwerk.host) |
| namespace | String | — | Yes | Namespace slug (e.g., acme-corp) |
| apiKey | String | null | No | API key sent as X-Api-Key header. Takes precedence over accessToken |
| accessToken | String | null | No | JWT/OIDC token sent as Authorization: Bearer header |
| pluginDirectory | Path | null | No* | Local directory for downloaded plugin artifacts |
| connectionTimeoutMs| Long | 10000 | No | TCP connection timeout in milliseconds |
| readTimeoutMs | Long | 30000 | No | HTTP read timeout in milliseconds |
Authentication
Section titled “Authentication”Authentication is only required for private namespaces. Public namespaces can be accessed without any credentials — simply omit both apiKey and accessToken.
For private namespaces, use apiKey — this is the standard authentication method for automated clients and CI/CD pipelines. The accessToken parameter is intended for human users who authenticate via OIDC or username/password and already have a JWT token. In most client plugin integrations, apiKey is the right choice.
If both apiKey and accessToken are set, the API key takes precedence.
Properties File
Section titled “Properties File”As an alternative to the builder, you can load the configuration from a properties file:
var config = PlugwerkConfig.fromProperties( Path.of("plugwerk.properties"));val config = PlugwerkConfig.fromProperties( Path.of("plugwerk.properties"))Example plugwerk.properties:
plugwerk.serverUrl=https://myplugwerk.hostplugwerk.namespace=acme-corpplugwerk.apiKey=pwk_...plugwerk.pluginDirectory=./pluginsplugwerk.connectionTimeoutMs=5000plugwerk.readTimeoutMs=20000plugwerk.serverUrl and plugwerk.namespace are mandatory. All other properties are optional.
Connecting to the Server
Section titled “Connecting to the Server”PlugwerkPlugin.connect(config) is a JDBC-style factory: each call returns a fresh PlugwerkMarketplace, which is AutoCloseable. Close it when you are done — use try-with-resources (Java) or .use { } (Kotlin) for scoped access, or hold the reference for the lifetime of your application and call close() on shutdown (see Multi-Server Setup for the recommended long-lived pattern).
// Get the client plugin from PF4JPlugwerkPlugin plugin = (PlugwerkPlugin) pluginManager.getPlugin(PlugwerkPlugin.PLUGIN_ID).getPlugin();
// Scoped: marketplace is closed automaticallytry (PlugwerkMarketplace marketplace = plugin.connect(config)) { // ... use marketplace}// Get the client plugin from PF4Jval plugin = pluginManager .getPlugin(PlugwerkPlugin.PLUGIN_ID) .plugin as PlugwerkPlugin
// Scoped: marketplace is closed automaticallyplugin.connect(config).use { marketplace -> // ... use marketplace}Extension Points
Section titled “Extension Points”The marketplace facade provides access to three extension points:
| Extension Point | Purpose |
| ----------------------- | ------------------------------------- |
| PlugwerkCatalog | Browse and search the plugin catalog |
| PlugwerkInstaller | Download, verify, install (load + start), and uninstall plugins |
| PlugwerkUpdateChecker | Check installed plugins for updates |
| PlugwerkMarketplace | Unified facade combining all three |
Usage Examples
Section titled “Usage Examples”try (PlugwerkMarketplace marketplace = plugin.connect(config)) { // Browse catalog var plugins = marketplace.catalog().listPlugins();
// Search var results = marketplace.catalog() .searchPlugins(new SearchCriteria.Builder().query("crm").build());
// Install — downloads, verifies, loads and starts the plugin in one call var result = marketplace.installer() .install("com.acme.crm-connector", "3.0.0");
// Check for updates var installed = Map.of("com.acme.crm-connector", "2.0.0"); var updates = marketplace.updateChecker() .checkForUpdates(installed);}plugin.connect(config).use { marketplace -> // Browse catalog val plugins = marketplace.catalog().listPlugins()
// Search val results = marketplace.catalog() .searchPlugins(SearchCriteria(query = "crm"))
// Install — downloads, verifies, loads and starts the plugin in one call val result = marketplace.installer() .install("com.acme.crm-connector", "3.0.0")
// Check for updates val installed = mapOf("com.acme.crm-connector" to "2.0.0") val updates = marketplace.updateChecker() .checkForUpdates(installed)}Install and uninstall lifecycle
Section titled “Install and uninstall lifecycle”PlugwerkInstaller is responsible for the full plugin lifecycle, not just file placement. After a successful install(...) the plugin is loaded and started in PF4J — there is no separate pluginManager.loadPlugin(...) / startPlugin(...) step for the host to add. The installer exposes four methods, each with a narrow job:
| Method | What it does |
| ----------------------------------------------- | --------------------------------------------------------------------------------- |
| install(pluginId, version) | Download → SHA-256 verify → atomic move → loadPlugin → startPlugin. Plugin is live on success. |
| uninstall(pluginId) | stopPlugin → unloadPlugin → delete the artifact (and any expanded ZIP directory) |
| download(pluginId, version, targetDir) | Download + verify only — returns the Path of the verified artifact. Headless / dry-run / pre-stage use cases. |
| verifyChecksum(artifactPath, expectedSha256) | Verify a local file's SHA-256 against an expected hex digest. Returns boolean. |
Installing
Section titled “Installing”install(pluginId, version) returns an InstallResult — a sealed type with Success(pluginId, version) or Failure(pluginId, version, reason). Consume it with onSuccess / onFailure, or fold(...) for an exhaustive value mapping:
marketplace.installer() .install("com.acme.crm-connector", "3.0.0") .onSuccess(s -> log.info("Installed {} {}", s.getPluginId(), s.getVersion())) .onFailure(f -> log.warn("Install failed: {}", f.getReason()));marketplace.installer() .install("com.acme.crm-connector", "3.0.0") .onSuccess { log.info("Installed ${it.pluginId} ${it.version}") } .onFailure { log.warn("Install failed: ${it.reason}") }Uninstalling
Section titled “Uninstalling”uninstall(pluginId) stops and unloads the plugin via PF4J, then deletes the artifact file (and any expanded ZIP directory) from the plugin directory. It returns an UninstallResult — same sealed shape as InstallResult, but the Success and Failure cases carry only pluginId (the uninstaller does not need to know the version). If the plugin is not currently installed at all, uninstall returns UninstallResult.Failure rather than throwing.
marketplace.installer() .uninstall("com.acme.crm-connector") .onSuccess(s -> log.info("Uninstalled {}", s.getPluginId())) .onFailure(f -> log.warn("Uninstall failed: {}", f.getReason()));marketplace.installer() .uninstall("com.acme.crm-connector") .onSuccess { log.info("Uninstalled ${it.pluginId}") } .onFailure { log.warn("Uninstall failed: ${it.reason}") }Headless download or verify
Section titled “Headless download or verify”download(pluginId, version, targetDir) does the verified download without touching the PF4J PluginManager — useful for audit pipelines, dry runs, or pre-staging artifacts on a build agent that will not run the plugin itself. It returns the Path of the verified file inside targetDir and throws IOException on download or checksum failure.
Path artifact = marketplace.installer() .download("com.acme.crm-connector", "3.0.0", Path.of("./staging"));
// Re-verify a local file later (offline mirror, signed-distribution flow, …)boolean ok = marketplace.installer() .verifyChecksum(artifact, "a1b2c3...64-hex-chars");val artifact: Path = marketplace.installer() .download("com.acme.crm-connector", "3.0.0", Path.of("./staging"))
// Re-verify a local file later (offline mirror, signed-distribution flow, …)val ok = marketplace.installer() .verifyChecksum(artifact, "a1b2c3...64-hex-chars")Result types at a glance
Section titled “Result types at a glance”| Type | Success carries | Failure carries | Common helpers |
| ---------------- | ------------------ | --------------------------- | ------------------------------------------------------------------------------ |
| InstallResult | pluginId, version | pluginId, version, reason | onSuccess(...), onFailure(...), fold(...), isSuccess(), isFailure(), reasonOrNull() |
| UninstallResult| pluginId | pluginId, reason | same shape — version is intentionally absent |
Multi-Server Setup
Section titled “Multi-Server Setup”Connecting to multiple Plugwerk Servers or multiple namespaces is the host application's responsibility: keep a small collection of PlugwerkMarketplace instances and close each on shutdown. The plugin itself no longer maintains an internal registry.
The two configuration objects (prodConfig, stagingConfig, vendorConfig) are built with the Builder API shown earlier — one PlugwerkConfig per server URL / namespace / credential combination.
Plain class
Section titled “Plain class”public class PlugwerkServers implements AutoCloseable { public final PlugwerkMarketplace production; public final PlugwerkMarketplace staging; public final PlugwerkMarketplace vendor;
public PlugwerkServers( PlugwerkPlugin plugin, PlugwerkConfig prodConfig, PlugwerkConfig stagingConfig, PlugwerkConfig vendorConfig ) { this.production = plugin.connect(prodConfig); this.staging = plugin.connect(stagingConfig); this.vendor = plugin.connect(vendorConfig); }
@Override public void close() { production.close(); staging.close(); vendor.close(); }}class PlugwerkServers( plugin: PlugwerkPlugin, prodConfig: PlugwerkConfig, stagingConfig: PlugwerkConfig, vendorConfig: PlugwerkConfig,) : AutoCloseable { val production: PlugwerkMarketplace = plugin.connect(prodConfig) val staging: PlugwerkMarketplace = plugin.connect(stagingConfig) val vendor: PlugwerkMarketplace = plugin.connect(vendorConfig)
override fun close() { production.close() staging.close() vendor.close() }}Spring beans
Section titled “Spring beans”When using Spring, expose each marketplace as a bean and let the container drive close():
@Configurationpublic class PlugwerkBeans {
@Bean(destroyMethod = "close") public PlugwerkMarketplace production( PlugwerkPlugin plugin, @Qualifier("prodConfig") PlugwerkConfig config ) { return plugin.connect(config); }
@Bean(destroyMethod = "close") public PlugwerkMarketplace staging( PlugwerkPlugin plugin, @Qualifier("stagingConfig") PlugwerkConfig config ) { return plugin.connect(config); }
@Bean(destroyMethod = "close") public PlugwerkMarketplace vendor( PlugwerkPlugin plugin, @Qualifier("vendorConfig") PlugwerkConfig config ) { return plugin.connect(config); }}@Configurationclass PlugwerkBeans {
@Bean(destroyMethod = "close") fun production( plugin: PlugwerkPlugin, @Qualifier("prodConfig") config: PlugwerkConfig, ): PlugwerkMarketplace = plugin.connect(config)
@Bean(destroyMethod = "close") fun staging( plugin: PlugwerkPlugin, @Qualifier("stagingConfig") config: PlugwerkConfig, ): PlugwerkMarketplace = plugin.connect(config)
@Bean(destroyMethod = "close") fun vendor( plugin: PlugwerkPlugin, @Qualifier("vendorConfig") config: PlugwerkConfig, ): PlugwerkMarketplace = plugin.connect(config)}Why no internal registry?
Section titled “Why no internal registry?”Earlier versions of PlugwerkPlugin exposed a registry API (configure(id, …), marketplace(id), serverIds(), remove(id)). plugwerk/plugwerk#365 replaced this with a JDBC-style connect(config) factory: composition is now the host's job, which makes lifecycle, dependency injection, and testing straightforward — and avoids leaking a string-keyed registry across the SPI boundary.