getting-started.md 9.12 KB

Plug-in author: getting started

This walkthrough is for a developer building their first vibe_erp plug-in. By the end you will have a JAR that drops into ./plugins/, registers an extension, exposes a REST endpoint, and shows up in the plug-in loader log on the next restart.

For the conceptual overview of the plug-in API, read ../plugin-api/overview.md first. For the architectural reasoning behind the constraints, see ../architecture/overview.md and the full spec at ../superpowers/specs/2026-04-07-vibe-erp-architecture-design.md.

1. The only dependency you need

Add org.vibeerp:api-v1 from Maven Central. This is the only vibe_erp dependency a plug-in is allowed to declare. Importing anything from org.vibeerp.platform.* or any PBC's internal package will fail the plug-in linter at install time.

In Gradle (build.gradle.kts):

plugins {
    kotlin("jvm") version "2.0.0"
}

repositories {
    mavenCentral()
}

dependencies {
    compileOnly("org.vibeerp:api-v1:1.0.0")
    // Test against the same artifact at runtime
    testImplementation("org.vibeerp:api-v1:1.0.0")
}

compileOnly is correct: at runtime the plug-in classloader is given api.v1 from the host. Bundling it inside your plug-in JAR causes classloader confusion.

2. Implement org.vibeerp.api.v1.plugin.Plugin

The plug-in's entry class is the single point where the host hands you a context and asks you to wire yourself up.

package com.example.helloprint

import org.vibeerp.api.v1.plugin.Plugin
import org.vibeerp.api.v1.plugin.PluginContext

class HelloPrintPlugin : Plugin {

    override fun start(context: PluginContext) {
        context.log.info("hello-print plug-in starting")
        // Register listeners, seed metadata, etc.
    }

    override fun stop(context: PluginContext) {
        context.log.info("hello-print plug-in stopping")
    }
}

The host calls start after the plug-in's classloader, Spring child context, and Liquibase migrations are ready. Anything the plug-in needs from the host (event bus, translator, repositories, configuration) comes through PluginContext.

3. Write a plugin.yml manifest

The manifest sits at the root of the JAR (src/main/resources/plugin.yml). It tells the host who you are, what API version you target, and what permissions you need.

id: com.example.helloprint
version: 0.1.0
requiresApi: "1.x"
name: Hello Print
entryClass: com.example.helloprint.HelloPrintPlugin

description: >
  A minimal worked example for the vibe_erp plug-in author guide.
  Registers one extension and exposes one REST endpoint.

vendor:
  name: Example Print Co.
  url: https://example.com

permissions:
  - hello_print.greet.read

metadata:
  forms:
    - metadata/forms/greeting.json
  i18n:
    - i18n/en-US.properties
    - i18n/de-DE.properties

Field notes:

  • id is globally unique. Use a reverse-DNS style. The host uses it as the plug-in's schema namespace (plugin_helloprint__*) and as the source tag (plugin:com.example.helloprint) on every metadata row the plug-in seeds.
  • version is your plug-in's version, not the host's.
  • requiresApi: "1.x" means "any 1.x release of api.v1". A mismatch fails at install time, not at runtime. Across a major version, the host loads api.v1 and api.v2 side by side for at least one major release window.
  • permissions are auto-registered with the role editor. Plug-ins should not invent permissions outside their own namespace.
  • metadata lists files inside the JAR that the host should pick up on plug-in start: form definitions, BPMN files, message bundles, seed rules.

4. Register an extension via @Extension

Extensions are how a plug-in plugs into a typed seam declared by the core or another PBC. Every seam lives in org.vibeerp.api.v1.ext.<area>.

package com.example.helloprint

import org.vibeerp.api.v1.plugin.Extension
import org.vibeerp.api.v1.workflow.TaskHandler
import org.vibeerp.api.v1.workflow.WorkflowTask

@Extension(point = TaskHandler::class)
class GreetCustomerHandler : TaskHandler {

    override val id: String = "hello_print.greet_customer"

    override fun handle(task: WorkflowTask) {
        val name = task.variable<String>("customerName") ?: "world"
        task.setVariable("greeting", "hello, $name")
        task.complete()
    }
}

The host scans @Extension-annotated classes inside the plug-in JAR and registers them against the declared extension point. A BPMN service task referencing hello_print.greet_customer will now be routed to this handler.

