Skip to content

Spring Boot + Thymeleaf example

The plugwerk-springboot-thymeleaf-example project shows how to integrate Plugwerk into a Spring Boot web application so that PF4J plugins can contribute fully-formed pages (menu label + route + title + HTML body) at runtime. The host renders Thymeleaf templates as usual; plugin pages slot into the navigation bar without a restart and disappear cleanly when uninstalled.

  • Host-application developers integrating the Plugwerk client into an existing or new Spring Boot + Thymeleaf web application.
  • Teams that want a controlled "marketplace inside the app" experience — end users browse a plugin catalog and click Install instead of running shell commands.
  • A custom extension-point interface (PageContribution) where each plugin owns a route, a menu label, a page title, and the HTML body.
  • A web-UI plugin catalog at /plugins/available with one-click Install / Uninstall buttons that drive the Plugwerk REST API behind the scenes.
  • Live navigation updates: install a plugin, the menu item appears immediately; uninstall, it disappears.
  • A clean separation between host code and plugin code — host depends on plugwerk-springboot-thymeleaf-example-api, plugins depend on the same artifact and contribute extensions through PF4J.

| Module | Purpose | | ----------------------------------------------- | ------------------------------------------------------------------------- | | plugwerk-springboot-thymeleaf-example-api | Defines PageContribution, the PF4J ExtensionPoint for page plugins | | Host (src/) | Spring Boot host: PF4J wiring, Plugwerk client config, catalog UI | | plugwerk-example-plugin-sysinfo | Page plugin: JVM, OS, memory, uptime | | plugwerk-example-plugin-env | Page plugin: environment variables (secrets masked) |

The interface a page plugin implements is small and self-explanatory:

public interface PageContribution extends ExtensionPoint {
String getMenuLabel(); // label in the navigation bar
String getRoute(); // URL path segment (e.g. "sysinfo")
String getTitle(); // page <title> and heading
String renderHtml(); // HTML fragment for the page body
}

A plugin only needs to implement these four methods (plus the standard PF4J Plugin subclass and a manifest entry). The host discovers extensions through PF4J, mounts them on /page/{route}, and re-renders the navigation bar.

The full bootstrap (start the local Plugwerk server, create a default namespace, mint an API key into $PLUGWERK_API_KEY) lives in the Quick start section of the examples-repo root README. Once it is exported, two example-specific commands take you the rest of the way:

  1. Build and start the host:

    Terminal window
    cd plugwerk-springboot-thymeleaf-example
    PLUGWERK_API_KEY=$PLUGWERK_API_KEY ./gradlew bootRun

    The build automatically copies the Plugwerk client plugin ZIP into plugins/, so the host can talk to the server on first start.

  2. Build the example plugin ZIPs and publish them to the server:

    Terminal window
    ./gradlew :plugwerk-example-plugin-sysinfo:assemble \
    :plugwerk-example-plugin-env:assemble

    The full upload + approve flow (POST /plugin-releases, then POST /reviews/<release-id>/approve) is documented in the example's README.

  3. Open the host: http://localhost:8081

This is what an end user of the host application sees, end to end:

  1. Open the catalog at http://localhost:8081/plugins/available.

    Both example plugins are listed with their metadata pulled from the Plugwerk server.

  2. Click Install next to plugwerk-example-plugin-sysinfo.

    Behind the scenes the host calls Plugwerk to download the ZIP, drops it into the local plugins/ directory, and asks PF4J to load it. The page reloads with a confirmation.

  3. Watch the navigation bar. A new entry — System Info — appears.

  4. Click System Info. You land on /page/sysinfo, which is the path the plugin chose. The plugin's renderHtml() output (Java version, OS, heap, uptime) is rendered as the page body inside the host's standard layout.

  5. Repeat with the env plugin to see a second page mount at /page/env.

  6. Click Uninstall on either plugin. PF4J stops and unloads it; the menu entry vanishes; visiting the route returns a 404.

The plugin flow is identical regardless of where the Plugwerk server runs — your local Docker Compose during development, a shared staging server, or production. Only the configured plugwerk.server-url and plugwerk.api-key change.

The Spring host's Install action talks to the Plugwerk server (download the ZIP), so it sends the PLUGWERK_API_KEY it was started with whenever the namespace's catalog is not public. Anonymous read-only catalog browsing works for namespaces with publicCatalog = true without a key. Uninstall is local-only: the host removes the ZIP and extracted directory from the local plugins/ folder and asks PF4J to unload the plugin — no server call, no API key required. See Authentication for the full read/write split.

| Property | Env | Default | Description | | ----------------------- | ---------------------- | ------------------------- | -------------------------- | | server.port | SERVER_PORT | 8081 | Web server port | | plugwerk.server-url | PLUGWERK_SERVER_URL | http://localhost:8080 | Plugwerk server base URL | | plugwerk.namespace | PLUGWERK_NAMESPACE | default | Namespace slug | | plugwerk.api-key | PLUGWERK_API_KEY | (none) | Namespace-scoped API key | | plugwerk.plugins-dir | PLUGWERK_PLUGINS_DIR | ./plugins | PF4J plugins directory |

  • First plugin upload — the minimal "upload a plugin" walkthrough that this example builds on
  • Plugin lifecycle — the draft → published → archived flow you trigger when uploading the example plugins
  • Java CLI example — the same install / use / uninstall loop, but for CLI subcommands instead of web pages