You need to sign in before continuing.
Commit 0090e3af7bf4980055d21d1ddd1475d006fbacfe
1 parent
95b26a88
docs: align deeper how-to docs with current code state
The previous docs commit fixed README + CLAUDE + the three "scope note"
guides but explicitly left the deeper how-to docs as-is, claiming the
architecture hadn't changed. That was wrong on closer reading: those
docs still described the pre-single-tenant world (tenant_id columns,
two-wall isolation, per-tenant routing) and the pre-implementation
plug-in API (class-based @PluginEndpoint controllers, ResponseBuilder,
context.log instead of context.logger, plugin.yml entryClass field).
A reader following any of those guides would write code that doesn't
compile.
This commit aligns them with what the framework actually does today.
What changed:
* docs/architecture/overview.md
- Guardrail #5 rewritten from "Multi-tenant in spirit from day one"
to "Single-tenant per instance, isolated database" matching
CLAUDE.md.
- The api.v1 package layout updated to match the actual current
surface: dropped `Tenant`, added `PrincipalId`,
`PersistenceExceptions`, the full plug-in package
(PluginContext, PluginEndpointRegistrar, PluginRequest/Response,
PluginJdbc, PluginRow), and the two live cross-PBC facades
(IdentityApi, CatalogApi).
- The whole "Multi-tenancy" section replaced with
"Single-tenant per instance" — drops the two-wall framing,
drops per-region routing, drops tenant_id columns and RLS.
The single-tenant rationale is explained in line with the
rest of the framework.
- Custom fields and metadata store sections drop "per tenant"
and tenant_id from their column lists.
- Audit row in the cross-cutting table drops tenant_id from
the column list and adds the PrincipalContext bridge note
that's actually live as of P4.1.
- "Tier 1 — Key user, no-code" intro drops "scoped to their tenant".
* docs/plugin-api/overview.md
- api.v1 package layout updated to match the real current surface
(same set of additions and deletions as the architecture
overview).
- Per-package orientation table rewritten to describe what is
actually in each package today, including which parts are
live (event/, plugin/, http/) and which are stubbed (i18n/,
workflow/, form/) with the relevant implementation-plan unit
referenced.
* docs/plugin-author/getting-started.md — full rewrite. The previous
version walked an author through APIs that don't exist:
• `Plugin.start(context: PluginContext)` and
`Plugin.stop(context: PluginContext)` — actual API is
`start(context)` and `stop()` (no arg), with the Pf4jPlugin +
VibeErpPlugin dual-supertype trick using import aliases.
• `context.log` — actual is `context.logger` (PluginLogger
interface).
• `plugin.yml` with `entryClass:` field and a structured
`metadata.i18n:` subkey — actual schema has no entryClass
(PF4J reads `Plugin-Class` from MANIFEST.MF) and `metadata:`
is a flat list of paths.
• Class-based `@PluginEndpoint` controllers with `ResponseBuilder`
— actual is lambda-based registration via
`context.endpoints.register(method, path, handler)` returning
`PluginResponse`.
• `request.translator.format(...)` — translator currently
throws UnsupportedOperationException; the example would crash.
The new version walks through:
1. The compileOnly api.v1 + pf4j dependency setup.
2. The dual-supertype Pf4jPlugin + VibeErpPlugin trick with
import aliases (matches the actual reference plug-in).
3. The real JAR layout (META-INF/MANIFEST.MF for PF4J,
plugin.yml for human metadata, META-INF/vibe-erp/db/changelog.xml
for Liquibase, META-INF/vibe-erp/metadata/<id>.yml for the
metadata loader).
4. Liquibase changelog at the unique META-INF path (not the
host's `db/changelog/master.xml` path which would collide
on parent-first classloader lookup — the bug we hit and
fixed in commit 1ead32d7).
5. Lambda-based endpoint registration with real working
examples (path templates, queryForObject, update, JSON
body parsing).
6. Metadata YAML shape matching what MetadataLoader actually
parses.
7. The boot-log sequence the operator will see, taken from
a real smoke run.
8. An honest "what this guide does not yet cover" list with
implementation-plan unit references.
* docs/i18n/guide.md
- Added a "Status" callout at the top: the api.v1 contract is
locked, the ICU4J implementation is P1.6 and not yet wired,
`context.translator` currently throws.
- Bundle filename convention corrected from `<locale>.properties`
to `messages_<locale>.properties` (matches what the reference
plug-in actually ships).
- plugin.yml example updated to show the flat `metadata:` array
that PluginManifest actually parses (the previous structured
`metadata.i18n:` subkey doesn't exist in the schema).
- Merge precedence renamed "tenant overrides" → "operator
overrides" since vibe_erp is single-tenant per instance.
- LocaleProvider resolution chain updated to drop "tenant default"
and reference `vibeerp.i18n.default-locale` configuration.
- "Time zones are per-tenant" → "per-user with an instance default".
- Locale-add reference to "i18n-overrides volume" annotated as
"once P1.6 lands".
* docs/customer-onboarding/guide.md
- Section 3 "Create a tenant" deleted; replaced with
"Configure the instance" pointing at vibeerp.instance.* config.
- First-boot bullet list rewritten to drop the bogus
"create default tenant row in identity__tenant" step (no such
table) and add the real metadata-loader and per-plug-in lifecycle
steps that happen.
- Mounted-volume table updated: `i18n-overrides/` is "operator
translation overrides", not "tenant-level".
- "Bind users to roles, bind roles to tenants" simplified;
OIDC group claims annotated as P4.2 (not yet wired).
- Go-live checklist drops "non-production tenant" wording and
"tenant locale defaults" (now instance-level config).
- "Hosted multi-tenant operations" deferred-list item rewritten
to "hosted operations: provisioning many independent vibe_erp
instances", matching the new single-tenant deployment model.
* docs/form-authoring/guide.md
- Tier 1 section drops "scoped to the tenant",
"tweaking for a specific tenant", "tenant-specific custom field".
* docs/workflow-authoring/guide.md
- "Approval chains that vary per tenant" → "Approval chains the
customer wants to author themselves".
* docs/index.md
- Architecture overview row description updated from
"multi-tenancy" to "single-tenant deployment model".
- i18n guide row description updated from "tenant overrides"
to "operator overrides".
Build: ./gradlew build still green (no source touched).
The remaining `tenant` references in docs are now ALL intentional —
they appear in sentences like "vibe_erp is single-tenant per instance",
"there is no create-a-tenant step", "no per-tenant bundles". The
historical specs under docs/superpowers/specs/ were intentionally
NOT touched: those are dated design records, and the implementation
plan already had the single-tenant refactor noted at the top.
Showing
8 changed files
with
363 additions
and
226 deletions
docs/architecture/overview.md
| ... | ... | @@ -16,7 +16,7 @@ vibe_erp adopts SAP S/4HANA's **Clean Core** vocabulary: **extensions never modi |
| 16 | 16 | 2. **Workflows are data, not code.** State machines, approval chains, and form definitions are declarative, so a new customer is onboarded by editing definitions, not source. |
| 17 | 17 | 3. **Extensibility seams come first.** Before any feature is added, the extension point it hangs off of must exist. If no seam exists, the seam is designed first. |
| 18 | 18 | 4. **The reference customer is a test, not a requirement.** Anything implemented for the reference plug-in must be expressible by a different printing shop with different rules, without code changes. |
| 19 | -5. **Multi-tenant in spirit from day one.** Even single-tenant deployments use the same multi-tenant code path. | |
| 19 | +5. **Single-tenant per instance, isolated database.** vibe_erp deliberately does NOT support multiple companies in one process. Each running instance serves exactly one company against an isolated Postgres database. Hosting many customers means provisioning many independent instances, not multiplexing them. There are no `tenant_id` columns, no row-level filters, no Postgres Row-Level Security policies anywhere in the framework. Customer isolation is a deployment concern, not a code concern. | |
| 20 | 20 | 6. **Global / i18n from day one.** No hard-coded user-facing strings, currencies, date formats, time zones, address shapes, or tax models. |
| 21 | 21 | |
| 22 | 22 | ## Two-tier extensibility |
| ... | ... | @@ -25,7 +25,7 @@ vibe_erp offers **two extension paths**, both first-class. The same outcome can |
| 25 | 25 | |
| 26 | 26 | ### Tier 1 — Key user, no-code |
| 27 | 27 | |
| 28 | -Business analysts customize the system through the web UI. Everything they create is stored as **rows in the metadata tables**, scoped to their tenant, tagged `source = 'user'`, and preserved across plug-in install/uninstall and core upgrades. | |
| 28 | +Business analysts customize the system through the web UI. Everything they create is stored as **rows in the metadata tables**, tagged `source = 'user'`, and preserved across plug-in install/uninstall and core upgrades. | |
| 29 | 29 | |
| 30 | 30 | | Capability | Stored in | |
| 31 | 31 | |---|---| |
| ... | ... | @@ -120,18 +120,20 @@ Package layout: |
| 120 | 120 | |
| 121 | 121 | ``` |
| 122 | 122 | org.vibeerp.api.v1 |
| 123 | -├── core/ Tenant, Locale, Money, Quantity, Id<T>, Result<T,E> | |
| 124 | -├── entity/ Entity, Field, FieldType, EntityRegistry | |
| 125 | -├── persistence/ Repository<T>, Query, Page, Transaction | |
| 126 | -├── workflow/ WorkflowTask, WorkflowEvent, TaskHandler | |
| 127 | -├── form/ FormSchema, UiSchema | |
| 128 | -├── http/ @PluginEndpoint, RequestContext, ResponseBuilder | |
| 129 | -├── event/ DomainEvent, EventListener, EventBus | |
| 130 | -├── security/ Principal, Permission, PermissionCheck | |
| 123 | +├── core/ Id<T>, Money, Currency, Quantity, UnitOfMeasure, Result<T,E> | |
| 124 | +├── entity/ Entity, AuditedEntity, FieldType, CustomField, EntityRegistry | |
| 125 | +├── persistence/ Repository<T>, Query, Page, Transaction, PersistenceExceptions | |
| 126 | +├── workflow/ WorkflowTask, TaskHandler, TaskContext | |
| 127 | +├── form/ FormSchema | |
| 128 | +├── http/ @PluginEndpoint, RequestContext | |
| 129 | +├── event/ DomainEvent, EventListener, EventBus (with Subscription) | |
| 130 | +├── security/ Principal, PrincipalId, Permission, PermissionCheck | |
| 131 | 131 | ├── i18n/ MessageKey, Translator, LocaleProvider |
| 132 | -├── reporting/ ReportTemplate, ReportContext | |
| 133 | -├── plugin/ Plugin, PluginManifest, ExtensionPoint | |
| 134 | -└── ext/ Typed extension interfaces a plug-in implements | |
| 132 | +├── plugin/ Plugin, PluginContext, PluginManifest, ExtensionPoint, | |
| 133 | +│ PluginEndpointRegistrar, HttpMethod, PluginRequest, | |
| 134 | +│ PluginResponse, PluginEndpointHandler, PluginJdbc, PluginRow | |
| 135 | +└── ext/ Typed cross-PBC facades (ext.identity.IdentityApi, | |
| 136 | + ext.catalog.CatalogApi) | |
| 135 | 137 | ``` |
| 136 | 138 | |
| 137 | 139 | `api.v1` is published as `api-v1.jar` to **Maven Central**, so plug-in authors can build against it without pulling the entire vibe_erp source tree. |
| ... | ... | @@ -185,41 +187,35 @@ One Spring Boot process per instance, one Docker image per release, one mounted |
| 185 | 187 | |
| 186 | 188 | The only mandatory external dependency is **PostgreSQL**. Optional sidecars for larger deployments — Keycloak, Redis, OpenSearch, SMTP relay — are off by default. |
| 187 | 189 | |
| 188 | -## Multi-tenancy | |
| 190 | +## Single-tenant per instance | |
| 189 | 191 | |
| 190 | -vibe_erp is **multi-tenant in spirit from day one**, even when deployed for a single customer. The same code path serves self-hosted single-tenant and hosted multi-tenant deployments. | |
| 192 | +vibe_erp is **deliberately single-tenant per instance**. One running process serves exactly one company against an isolated Postgres database. Hosting many customers means provisioning many independent instances, each with its own DB. There are no `tenant_id` columns, no row-level filters, no Postgres Row-Level Security policies anywhere in the framework. Customer isolation is a deployment concern, not a code concern. | |
| 193 | + | |
| 194 | +This is the choice in CLAUDE.md guardrail #5. The rationale: most ERP/EBC customers will not accept a SaaS where their data shares a database with other companies, and per-instance isolation is dramatically simpler than per-row tenant filtering. The cost is that hosting many customers requires real provisioning infrastructure (one container + one DB per customer); the benefit is that the framework itself stays simple, the audit story is trivial, and GDPR data residency is automatic — the customer chose where Postgres lives. | |
| 191 | 195 | |
| 192 | 196 | ### Schema namespacing |
| 193 | 197 | |
| 194 | 198 | PBCs and plug-ins use **table name prefixes**, not Postgres schemas: |
| 195 | 199 | |
| 196 | 200 | ``` |
| 197 | -identity__user, identity__role | |
| 198 | -catalog__item, catalog__item_attribute | |
| 199 | -inventory__stock_item, inventory__movement | |
| 200 | -orders_sales__order, orders_sales__order_line | |
| 201 | -production__work_order, production__operation | |
| 202 | -plugin_printingshop__plate_spec (reference plug-in) | |
| 203 | -metadata__custom_field, metadata__form, metadata__workflow | |
| 204 | -flowable_* (Flowable's own tables, untouched) | |
| 201 | +identity__user, identity__role, identity__user_credential | |
| 202 | +catalog__item, catalog__uom | |
| 203 | +inventory__stock_item, inventory__movement (future PBC) | |
| 204 | +orders_sales__order, orders_sales__order_line (future PBC) | |
| 205 | +plugin_printingshop__plate, plugin_printingshop__ink_recipe (reference plug-in) | |
| 206 | +metadata__entity, metadata__permission, metadata__menu, metadata__custom_field, ... | |
| 207 | +platform__event_outbox | |
| 208 | +flowable_* (Flowable's own tables, untouched, when wired) | |
| 205 | 209 | ``` |
| 206 | 210 | |
| 207 | -This keeps Hibernate, RLS policies, and migrations all in one logical schema (`public`), avoids `search_path` traps, and gives clean uninstall semantics. | |
| 208 | - | |
| 209 | -### Tenant isolation — two independent walls | |
| 210 | - | |
| 211 | -- Every business table has `tenant_id`, NOT NULL. | |
| 212 | -- Hibernate `@TenantId` filters every query at the application layer. | |
| 213 | -- Postgres Row-Level Security policies filter every query at the database layer. | |
| 214 | - | |
| 215 | -Two independent walls. A bug in one is not a data leak. Self-hosted single-customer deployments use one tenant row called `default`. Hosted multi-tenant deployments use many tenant rows. **Same code path.** | |
| 211 | +This keeps Hibernate and migrations all in one logical schema (`public`), avoids `search_path` traps, and gives clean plug-in uninstall semantics: drop everything matching `plugin_<id>__*`. | |
| 216 | 212 | |
| 217 | 213 | ### Data sovereignty |
| 218 | 214 | |
| 219 | -- Self-hosted is automatically compliant — the customer chose where Postgres lives. | |
| 220 | -- Hosted supports per-region tenant routing: each tenant row carries a region; connections are routed to the right regional Postgres cluster. | |
| 221 | -- PII tagging on field metadata drives auto-generated DSAR exports and erasure jobs (GDPR Articles 15/17). | |
| 222 | -- Append-only audit log records access to PII fields when audit-strict mode is on. | |
| 215 | +- **Self-hosted is automatically compliant** — the customer chose where Postgres lives. | |
| 216 | +- **Hosted** is a separate "operator console" concern (post-v1.0): provision a fresh vibe_erp instance + dedicated Postgres per customer. The framework code is unchanged. | |
| 217 | +- **PII tagging** on field metadata drives auto-generated DSAR exports and erasure jobs (GDPR Articles 15/17). The metadata seeder ships the tag; the eraser is future work (P3.x). | |
| 218 | +- **Append-only audit log** records the principal and timestamp of every save via the JPA listener. | |
| 223 | 219 | |
| 224 | 220 | ## Custom fields via JSONB |
| 225 | 221 | |
| ... | ... | @@ -230,7 +226,7 @@ ext jsonb not null default '{}', |
| 230 | 226 | ext_meta text generated |
| 231 | 227 | ``` |
| 232 | 228 | |
| 233 | -Custom fields are JSON keys inside `ext`. A GIN index on `ext` makes them queryable. The `metadata__custom_field` table describes the JSON shape per entity per tenant. The form designer, list views, OpenAPI generator, and AI-agent function catalog all read from this table. | |
| 229 | +Custom fields are JSON keys inside `ext`. A GIN index on `ext` makes them queryable. The `metadata__custom_field` table describes the JSON shape per entity. The form designer, list views, OpenAPI generator, and AI-agent function catalog all read from this table. | |
| 234 | 230 | |
| 235 | 231 | Why JSONB and not EAV: one row, one read, indexable, no migrations needed for additions, no joins. EAV is the wrong tool. |
| 236 | 232 | |
| ... | ... | @@ -245,7 +241,7 @@ metadata__workflow metadata__rule metadata__menu |
| 245 | 241 | metadata__report metadata__translation metadata__plugin_config |
| 246 | 242 | ``` |
| 247 | 243 | |
| 248 | -Every row carries `tenant_id`, `source` (`core` / `plugin:<id>` / `user`), `version`, `is_active`. The `source` column makes uninstall and upgrade safe: removing a plug-in cleans up its metadata, and user-created metadata is sacred and never touched by an upgrade. | |
| 244 | +Every row carries `source` (`core` / `plugin:<id>` / `user`) plus timestamps. The `source` column makes uninstall and upgrade safe: removing a plug-in cleans up its metadata via a single `DELETE WHERE source = 'plugin:<id>'`, and user-created metadata is sacred and never touched by core or plug-in upgrades. The current metadata loader (P1.5) implements `loadCore()` and `loadFromPluginJar(pluginId, jarPath)` and exposes the seeded data at the public `GET /api/v1/_meta/metadata` endpoint. | |
| 249 | 245 | |
| 250 | 246 | Every consumer reads from this store: the form renderer, list views, OpenAPI generator, AI-agent function catalog, role editor. There are no parallel sources of truth. |
| 251 | 247 | |
| ... | ... | @@ -269,7 +265,7 @@ The temptation to invent a vibe_erp-only workflow language must be rejected. BPM |
| 269 | 265 | |---|---| |
| 270 | 266 | | Security | `PermissionCheck` declared in `api.v1.security`; plug-ins register their own permissions, auto-listed in the role editor | |
| 271 | 267 | | Transactions | Spring `@Transactional` at the application-service layer; plug-ins use `api.v1.persistence.Transaction`, never Spring directly | |
| 272 | -| Audit | `created_at`, `created_by`, `updated_at`, `updated_by`, `tenant_id` on every entity, applied by a JPA listener | | |
| 268 | +| Audit | `created_at`, `created_by`, `updated_at`, `updated_by`, `version` on every entity, applied by a JPA listener that reads `created_by` from `PrincipalContext` (bound by the JWT auth filter) | | |
| 273 | 269 | | Events | Typed `DomainEvent`s on every state change; in-process bus by default; outbox table in Postgres for cross-crash reliability and as the seam where Kafka/NATS plugs in later | |
| 274 | 270 | | AI-agent surface | Same business operations exposed through REST are exposable through an MCP server; v1.1 ships the MCP endpoint, v1.0 architects the seam | |
| 275 | 271 | | Reporting | JasperReports; templates shipped by core or by plug-ins, customer-skinnable | | ... | ... |
docs/customer-onboarding/guide.md
| ... | ... | @@ -27,10 +27,11 @@ docker run -d --name vibe-erp \ |
| 27 | 27 | What happens on first boot: |
| 28 | 28 | |
| 29 | 29 | 1. The host connects to Postgres. |
| 30 | -2. Liquibase runs every core PBC's migrations and creates the `flowable_*` tables. | |
| 31 | -3. A `default` tenant row is created in `identity__tenant`. | |
| 32 | -4. A bootstrap admin user is created and its one-time password is printed to the boot log. | |
| 33 | -5. The host is ready in under 30 seconds. | |
| 30 | +2. Liquibase runs every core PBC's migrations (plus `flowable_*` once that lands). | |
| 31 | +3. A bootstrap admin user is created and its one-time password is printed to the boot log. | |
| 32 | +4. The metadata loader walks the host classpath and seeds `metadata__entity` / `metadata__permission` / `metadata__menu` from each PBC's YAML file. | |
| 33 | +5. Each plug-in JAR is linted, migrated, metadata-loaded, and started in turn. | |
| 34 | +6. The host is ready in under 30 seconds. | |
| 34 | 35 | |
| 35 | 36 | The mounted volume `/srv/vibeerp` (mapped to `/opt/vibe-erp` inside the container) holds: |
| 36 | 37 | |
| ... | ... | @@ -38,32 +39,24 @@ The mounted volume `/srv/vibeerp` (mapped to `/opt/vibe-erp` inside the containe |
| 38 | 39 | /opt/vibe-erp/ |
| 39 | 40 | ├── config/vibe-erp.yaml single config file (closed key set) |
| 40 | 41 | ├── plugins/ drop *.jar to install |
| 41 | -├── i18n-overrides/ tenant-level translation overrides | |
| 42 | +├── i18n-overrides/ operator translation overrides | |
| 42 | 43 | ├── files/ file store (if not using S3) |
| 43 | 44 | └── logs/ |
| 44 | 45 | ``` |
| 45 | 46 | |
| 46 | -Customer extensions live entirely outside the image. Upgrading the host is `docker rm` plus `docker run` with the new image tag — extensions and config stay put. | |
| 47 | +Customer extensions live entirely outside the image. Upgrading the host is `docker rm` plus `docker run` with the new image tag — extensions and config stay put. **vibe_erp is single-tenant per instance: this container serves exactly one company. Hosting a second customer means provisioning a second container with its own Postgres.** | |
| 47 | 48 | |
| 48 | 49 | ## 2. Log in as the bootstrap admin |
| 49 | 50 | |
| 50 | -Open `http://<host>:8080/`, log in with `admin` and the one-time password from the boot log, and change the password immediately. Configure OIDC (Keycloak-compatible) at this point if the customer has an existing identity provider — built-in JWT auth and OIDC SSO are both shipped from day one. | |
| 51 | +Open `http://<host>:8080/`, log in with `admin` and the one-time password from the boot log, and change the password immediately. Configure OIDC (Keycloak-compatible) at this point if the customer has an existing identity provider — built-in JWT auth is live today; OIDC is P4.2 in the implementation plan and not yet wired. | |
| 51 | 52 | |
| 52 | -## 3. Create a tenant (hosted only) | |
| 53 | +## 3. Configure the instance | |
| 53 | 54 | |
| 54 | -For a self-hosted single-customer deployment, the `default` tenant is everything you need. Skip this step. | |
| 55 | - | |
| 56 | -For a hosted multi-tenant deployment, create one tenant row per customer. Each tenant carries: | |
| 57 | - | |
| 58 | -- A unique tenant id. | |
| 59 | -- A region (used by the per-region routing layer in hosted mode). | |
| 60 | -- A default locale, currency, and time zone. | |
| 61 | - | |
| 62 | -Tenant onboarding is an `INSERT` plus seed metadata, not a migration — sub-second per tenant. | |
| 55 | +Set the company name, default locale, default currency, and default time zone in `vibe-erp.yaml` under `vibeerp.instance.*`. There is no "create a tenant" step — this entire instance belongs to one company. | |
| 63 | 56 | |
| 64 | 57 | ## 4. Use the Customize UI to model the customer's reality |
| 65 | 58 | |
| 66 | -This is where most of the integrator's time is spent. Everything here is **Tier 1**: rows in `metadata__*` tables, tagged `source = 'user'`, scoped to the tenant. No build, no restart, no deploy. **(current: API only — POST to the metadata endpoints; the UIs ship in v1.0.)** | |
| 59 | +This is where most of the integrator's time is spent. Everything here is **Tier 1**: rows in `metadata__*` tables, tagged `source = 'user'`. No build, no restart, no deploy. **(current: API only — POST to the metadata endpoints; the UIs ship in v1.0.)** | |
| 67 | 60 | |
| 68 | 61 | ### Custom fields |
| 69 | 62 | |
| ... | ... | @@ -101,7 +94,7 @@ Define the customer's roles (e.g. *sales clerk*, *production planner*, *warehous |
| 101 | 94 | - Plug-ins (each plug-in registers its own permissions through `api.v1.security.PermissionCheck`). |
| 102 | 95 | - Custom entities (each generates a standard CRUD permission set). |
| 103 | 96 | |
| 104 | -Bind users to roles, bind roles to tenants. Hosted deployments can also bind roles via OIDC group claims. | |
| 97 | +Bind users to roles. Once OIDC (P4.2) lands, roles can also be bound via the identity provider's group claims. | |
| 105 | 98 | |
| 106 | 99 | ## 7. Add Tier 2 plug-ins as needed |
| 107 | 100 | |
| ... | ... | @@ -122,14 +115,14 @@ A guiding principle: **anything a Tier 2 plug-in does should also become possibl |
| 122 | 115 | |
| 123 | 116 | - [ ] Backups configured against the Postgres instance. Customer data lives there exclusively (plus the file store). |
| 124 | 117 | - [ ] Audit log enabled for PII fields if the customer is subject to GDPR or equivalent. |
| 125 | -- [ ] DSAR export and erasure jobs tested against a non-production tenant. | |
| 126 | -- [ ] Locale, currency, time zone defaults set for the tenant. | |
| 127 | -- [ ] OIDC integration tested with the customer's identity provider. | |
| 118 | +- [ ] DSAR export and erasure jobs tested against a non-production copy. | |
| 119 | +- [ ] Locale, currency, time zone defaults set in `vibe-erp.yaml`. | |
| 120 | +- [ ] OIDC integration tested with the customer's identity provider (once P4.2 lands). | |
| 128 | 121 | - [ ] Health check (`/actuator/health`) wired into the customer's monitoring. |
| 129 | 122 | - [ ] Documented upgrade procedure for the operator: `docker rm` plus `docker run` with the new image tag, plug-ins and config stay put. |
| 130 | 123 | |
| 131 | 124 | ## What this guide deliberately does not cover |
| 132 | 125 | |
| 133 | 126 | - **Plug-in development.** That is the [plug-in author getting-started guide](../plugin-author/getting-started.md). |
| 134 | -- **Hosted multi-tenant operations** (per-region routing, billing, tenant provisioning UI). Hosted mode is a v2 deliverable; the data model and routing layer are architected for it from day one but the operations UI is not in v1.0. | |
| 135 | -- **MCP / AI-agent endpoint.** v1.1 deliverable. The seam exists in v1.0; the endpoint does not. | |
| 127 | +- **Hosted operations** (provisioning many independent vibe_erp instances, one per customer; billing; an operator console). vibe_erp is single-tenant per instance — hosting many customers means running many independent containers with their own databases. The provisioning console is a post-v1.0 deliverable. | |
| 128 | +- **MCP / AI-agent endpoint.** v1.1 deliverable. The seam exists today; the endpoint does not. | ... | ... |
docs/form-authoring/guide.md
| ... | ... | @@ -55,12 +55,12 @@ Both paths are first-class. The renderer does not know which one produced a give |
| 55 | 55 | |
| 56 | 56 | ### Tier 1 — Form designer in the web UI (v1.0) |
| 57 | 57 | |
| 58 | -Business analysts build forms in the form designer that ships in the web SPA. The result is stored as a row in `metadata__form`, scoped to the tenant, tagged `source = 'user'` so it survives plug-in install/uninstall and core upgrades. No build, no restart. | |
| 58 | +Business analysts build forms in the form designer that ships in the web SPA. The result is stored as a row in `metadata__form`, tagged `source = 'user'` so it survives plug-in install/uninstall and core upgrades. No build, no restart. | |
| 59 | 59 | |
| 60 | 60 | This path is the right one for: |
| 61 | 61 | |
| 62 | -- Tweaking the layout of an existing entity for a specific tenant. | |
| 63 | -- Adding a tenant-specific custom field to an existing form. | |
| 62 | +- Tweaking the layout of an existing entity for the customer's specific needs. | |
| 63 | +- Adding a custom field to an existing form. | |
| 64 | 64 | - Building a form for a Tier 1 custom entity. |
| 65 | 65 | |
| 66 | 66 | The form designer is a **v1.0 deliverable**. | ... | ... |
docs/i18n/guide.md
| ... | ... | @@ -2,6 +2,14 @@ |
| 2 | 2 | |
| 3 | 3 | vibe_erp is built to be sold worldwide. There are no hard-coded user-facing strings, currencies, date formats, time zones, number formats, address shapes, or tax models in the framework — everything user-facing flows through the i18n and formatting layers from day one. This is guardrail #6 in `CLAUDE.md`, and it applies to the core, every PBC, and every plug-in. |
| 4 | 4 | |
| 5 | +> **Status (current build).** The api.v1 contract for i18n (`Translator`, | |
| 6 | +> `LocaleProvider`, `MessageKey`) is locked in and present. The host | |
| 7 | +> implementation backed by ICU4J is **P1.6** in the implementation plan | |
| 8 | +> and is **not yet wired** — `context.translator` currently throws | |
| 9 | +> `UnsupportedOperationException`. This guide describes the design that | |
| 10 | +> P1.6 will deliver. See [`../../PROGRESS.md`](../../PROGRESS.md) for | |
| 11 | +> the live status of every feature. | |
| 12 | + | |
| 5 | 13 | For the architectural placement of i18n, see [`../architecture/overview.md`](../architecture/overview.md) and the full spec at [`../superpowers/specs/2026-04-07-vibe-erp-architecture-design.md`](../superpowers/specs/2026-04-07-vibe-erp-architecture-design.md). |
| 6 | 14 | |
| 7 | 15 | ## ICU MessageFormat is the backbone |
| ... | ... | @@ -23,25 +31,24 @@ The same key produces correct output in English, German, Japanese, Chinese, and |
| 23 | 31 | |
| 24 | 32 | ## Plug-ins ship message bundles |
| 25 | 33 | |
| 26 | -Each plug-in (and each PBC) ships its message bundles inside its JAR under `i18n/<locale>.properties`. The locale tag follows BCP 47: | |
| 34 | +Each plug-in (and each PBC) ships its message bundles inside its JAR under `i18n/messages_<locale>.properties`. The locale tag follows BCP 47: | |
| 27 | 35 | |
| 28 | 36 | ``` |
| 29 | 37 | src/main/resources/ |
| 30 | 38 | └── i18n/ |
| 31 | - ├── en-US.properties | |
| 32 | - ├── zh-CN.properties | |
| 33 | - ├── de-DE.properties | |
| 34 | - ├── ja-JP.properties | |
| 35 | - └── es-ES.properties | |
| 39 | + ├── messages_en-US.properties | |
| 40 | + ├── messages_zh-CN.properties | |
| 41 | + ├── messages_de-DE.properties | |
| 42 | + ├── messages_ja-JP.properties | |
| 43 | + └── messages_es-ES.properties | |
| 36 | 44 | ``` |
| 37 | 45 | |
| 38 | -The `plugin.yml` manifest lists the bundles so the loader knows to pick them up: | |
| 46 | +The `plugin.yml` manifest lists the bundle paths under its flat `metadata:` array so the loader knows to pick them up: | |
| 39 | 47 | |
| 40 | 48 | ```yaml |
| 41 | 49 | metadata: |
| 42 | - i18n: | |
| 43 | - - i18n/en-US.properties | |
| 44 | - - i18n/de-DE.properties | |
| 50 | + - i18n/messages_en-US.properties | |
| 51 | + - i18n/messages_zh-CN.properties | |
| 45 | 52 | ``` |
| 46 | 53 | |
| 47 | 54 | ## How the host merges bundles |
| ... | ... | @@ -49,16 +56,16 @@ metadata: |
| 49 | 56 | On boot, and on every plug-in install, the host builds a merged `MessageSource` per locale. The merge order, from lowest precedence to highest: |
| 50 | 57 | |
| 51 | 58 | 1. **Core bundles** shipped inside the vibe_erp image. |
| 52 | -2. **Plug-in bundles** picked up from `./plugins/*.jar` in plug-in load order. A plug-in may override a core key with the same key in its own bundle, but is not encouraged to. | |
| 53 | -3. **Tenant overrides** stored in `metadata__translation` rows tagged with the tenant id. These are entered through the Tier 1 customization UI and let an integrator rewrite any string for any tenant without rebuilding anything. | |
| 59 | +2. **Plug-in bundles** picked up from each plug-in's JAR in plug-in load order. A plug-in may override a core key with the same key in its own bundle, but is not encouraged to. | |
| 60 | +3. **Operator overrides** stored as `metadata__translation` rows tagged `source = 'user'`. These will be entered through the Tier 1 customization UI (P3.x) and let an integrator rewrite any string for the instance without rebuilding anything. | |
| 54 | 61 | |
| 55 | 62 | ``` |
| 56 | -tenant overrides ← highest precedence | |
| 63 | +operator overrides ← highest precedence | |
| 57 | 64 | plug-in bundles |
| 58 | - core bundles ← lowest precedence | |
| 65 | + core bundles ← lowest precedence | |
| 59 | 66 | ``` |
| 60 | 67 | |
| 61 | -The merged `MessageSource` is per-locale and per-tenant. Switching the user's locale or switching tenants picks the right merged bundle without restarting anything. | |
| 68 | +The merged `MessageSource` is per-locale. Switching the user's locale picks the right merged bundle without restarting anything. (vibe_erp is single-tenant per instance, so there are no per-tenant bundles — every user of the same instance shares the same merge result for a given locale.) | |
| 62 | 69 | |
| 63 | 70 | ## Shipping locales for v1.0 |
| 64 | 71 | |
| ... | ... | @@ -72,7 +79,7 @@ vibe_erp v1.0 ships with five locales: |
| 72 | 79 | | `ja-JP` | Japanese | |
| 73 | 80 | | `es-ES` | Spanish (Spain) | |
| 74 | 81 | |
| 75 | -Adding a sixth locale is a Tier 1 operation: drop a `<locale>.properties` file into `i18n-overrides/` on the mounted volume, or add `metadata__translation` rows through the customization UI. No rebuild. | |
| 82 | +Adding a sixth locale (once P1.6 lands) is a Tier 1 operation: drop a `<locale>.properties` file into `i18n-overrides/` on the mounted volume, or add `metadata__translation` rows through the customization UI. No rebuild. | |
| 76 | 83 | |
| 77 | 84 | The reference business documentation under `raw/业务流程设计文档/` is in Chinese. **That does not make Chinese the default.** Chinese is one supported locale among the five. |
| 78 | 85 | |
| ... | ... | @@ -116,7 +123,7 @@ What this means in practice: |
| 116 | 123 | - A reviewer who sees a `String.format` against a hard-coded format string rejects the PR. |
| 117 | 124 | - A reviewer who sees `if (locale == "en") "..." else "..."` rejects the PR with prejudice. |
| 118 | 125 | |
| 119 | -The `Translator` is locale-aware: it picks up the request's resolved locale from the `LocaleProvider`, which in turn resolves from (in order) the user's profile preference, the request's `Accept-Language`, the tenant default, and the system default. | |
| 126 | +The `Translator` is locale-aware: it picks up the request's resolved locale from the `LocaleProvider`, which in turn resolves from (in order) the user's profile preference, the request's `Accept-Language`, and the instance default configured in `vibeerp.i18n.default-locale`. | |
| 120 | 127 | |
| 121 | 128 | ## PII, dates, numbers, currency |
| 122 | 129 | |
| ... | ... | @@ -125,7 +132,7 @@ Locale handling does not stop at strings: |
| 125 | 132 | - **Dates and times** are formatted through the locale, not through hard-coded patterns. The host exposes locale-aware formatters; plug-ins use them. |
| 126 | 133 | - **Numbers** use the locale's grouping and decimal separators. |
| 127 | 134 | - **Currency** values are `Money` (from `api.v1.core`) — an amount plus an ISO 4217 code — and the formatter renders them in the user's locale (e.g. `$1,234.56` vs. `1.234,56 €` vs. `¥1,235`). |
| 128 | -- **Time zones** are per-tenant and per-user, not per-server. | |
| 135 | +- **Time zones** are per-user (with an instance default configured in `vibeerp.instance.*`), not per-server. | |
| 129 | 136 | - **PII fields** are tagged in the field metadata. Tagged fields drive auto-generated DSAR exports and erasure jobs (GDPR Articles 15/17), and the audit log records access to them when audit-strict mode is on. |
| 130 | 137 | - **Address shapes** are not assumed. There is no "state/province" or "ZIP code" hard-coded in the core; address fields are configurable via metadata so a Japanese address can have prefecture/city/ward and a French address can have arrondissement. |
| 131 | 138 | ... | ... |
docs/index.md
| ... | ... | @@ -6,10 +6,10 @@ vibe_erp is an ERP/EBC framework for the printing industry, designed so customer |
| 6 | 6 | |
| 7 | 7 | | Guide | What it covers | |
| 8 | 8 | |---|---| |
| 9 | -| [Architecture overview](architecture/overview.md) | Clean Core philosophy, two-tier extensibility, PBCs, the dependency rule, multi-tenancy, custom fields, the workflow engine. | | |
| 9 | +| [Architecture overview](architecture/overview.md) | Clean Core philosophy, two-tier extensibility, PBCs, the dependency rule, single-tenant deployment model, custom fields, the workflow engine. | | |
| 10 | 10 | | [Plug-in API overview](plugin-api/overview.md) | What `api.v1` is, the package layout, the stability contract, the A/B/C/D extension grading. | |
| 11 | 11 | | [Plug-in author getting started](plugin-author/getting-started.md) | A concrete walkthrough for building, packaging, and loading your first vibe_erp plug-in. | |
| 12 | 12 | | [Workflow authoring guide](workflow-authoring/guide.md) | BPMN 2.0 workflows on embedded Flowable: visual designer (Tier 1) and `.bpmn` files in plug-ins (Tier 2). | |
| 13 | 13 | | [Form authoring guide](form-authoring/guide.md) | JSON Schema + UI Schema forms: visual designer (Tier 1) and JSON files in plug-ins (Tier 2). | |
| 14 | -| [i18n guide](i18n/guide.md) | ICU MessageFormat, message bundles, locale resolution, tenant overrides, shipping locales. | | |
| 14 | +| [i18n guide](i18n/guide.md) | ICU MessageFormat, message bundles, locale resolution, operator overrides, shipping locales. | | |
| 15 | 15 | | [Customer onboarding guide](customer-onboarding/guide.md) | Integrator's checklist for standing up a new vibe_erp customer end-to-end. | | ... | ... |
docs/plugin-api/overview.md
| ... | ... | @@ -15,34 +15,37 @@ For the architectural reasoning behind `api.v1`, see [`../architecture/overview. |
| 15 | 15 | |
| 16 | 16 | ``` |
| 17 | 17 | org.vibeerp.api.v1 |
| 18 | -├── core/ Tenant, Locale, Money, Quantity, Id<T>, Result<T,E> | |
| 19 | -├── entity/ Entity, Field, FieldType, EntityRegistry | |
| 20 | -├── persistence/ Repository<T>, Query, Page, Transaction | |
| 21 | -├── event/ DomainEvent, EventListener, EventBus | |
| 22 | -├── security/ Principal, Permission, PermissionCheck | |
| 18 | +├── core/ Id<T>, Money, Currency, Quantity, UnitOfMeasure, Result<T,E> | |
| 19 | +├── entity/ Entity, AuditedEntity, FieldType, CustomField, EntityRegistry | |
| 20 | +├── persistence/ Repository<T>, Query, Page, Transaction, PersistenceExceptions | |
| 21 | +├── event/ DomainEvent, EventListener, EventBus (with Subscription) | |
| 22 | +├── security/ Principal, PrincipalId, Permission, PermissionCheck | |
| 23 | 23 | ├── i18n/ MessageKey, Translator, LocaleProvider |
| 24 | -├── http/ @PluginEndpoint, RequestContext, ResponseBuilder | |
| 25 | -├── plugin/ Plugin, PluginManifest, ExtensionPoint | |
| 26 | -├── ext/ Typed extension interfaces a plug-in implements | |
| 27 | -├── workflow/ WorkflowTask, WorkflowEvent, TaskHandler | |
| 28 | -└── form/ FormSchema, UiSchema | |
| 24 | +├── http/ @PluginEndpoint, RequestContext | |
| 25 | +├── plugin/ Plugin, PluginContext, PluginManifest, ExtensionPoint, | |
| 26 | +│ PluginEndpointRegistrar, HttpMethod, PluginRequest, | |
| 27 | +│ PluginResponse, PluginEndpointHandler, PluginJdbc, PluginRow | |
| 28 | +├── ext/ Typed cross-PBC facades: ext.identity.IdentityApi, | |
| 29 | +│ ext.catalog.CatalogApi | |
| 30 | +├── workflow/ WorkflowTask, TaskHandler, TaskContext | |
| 31 | +└── form/ FormSchema | |
| 29 | 32 | ``` |
| 30 | 33 | |
| 31 | 34 | A short orientation: |
| 32 | 35 | |
| 33 | 36 | | Package | What it gives you | |
| 34 | 37 | |---|---| |
| 35 | -| `core/` | The primitive value types every plug-in needs: `Tenant`, `Locale`, `Money`, `Quantity`, typed `Id<T>`, `Result<T,E>`. No printing concepts. | | |
| 36 | -| `entity/` | Declarative entity model. Plug-ins describe entities, fields, and field types through `EntityRegistry`; the platform handles persistence and OpenAPI. | | |
| 37 | -| `persistence/` | `Repository<T>`, `Query`, `Page`, `Transaction`. Plug-ins never see Hibernate or Spring `@Transactional` directly. | | |
| 38 | -| `event/` | `DomainEvent`, `EventListener`, `EventBus`. The primary cross-PBC and cross-plug-in communication channel. | | |
| 39 | -| `security/` | `Principal`, `Permission`, `PermissionCheck`. Plug-ins register their own permissions; the role editor auto-discovers them. | | |
| 40 | -| `i18n/` | `MessageKey`, `Translator`, `LocaleProvider`. The only sanctioned way for a plug-in to produce user-facing text. | | |
| 41 | -| `http/` | `@PluginEndpoint` and the request/response abstractions for adding REST endpoints from a plug-in. | | |
| 42 | -| `plugin/` | `Plugin`, `PluginManifest`, `ExtensionPoint`, and the `@Extension` annotation. The plug-in lifecycle entry points. | | |
| 43 | -| `ext/` | Typed extension interfaces that PBCs declare and plug-ins implement (e.g. `api.v1.ext.inventory.StockReservationStrategy`). The cross-PBC interaction surface. | | |
| 44 | -| `workflow/` | `WorkflowTask`, `WorkflowEvent`, `TaskHandler`. The hooks BPMN service tasks call into. | | |
| 45 | -| `form/` | `FormSchema`, `UiSchema`. JSON Schema and UI Schema as Kotlin types, for plug-ins shipping form definitions. | | |
| 38 | +| `core/` | The primitive value types every plug-in needs: typed `Id<T>`, `Money` + `Currency`, `Quantity` + `UnitOfMeasure`, `Result<T,E>`. No printing concepts. | | |
| 39 | +| `entity/` | Declarative entity model. `Entity` and `AuditedEntity` markers, `FieldType` sealed hierarchy, `CustomField` and `EntityRegistry` for the metadata-driven custom fields story. | | |
| 40 | +| `persistence/` | `Repository<T>`, `Query`, `Page`, `Transaction`, plus the closed `PersistenceException` hierarchy (`OptimisticLockConflictException`, `UniqueConstraintViolationException`, `EntityValidationException`, `EntityNotFoundException`). Plug-ins never see Hibernate, JPA, or Spring `@Transactional` directly. | | |
| 41 | +| `event/` | `DomainEvent`, `EventListener`, `EventBus` with `Subscription` (closeable) and topic-string + class-keyed subscribe overloads. The primary cross-PBC communication channel. **Live as of P1.7.** | | |
| 42 | +| `security/` | `Principal` (sealed: `User`, `System`, `PluginPrincipal`), `PrincipalId`, `Permission`, `PermissionCheck`. Plug-ins register their own permissions; the role editor auto-discovers them. | | |
| 43 | +| `i18n/` | `MessageKey`, `Translator`, `LocaleProvider`. The only sanctioned way for a plug-in to produce user-facing text. (Real ICU4J `Translator` impl is P1.6 — the api.v1 contract is locked, the host implementation throws `UnsupportedOperationException` until then.) | | |
| 44 | +| `http/` | `@PluginEndpoint` annotation and `RequestContext` for plug-in HTTP handlers. | | |
| 45 | +| `plugin/` | The plug-in lifecycle: `Plugin` interface, `PluginContext` (with live accessors for `logger`, `endpoints`, `eventBus`, `jdbc`), `PluginManifest`, `ExtensionPoint`/`@Extension`, and the endpoint types: `PluginEndpointRegistrar`, `HttpMethod`, `PluginRequest`, `PluginResponse`, `PluginEndpointHandler`, plus the typed-SQL surface `PluginJdbc`/`PluginRow`. | | |
| 46 | +| `ext/` | Typed cross-PBC facades. Two are live today: `ext.identity.IdentityApi` (with `UserRef`) and `ext.catalog.CatalogApi` (with `ItemRef`/`UomRef`). Future PBCs will add their own under the same pattern. | | |
| 47 | +| `workflow/` | `WorkflowTask`, `TaskHandler`, `TaskContext` (with `tenantId` removed — single-tenant — and `principal()`, `locale()`, `correlationId()` accessors). The hooks BPMN service tasks call into when Flowable lands (P2.1). | | |
| 48 | +| `form/` | `FormSchema` — JSON Schema string + UI Schema string + version. Form parsing and rendering happens in the host (server) and the SPA (client). | | |
| 46 | 49 | |
| 47 | 50 | ## The stability contract |
| 48 | 51 | ... | ... |
docs/plugin-author/getting-started.md
| 1 | 1 | # Plug-in author: getting started |
| 2 | 2 | |
| 3 | -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. | |
| 3 | +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/`, owns its own database tables, registers HTTP endpoints, and shows up in the host's boot log. | |
| 4 | 4 | |
| 5 | -For the conceptual overview of the plug-in API, read [`../plugin-api/overview.md`](../plugin-api/overview.md) first. For the architectural reasoning behind the constraints, see [`../architecture/overview.md`](../architecture/overview.md) and the full spec at [`../superpowers/specs/2026-04-07-vibe-erp-architecture-design.md`](../superpowers/specs/2026-04-07-vibe-erp-architecture-design.md). | |
| 5 | +For the conceptual overview of the plug-in API, read [`../plugin-api/overview.md`](../plugin-api/overview.md) first. For the architectural reasoning behind the constraints, see [`../architecture/overview.md`](../architecture/overview.md). The most up-to-date worked example is the **reference printing-shop plug-in** at `reference-customer/plugin-printing-shop/` in this repo — it exercises everything described below. | |
| 6 | 6 | |
| 7 | 7 | ## 1. The only dependency you need |
| 8 | 8 | |
| 9 | -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. | |
| 9 | +Add `org.vibeerp:api-v1` and PF4J's `Plugin` base class. **`api-v1` is the only vibe_erp dependency a plug-in is allowed to declare** — the host's `PluginLinter` will scan your JAR's bytecode at install time and reject it if it references anything in `org.vibeerp.platform.*` or `org.vibeerp.pbc.*`. | |
| 10 | 10 | |
| 11 | 11 | In Gradle (`build.gradle.kts`): |
| 12 | 12 | |
| 13 | 13 | ```kotlin |
| 14 | 14 | plugins { |
| 15 | - kotlin("jvm") version "2.0.0" | |
| 15 | + kotlin("jvm") version "1.9.24" | |
| 16 | 16 | } |
| 17 | 17 | |
| 18 | 18 | repositories { |
| ... | ... | @@ -20,185 +20,323 @@ repositories { |
| 20 | 20 | } |
| 21 | 21 | |
| 22 | 22 | dependencies { |
| 23 | + // The only vibe_erp dependency. Use `compileOnly` so it isn't bundled | |
| 24 | + // into your JAR — the host provides api-v1 at runtime via parent-first | |
| 25 | + // classloading. | |
| 23 | 26 | compileOnly("org.vibeerp:api-v1:1.0.0") |
| 24 | - // Test against the same artifact at runtime | |
| 25 | - testImplementation("org.vibeerp:api-v1:1.0.0") | |
| 27 | + | |
| 28 | + // PF4J's Plugin base class — also `compileOnly`. The host provides it. | |
| 29 | + compileOnly("org.pf4j:pf4j:3.12.0") | |
| 30 | +} | |
| 31 | + | |
| 32 | +// PF4J expects manifest entries to identify the plug-in. | |
| 33 | +tasks.jar { | |
| 34 | + duplicatesStrategy = DuplicatesStrategy.EXCLUDE | |
| 35 | + manifest { | |
| 36 | + attributes( | |
| 37 | + "Plugin-Id" to "hello-print", | |
| 38 | + "Plugin-Version" to project.version, | |
| 39 | + "Plugin-Provider" to "Example Print Co.", | |
| 40 | + "Plugin-Class" to "com.example.helloprint.HelloPrintPlugin", | |
| 41 | + "Plugin-Description" to "Hello-print example plug-in", | |
| 42 | + "Plugin-Requires" to "1.x", | |
| 43 | + ) | |
| 44 | + } | |
| 26 | 45 | } |
| 27 | 46 | ``` |
| 28 | 47 | |
| 29 | -`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. | |
| 48 | +`compileOnly` is correct for both deps: at runtime the host's parent-first classloader serves them. Bundling either one inside your JAR causes ClassCastException at the host/plug-in boundary. | |
| 49 | + | |
| 50 | +## 2. Implement the plug-in entry class | |
| 30 | 51 | |
| 31 | -## 2. Implement `org.vibeerp.api.v1.plugin.Plugin` | |
| 52 | +Your plug-in's main class needs to do two things at once: | |
| 32 | 53 | |
| 33 | -The plug-in's entry class is the single point where the host hands you a context and asks you to wire yourself up. | |
| 54 | +- **Extend `org.pf4j.Plugin`** so PF4J's loader can instantiate it from the `Plugin-Class` manifest entry. | |
| 55 | +- **Implement `org.vibeerp.api.v1.plugin.Plugin`** so vibe_erp's lifecycle hook can call `start(context)` after PF4J's no-arg `start()` runs. | |
| 56 | + | |
| 57 | +The two have the same simple name `Plugin` but live in different packages, so use Kotlin import aliases: | |
| 34 | 58 | |
| 35 | 59 | ```kotlin |
| 36 | 60 | package com.example.helloprint |
| 37 | 61 | |
| 38 | -import org.vibeerp.api.v1.plugin.Plugin | |
| 62 | +import org.pf4j.PluginWrapper | |
| 63 | +import org.vibeerp.api.v1.plugin.HttpMethod | |
| 39 | 64 | import org.vibeerp.api.v1.plugin.PluginContext |
| 65 | +import org.vibeerp.api.v1.plugin.PluginRequest | |
| 66 | +import org.vibeerp.api.v1.plugin.PluginResponse | |
| 67 | +import org.pf4j.Plugin as Pf4jPlugin | |
| 68 | +import org.vibeerp.api.v1.plugin.Plugin as VibeErpPlugin | |
| 69 | + | |
| 70 | +class HelloPrintPlugin(wrapper: PluginWrapper) : Pf4jPlugin(wrapper), VibeErpPlugin { | |
| 71 | + | |
| 72 | + private var context: PluginContext? = null | |
| 40 | 73 | |
| 41 | -class HelloPrintPlugin : Plugin { | |
| 74 | + override fun id(): String = "hello-print" | |
| 75 | + override fun version(): String = "0.1.0" | |
| 42 | 76 | |
| 43 | 77 | override fun start(context: PluginContext) { |
| 44 | - context.log.info("hello-print plug-in starting") | |
| 45 | - // Register listeners, seed metadata, etc. | |
| 78 | + this.context = context | |
| 79 | + context.logger.info("hello-print plug-in starting") | |
| 80 | + | |
| 81 | + // Register HTTP handlers, subscribe to events, etc., here. | |
| 46 | 82 | } |
| 47 | 83 | |
| 48 | - override fun stop(context: PluginContext) { | |
| 49 | - context.log.info("hello-print plug-in stopping") | |
| 84 | + override fun stop() { | |
| 85 | + context?.logger?.info("hello-print plug-in stopping") | |
| 86 | + context = null | |
| 50 | 87 | } |
| 51 | 88 | } |
| 52 | 89 | ``` |
| 53 | 90 | |
| 54 | -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`. | |
| 91 | +The host's lifecycle order is **load → lint → migrate → metadata → start**. By the time your `start(context)` runs: | |
| 55 | 92 | |
| 56 | -## 3. Write a `plugin.yml` manifest | |
| 93 | +- PF4J has loaded your JAR. | |
| 94 | +- The plug-in linter has scanned your bytecode for forbidden imports. | |
| 95 | +- The host has applied your `META-INF/vibe-erp/db/changelog.xml` against the shared Postgres datasource. | |
| 96 | +- The host has loaded your `META-INF/vibe-erp/metadata/*.yml` files. | |
| 97 | +- A real `PluginContext` is wired with `logger`, `endpoints`, `eventBus`, and `jdbc` accessors. | |
| 57 | 98 | |
| 58 | -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. | |
| 99 | +## 3. The plug-in JAR layout | |
| 59 | 100 | |
| 60 | -```yaml | |
| 61 | -id: com.example.helloprint | |
| 62 | -version: 0.1.0 | |
| 63 | -requiresApi: "1.x" | |
| 64 | -name: Hello Print | |
| 65 | -entryClass: com.example.helloprint.HelloPrintPlugin | |
| 101 | +``` | |
| 102 | +src/main/ | |
| 103 | +├── kotlin/ | |
| 104 | +│ └── com/example/helloprint/ | |
| 105 | +│ └── HelloPrintPlugin.kt | |
| 106 | +└── resources/ | |
| 107 | + ├── plugin.yml ← human-readable manifest | |
| 108 | + └── META-INF/ | |
| 109 | + └── vibe-erp/ | |
| 110 | + ├── db/ | |
| 111 | + │ └── changelog.xml ← Liquibase, applied at start | |
| 112 | + └── metadata/ | |
| 113 | + └── hello-print.yml ← entities, perms, menus | |
| 114 | +``` | |
| 66 | 115 | |
| 67 | -description: > | |
| 68 | - A minimal worked example for the vibe_erp plug-in author guide. | |
| 69 | - Registers one extension and exposes one REST endpoint. | |
| 116 | +Two manifest files: | |
| 70 | 117 | |
| 71 | -vendor: | |
| 72 | - name: Example Print Co. | |
| 73 | - url: https://example.com | |
| 118 | +- **`META-INF/MANIFEST.MF`** is what PF4J reads to discover the plug-in (set via Gradle's `tasks.jar.manifest { attributes(...) }`). | |
| 119 | +- **`plugin.yml`** is human-readable metadata used by the SPA and the operator console. It's parsed by `org.vibeerp.api.v1.plugin.PluginManifest`. | |
| 74 | 120 | |
| 121 | +A minimal `plugin.yml`: | |
| 122 | + | |
| 123 | +```yaml | |
| 124 | +id: hello-print | |
| 125 | +version: 0.1.0 | |
| 126 | +requiresApi: "1.x" | |
| 127 | +name: | |
| 128 | + en-US: Hello Print | |
| 129 | + zh-CN: 你好印刷 | |
| 75 | 130 | permissions: |
| 76 | - - hello_print.greet.read | |
| 77 | - | |
| 78 | -metadata: | |
| 79 | - forms: | |
| 80 | - - metadata/forms/greeting.json | |
| 81 | - i18n: | |
| 82 | - - i18n/en-US.properties | |
| 83 | - - i18n/de-DE.properties | |
| 131 | + - hello.print.greet | |
| 132 | +dependencies: [] | |
| 84 | 133 | ``` |
| 85 | 134 | |
| 86 | -Field notes: | |
| 135 | +`requiresApi: "1.x"` means "any 1.y.z release of api.v1". A mismatch fails the plug-in at install time, never at runtime. | |
| 136 | + | |
| 137 | +## 4. Own your own database tables | |
| 138 | + | |
| 139 | +Plug-ins use the same Liquibase that the host uses, applied via `PluginLiquibaseRunner` against the shared host datasource. The host looks for the changelog at exactly `META-INF/vibe-erp/db/changelog.xml`. **The unique `META-INF/vibe-erp/db/` path matters**: if you put your changelog at `db/changelog/master.xml` (the host's path), the parent-first classloader will find both files and Liquibase will throw `ChangeLogParseException` at install time. | |
| 140 | + | |
| 141 | +Convention: every table you create is prefixed `plugin_<your-id>__`. The host doesn't enforce this in v0.6, but a future linter will, and the prefix is the only thing that lets a future "uninstall plug-in" cleanly drop your tables. | |
| 142 | + | |
| 143 | +```xml | |
| 144 | +<?xml version="1.0" encoding="UTF-8"?> | |
| 145 | +<databaseChangeLog xmlns="http://www.liquibase.org/xml/ns/dbchangelog" | |
| 146 | + xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" | |
| 147 | + xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog | |
| 148 | + https://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-4.27.xsd"> | |
| 149 | + | |
| 150 | + <changeSet id="hello-print-001" author="hello-print"> | |
| 151 | + <sql> | |
| 152 | + CREATE TABLE plugin_helloprint__greeting ( | |
| 153 | + id uuid PRIMARY KEY, | |
| 154 | + name varchar(128) NOT NULL, | |
| 155 | + greeting text NOT NULL, | |
| 156 | + created_at timestamptz NOT NULL DEFAULT now() | |
| 157 | + ); | |
| 158 | + CREATE UNIQUE INDEX plugin_helloprint__greeting_name_uk | |
| 159 | + ON plugin_helloprint__greeting (name); | |
| 160 | + </sql> | |
| 161 | + <rollback> | |
| 162 | + DROP TABLE plugin_helloprint__greeting; | |
| 163 | + </rollback> | |
| 164 | + </changeSet> | |
| 165 | + | |
| 166 | +</databaseChangeLog> | |
| 167 | +``` | |
| 87 | 168 | |
| 88 | -- `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. | |
| 89 | -- `version` is your plug-in's version, not the host's. | |
| 90 | -- `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. | |
| 91 | -- `permissions` are auto-registered with the role editor. Plug-ins should not invent permissions outside their own namespace. | |
| 92 | -- `metadata` lists files inside the JAR that the host should pick up on plug-in start: form definitions, BPMN files, message bundles, seed rules. | |
| 169 | +## 5. Register HTTP endpoints | |
| 93 | 170 | |
| 94 | -## 4. Register an extension via `@Extension` | |
| 171 | +Plug-ins do NOT use Spring annotations. The host exposes a **lambda-based registrar** through `context.endpoints` that mounts your handlers under `/api/v1/plugins/<plugin-id>/<path>`. The handler receives a `PluginRequest` (path parameters, query parameters, raw body string) and returns a `PluginResponse` (status + body, serialized as JSON by the host). | |
| 95 | 172 | |
| 96 | -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>`. | |
| 173 | +Inside your `start(context)`: | |
| 97 | 174 | |
| 98 | 175 | ```kotlin |
| 99 | -package com.example.helloprint | |
| 100 | - | |
| 101 | -import org.vibeerp.api.v1.plugin.Extension | |
| 102 | -import org.vibeerp.api.v1.workflow.TaskHandler | |
| 103 | -import org.vibeerp.api.v1.workflow.WorkflowTask | |
| 176 | +override fun start(context: PluginContext) { | |
| 177 | + this.context = context | |
| 178 | + context.logger.info("hello-print plug-in starting") | |
| 179 | + | |
| 180 | + // GET /api/v1/plugins/hello-print/ping | |
| 181 | + context.endpoints.register(HttpMethod.GET, "/ping") { _: PluginRequest -> | |
| 182 | + PluginResponse( | |
| 183 | + status = 200, | |
| 184 | + body = mapOf("plugin" to "hello-print", "ok" to true), | |
| 185 | + ) | |
| 186 | + } | |
| 104 | 187 | |
| 105 | -@Extension(point = TaskHandler::class) | |
| 106 | -class GreetCustomerHandler : TaskHandler { | |
| 188 | + // GET /api/v1/plugins/hello-print/greetings | |
| 189 | + context.endpoints.register(HttpMethod.GET, "/greetings") { _ -> | |
| 190 | + val rows = context.jdbc.query( | |
| 191 | + "SELECT id, name, greeting FROM plugin_helloprint__greeting ORDER BY name", | |
| 192 | + ) { row -> | |
| 193 | + mapOf( | |
| 194 | + "id" to row.uuid("id").toString(), | |
| 195 | + "name" to row.string("name"), | |
| 196 | + "greeting" to row.string("greeting"), | |
| 197 | + ) | |
| 198 | + } | |
| 199 | + PluginResponse(body = rows) | |
| 200 | + } | |
| 107 | 201 | |
| 108 | - override val id: String = "hello_print.greet_customer" | |
| 202 | + // POST /api/v1/plugins/hello-print/greetings | |
| 203 | + context.endpoints.register(HttpMethod.POST, "/greetings") { request -> | |
| 204 | + // Plug-ins parse JSON themselves. The reference plug-in uses the | |
| 205 | + // host's Jackson via reflection; a real plug-in author would | |
| 206 | + // ship their own JSON library or use kotlinx.serialization. | |
| 207 | + val body = parseJsonObject(request.body.orEmpty()) | |
| 208 | + val name = body["name"] as? String | |
| 209 | + ?: return@register PluginResponse(400, mapOf("detail" to "name is required")) | |
| 210 | + | |
| 211 | + val id = java.util.UUID.randomUUID() | |
| 212 | + context.jdbc.update( | |
| 213 | + """ | |
| 214 | + INSERT INTO plugin_helloprint__greeting (id, name, greeting) | |
| 215 | + VALUES (:id, :name, :greeting) | |
| 216 | + """.trimIndent(), | |
| 217 | + mapOf( | |
| 218 | + "id" to id, | |
| 219 | + "name" to name, | |
| 220 | + "greeting" to "hello, $name", | |
| 221 | + ), | |
| 222 | + ) | |
| 223 | + PluginResponse( | |
| 224 | + status = 201, | |
| 225 | + body = mapOf("id" to id.toString(), "name" to name), | |
| 226 | + ) | |
| 227 | + } | |
| 109 | 228 | |
| 110 | - override fun handle(task: WorkflowTask) { | |
| 111 | - val name = task.variable<String>("customerName") ?: "world" | |
| 112 | - task.setVariable("greeting", "hello, $name") | |
| 113 | - task.complete() | |
| 229 | + // GET /api/v1/plugins/hello-print/greetings/{name} — path variable | |
| 230 | + context.endpoints.register(HttpMethod.GET, "/greetings/{name}") { request -> | |
| 231 | + val name = request.pathParameters["name"] | |
| 232 | + ?: return@register PluginResponse(400, mapOf("detail" to "name is required")) | |
| 233 | + val row = context.jdbc.queryForObject( | |
| 234 | + "SELECT name, greeting FROM plugin_helloprint__greeting WHERE name = :name", | |
| 235 | + mapOf("name" to name), | |
| 236 | + ) { r -> mapOf("name" to r.string("name"), "greeting" to r.string("greeting")) } | |
| 237 | + if (row == null) { | |
| 238 | + PluginResponse(404, mapOf("detail" to "no greeting for '$name'")) | |
| 239 | + } else { | |
| 240 | + PluginResponse(body = row) | |
| 241 | + } | |
| 114 | 242 | } |
| 115 | 243 | } |
| 116 | 244 | ``` |
| 117 | 245 | |
| 118 | -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. | |
| 246 | +A few things this is doing on purpose: | |
| 119 | 247 | |
| 120 | -## 5. Add a `@PluginEndpoint` controller | |
| 248 | +- **Path templates** with `{var}` placeholders. Spring's `AntPathMatcher` extracts the values into `PluginRequest.pathParameters`. | |
| 249 | +- **`context.jdbc`** is the api.v1 typed-SQL surface. It wraps Spring's `NamedParameterJdbcTemplate` behind a small interface that hides every JDBC and Spring type. Plug-ins use named parameters only — positional `?` is not supported. | |
| 250 | +- **`PluginResponse`** is serialized as JSON by the host. A `Map<String, Any?>` round-trips through Jackson without you doing anything. | |
| 251 | +- **Spring Security still applies.** Every plug-in endpoint inherits the `anyRequest().authenticated()` rule, so callers need a valid Bearer token. The public allowlist (actuator health, `/api/v1/_meta/**`, `/api/v1/auth/login`, `/api/v1/auth/refresh`) does NOT include plug-in endpoints. | |
| 121 | 252 | |
| 122 | -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. | |
| 253 | +## 6. Ship metadata about your plug-in | |
| 123 | 254 | |
| 124 | -```kotlin | |
| 125 | -package com.example.helloprint | |
| 255 | +Drop a YAML file at `META-INF/vibe-erp/metadata/hello-print.yml`: | |
| 126 | 256 | |
| 127 | -import org.vibeerp.api.v1.http.PluginEndpoint | |
| 128 | -import org.vibeerp.api.v1.http.RequestContext | |
| 129 | -import org.vibeerp.api.v1.http.ResponseBuilder | |
| 130 | -import org.vibeerp.api.v1.i18n.MessageKey | |
| 131 | -import org.vibeerp.api.v1.security.Permission | |
| 132 | - | |
| 133 | -@PluginEndpoint( | |
| 134 | - path = "/api/plugin/hello-print/greet", | |
| 135 | - method = "GET", | |
| 136 | - permission = "hello_print.greet.read", | |
| 137 | -) | |
| 138 | -class GreetEndpoint { | |
| 139 | - | |
| 140 | - fun handle(request: RequestContext): ResponseBuilder { | |
| 141 | - val name = request.query("name") ?: "world" | |
| 142 | - val greeting = request.translator.format( | |
| 143 | - MessageKey("hello_print.greet.message"), | |
| 144 | - mapOf("name" to name), | |
| 145 | - ) | |
| 146 | - return ResponseBuilder.ok(mapOf("greeting" to greeting)) | |
| 147 | - } | |
| 148 | -} | |
| 149 | -``` | |
| 257 | +```yaml | |
| 258 | +entities: | |
| 259 | + - name: Greeting | |
| 260 | + pbc: hello-print | |
| 261 | + table: plugin_helloprint__greeting | |
| 262 | + description: A personalized greeting record | |
| 150 | 263 | |
| 151 | -A few things this snippet is doing on purpose: | |
| 264 | +permissions: | |
| 265 | + - key: hello.print.greet.read | |
| 266 | + description: Read greetings | |
| 267 | + - key: hello.print.greet.create | |
| 268 | + description: Create greetings | |
| 269 | + | |
| 270 | +menus: | |
| 271 | + - path: /plugins/hello-print/greetings | |
| 272 | + label: Greetings | |
| 273 | + icon: message-circle | |
| 274 | + section: Hello Print | |
| 275 | + order: 100 | |
| 276 | +``` | |
| 152 | 277 | |
| 153 | -- 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. | |
| 154 | -- The greeting goes through the `Translator`. There is **no** string concatenation in user-facing code. See the [i18n guide](../i18n/guide.md). | |
| 155 | -- The handler returns through `ResponseBuilder`, not through Spring's `ResponseEntity`. Plug-ins do not see Spring directly. | |
| 278 | +The host's `MetadataLoader` will pick up this file at plug-in start, tag every row with `source = 'plugin:hello-print'`, and expose it at `GET /api/v1/_meta/metadata`. The future SPA reads that endpoint to discover what entities and menus exist. | |
| 156 | 279 | |
| 157 | -## 6. Loading the plug-in | |
| 280 | +## 7. Building, deploying, loading | |
| 158 | 281 | |
| 159 | 282 | Build the JAR and drop it into the host's plug-in directory: |
| 160 | 283 | |
| 161 | 284 | ```bash |
| 162 | -./gradlew :jar | |
| 285 | +./gradlew jar | |
| 163 | 286 | cp build/libs/hello-print-0.1.0.jar /opt/vibe-erp/plugins/ |
| 164 | -# or, for a Docker deployment | |
| 165 | -docker cp build/libs/hello-print-0.1.0.jar vibe-erp:/opt/vibe-erp/plugins/ | |
| 287 | +docker restart vibe-erp | |
| 166 | 288 | ``` |
| 167 | 289 | |
| 168 | -Restart the host: | |
| 290 | +For local development against this repo, the reference plug-in uses an `installToDev` Gradle task that stages the JAR into `./plugins-dev/` so `:distribution:bootRun` finds it on boot. The same pattern works for any plug-in. | |
| 169 | 291 | |
| 170 | -```bash | |
| 171 | -docker restart vibe-erp | |
| 172 | -``` | |
| 292 | +Hot reload of plug-ins without a restart is not in v1.0 — it is on the v1.2+ roadmap. | |
| 293 | + | |
| 294 | +## 8. What you'll see in the boot log | |
| 295 | + | |
| 296 | +A successfully-loading plug-in produces a sequence like this: | |
| 173 | 297 | |
| 174 | -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. | |
| 298 | +``` | |
| 299 | +PF4J: Plugin 'hello-print@0.1.0' resolved | |
| 300 | +PF4J: Start plugin 'hello-print@0.1.0' | |
| 301 | +PluginLiquibaseRunner: plug-in 'hello-print' has META-INF/vibe-erp/db/changelog.xml — running plug-in Liquibase migrations | |
| 302 | +liquibase.changelog: ChangeSet META-INF/vibe-erp/db/changelog.xml::hello-print-001::hello-print ran successfully | |
| 303 | +PluginLiquibaseRunner: plug-in 'hello-print' Liquibase migrations applied successfully | |
| 304 | +MetadataLoader: source='plugin:hello-print' loaded 1 entities, 2 permissions, 1 menus from 1 file(s) | |
| 305 | +VibeErpPluginManager: vibe_erp plug-in loaded: id=hello-print version=0.1.0 state=STARTED | |
| 306 | +plugin.hello-print: hello-print plug-in starting | |
| 307 | +VibeErpPluginManager: vibe_erp plug-in 'hello-print' started successfully | |
| 308 | +``` | |
| 175 | 309 | |
| 176 | -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`](../superpowers/specs/2026-04-07-vibe-erp-architecture-design.md). | |
| 310 | +If anything goes wrong, the host logs the per-plug-in failure loudly and continues with the other plug-ins. A failed lint, a failed migration, or a failed `start()` does NOT bring the framework down. | |
| 177 | 311 | |
| 178 | -## 7. Where to find logs and errors | |
| 312 | +## 9. Where to find logs and errors | |
| 179 | 313 | |
| 180 | -- **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. | |
| 181 | -- **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. | |
| 182 | -- **Migration failures:** Liquibase output is logged under the `platform-persistence` logger, scoped to your plug-in id. | |
| 183 | -- **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. | |
| 184 | -- **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. | |
| 314 | +- **Boot log:** `/opt/vibe-erp/logs/vibe-erp.log` (or stdout for `bootRun`). Filter on `o.v.p.plugins` for the plug-in lifecycle, `o.v.p.p.lint` for the linter, `o.v.p.p.migration` for Liquibase, `o.v.platform.metadata` for the metadata loader, and `plugin.<your-id>` for your own messages routed through `context.logger`. | |
| 315 | +- **Linter rejections** print every offending class + the forbidden type referenced. Fix the import; never work around the linter — it will catch you again. | |
| 316 | +- **Migration failures** abort the plug-in's start step but don't stop the framework. Read the Liquibase output carefully. | |
| 317 | +- **Runtime errors inside your handler lambdas** are caught by the dispatcher, logged with a full stack trace, and returned to the caller as a 500 with a generic body. Use `context.logger` (not your own SLF4J binding) so your messages get the per-plug-in tag. | |
| 185 | 318 | |
| 186 | -## 8. The reference printing-shop plug-in is your worked example | |
| 319 | +## 10. The reference printing-shop plug-in is your worked example | |
| 187 | 320 | |
| 188 | -`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": | |
| 321 | +`reference-customer/plugin-printing-shop/` is a real, CI-tested plug-in. When this guide leaves a question unanswered, the answer is almost always "look at how `plugin-printing-shop` does it": | |
| 189 | 322 | |
| 190 | -- The shape of `plugin.yml` for a non-trivial plug-in. | |
| 191 | -- How a plug-in declares a brand-new entity (printing plates, presses, color proofs) without touching any PBC. | |
| 192 | -- How a multi-step BPMN workflow is shipped inside a JAR and wired up to typed `TaskHandler` implementations. | |
| 193 | -- How form definitions reference custom fields. | |
| 194 | -- How i18n bundles are organized for a plug-in that ships in five locales. | |
| 323 | +- The shape of `plugin.yml` and the `META-INF/MANIFEST.MF` attributes. | |
| 324 | +- A real Liquibase changelog with multiple changesets at `META-INF/vibe-erp/db/changelog.xml`. | |
| 325 | +- A real metadata YAML at `META-INF/vibe-erp/metadata/printing-shop.yml`. | |
| 326 | +- Seven HTTP endpoints, including path-variable extraction and POST handlers that parse JSON bodies. | |
| 327 | +- The dual-supertype trick (`Pf4jPlugin` + `VibeErpPlugin`) with import aliases. | |
| 328 | +- A working `installToDev` Gradle task for local development. | |
| 195 | 329 | |
| 196 | -The plug-in is built by the main Gradle build but **not loaded by default**. Drop its JAR into `./plugins/` to load it locally. | |
| 330 | +The plug-in is built by the main Gradle build but **not loaded by default**. Drop its JAR into `./plugins/` (or use `installToDev` for development against this repo) to load it. | |
| 197 | 331 | |
| 198 | 332 | ## What this guide does not yet cover |
| 199 | 333 | |
| 200 | -- Publishing a plug-in to a marketplace — the marketplace and signed plug-ins are a v2 deliverable. | |
| 201 | -- Hot reload without restart — v1.2+. | |
| 202 | -- Calling the MCP endpoint as an AI agent — v1.1. | |
| 334 | +- **Subscribing to events.** `context.eventBus.subscribe(...)` is wired and live, but no worked example yet — see `EventBusImpl` and `EventBus` in api.v1 for the contract. | |
| 335 | +- **Workflow handlers.** `TaskHandler` is in api.v1 but Flowable isn't wired yet (P2.1, deferred). Plug-ins that try to register handlers today have nothing to call them. | |
| 336 | +- **Translator.** `context.translator` currently throws `UnsupportedOperationException` until P1.6 lands. | |
| 337 | +- **Custom permission enforcement.** Your declared permissions are stored in `metadata__permission` but `@RequirePermission`-style checks aren't enforced yet (P4.3). | |
| 338 | +- **Publishing a plug-in to a marketplace** — v2 deliverable. | |
| 339 | +- **Hot reload without restart** — v1.2+. | |
| 340 | +- **Calling the MCP endpoint as an AI agent** — v1.1. | |
| 203 | 341 | |
| 204 | -For everything else, the source of truth is [`../superpowers/specs/2026-04-07-vibe-erp-architecture-design.md`](../superpowers/specs/2026-04-07-vibe-erp-architecture-design.md). | |
| 342 | +For everything else, the source of truth is [`../superpowers/specs/2026-04-07-vibe-erp-architecture-design.md`](../superpowers/specs/2026-04-07-vibe-erp-architecture-design.md), and the live status of every feature is in [`../../PROGRESS.md`](../../PROGRESS.md). | ... | ... |
docs/workflow-authoring/guide.md
| ... | ... | @@ -20,7 +20,7 @@ Business analysts draw workflows in the BPMN designer that ships in the web SPA. |
| 20 | 20 | |
| 21 | 21 | This path is the right one for: |
| 22 | 22 | |
| 23 | -- Approval chains that vary per tenant. | |
| 23 | +- Approval chains the customer wants to author themselves. | |
| 24 | 24 | - Document routing rules that the customer wants to tune themselves. |
| 25 | 25 | - Workflows that compose existing typed task handlers in a new order. |
| 26 | 26 | ... | ... |