5. Add a @PluginEndpoint controller

Plug-ins add REST endpoints through @PluginEndpoint. The endpoint runs inside the plug-in's Spring child context and is auto-listed in the OpenAPI document under the plug-in's namespace.

package com.example.helloprint

import org.vibeerp.api.v1.http.PluginEndpoint
import org.vibeerp.api.v1.http.RequestContext
import org.vibeerp.api.v1.http.ResponseBuilder
import org.vibeerp.api.v1.i18n.MessageKey
import org.vibeerp.api.v1.security.Permission

@PluginEndpoint(
    path = "/api/plugin/hello-print/greet",
    method = "GET",
    permission = "hello_print.greet.read",
)
class GreetEndpoint {

    fun handle(request: RequestContext): ResponseBuilder {
        val name = request.query("name") ?: "world"
        val greeting = request.translator.format(
            MessageKey("hello_print.greet.message"),
            mapOf("name" to name),
        )
        return ResponseBuilder.ok(mapOf("greeting" to greeting))
    }
}

A few things this snippet is doing on purpose:

  • The endpoint declares a permission. The plug-in loader registers hello_print.greet.read with the role editor; an admin grants it before the endpoint becomes callable.
  • The greeting goes through the Translator. There is no string concatenation in user-facing code. See the i18n guide.
  • The handler returns through ResponseBuilder, not through Spring's ResponseEntity. Plug-ins do not see Spring directly.

6. Loading the plug-in

Build the JAR and drop it into the host's plug-in directory:

./gradlew :jar
cp build/libs/hello-print-0.1.0.jar /opt/vibe-erp/plugins/
# or, for a Docker deployment
docker cp build/libs/hello-print-0.1.0.jar vibe-erp:/opt/vibe-erp/plugins/

Restart the host:

docker restart vibe-erp

Hot reload of plug-ins without a restart is not in v1.0 — it is on the v1.2+ roadmap. For v1.0 and v1.1, install and uninstall require a restart.

On boot, the host scans ./plugins/, validates each manifest, runs the plug-in linter, creates a classloader and Spring child context per plug-in, runs the plug-in's Liquibase changesets in plugin_<id>__*, seeds metadata, and finally calls Plugin.start. The lifecycle in full is described in section 7 of ../superpowers/specs/2026-04-07-vibe-erp-architecture-design.md.

7. Where to find logs and errors

  • Boot log: /opt/vibe-erp/logs/vibe-erp.log. Filter on the platform-plugins logger to see exactly which plug-ins were scanned, accepted, rejected, and why.
  • Linter rejections: if your JAR imports a class outside org.vibeerp.api.v1.*, the linter rejects it at install time and logs the offending import. Fix the import; never work around the linter.
  • Migration failures: Liquibase output is logged under the platform-persistence logger, scoped to your plug-in id.
  • Runtime errors inside your plug-in: logged under the plug-in's id (com.example.helloprint). Use the PluginContext.log handed to you in start; do not pull in your own logging framework.
  • Health endpoint: /actuator/health reports per-plug-in status. A plug-in stuck in a half-loaded state shows up here before it shows up in user-visible failures.

8. The reference printing-shop plug-in is your worked example

reference-customer/plugin-printing-shop/ is a real, production-shaped plug-in built and CI-tested on every PR. It expresses the workflows in raw/业务流程设计文档/ using only api.v1. When this guide leaves a question unanswered, the answer is almost always "look at how plugin-printing-shop does it":

  • The shape of plugin.yml for a non-trivial plug-in.
  • How a plug-in declares a brand-new entity (printing plates, presses, color proofs) without touching any PBC.
  • How a multi-step BPMN workflow is shipped inside a JAR and wired up to typed TaskHandler implementations.
  • How form definitions reference custom fields.
  • How i18n bundles are organized for a plug-in that ships in five locales.

The plug-in is built by the main Gradle build but not loaded by default. Drop its JAR into ./plugins/ to load it locally.

What this guide does not yet cover

  • Publishing a plug-in to a marketplace — the marketplace and signed plug-ins are a v2 deliverable.
  • Hot reload without restart — v1.2+.
  • Calling the MCP endpoint as an AI agent — v1.1.

For everything else, the source of truth is ../superpowers/specs/2026-04-07-vibe-erp-architecture-design.md.