From 32942f6e4f14232a6d5c1b87e784d95d84948103 Mon Sep 17 00:00:00 2001 From: vibe_erp Date: Tue, 7 Apr 2026 15:29:48 +0800 Subject: [PATCH] docs: README, CONTRIBUTING, LICENSE, full docs site, architecture spec, implementation plan, updated CLAUDE.md --- CONTRIBUTING.md | 99 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ LICENSE | 201 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ README.md | 109 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ docs/architecture/overview.md | 283 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ docs/customer-onboarding/guide.md | 135 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ docs/form-authoring/guide.md | 110 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ docs/i18n/guide.md | 135 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ docs/index.md | 15 +++++++++++++++ docs/plugin-api/overview.md | 90 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ docs/plugin-author/getting-started.md | 204 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ docs/superpowers/specs/2026-04-07-vibe-erp-implementation-plan.md | 373 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ docs/workflow-authoring/guide.md | 118 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 12 files changed, 1872 insertions(+), 0 deletions(-) create mode 100644 CONTRIBUTING.md create mode 100644 LICENSE create mode 100644 README.md create mode 100644 docs/architecture/overview.md create mode 100644 docs/customer-onboarding/guide.md create mode 100644 docs/form-authoring/guide.md create mode 100644 docs/i18n/guide.md create mode 100644 docs/index.md create mode 100644 docs/plugin-api/overview.md create mode 100644 docs/plugin-author/getting-started.md create mode 100644 docs/superpowers/specs/2026-04-07-vibe-erp-implementation-plan.md create mode 100644 docs/workflow-authoring/guide.md diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..8d033ec --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,99 @@ +# Contributing to vibe_erp + +vibe_erp is a framework that other developers will build on. The rules below exist so the framework stays reusable across customers and upgrade-safe across releases. They are not negotiable. + +For the full architectural reasoning, see [`docs/superpowers/specs/2026-04-07-vibe-erp-architecture-design.md`](docs/superpowers/specs/2026-04-07-vibe-erp-architecture-design.md) and `CLAUDE.md`. + +## The dependency rule + +The Gradle build enforces this. CI fails on violations. + +``` +api/api-v1 depends on: nothing (Kotlin stdlib + jakarta.validation only) +platform/* depends on: api/api-v1 + Spring + libs +pbc/* depends on: api/api-v1 + platform/* (NEVER another pbc) +plugins (incl. ref) depend on: api/api-v1 only +``` + +Consequences: + +- **PBCs never import each other.** `pbc-orders-sales` cannot declare `pbc-inventory` as a dependency. Cross-PBC interaction goes through (a) the event bus or (b) service interfaces declared in `api.v1.ext.`. +- **Plug-ins only see `api.v1`.** Importing anything from `platform.*` or any PBC's internal package fails the plug-in linter at install time. +- **Reaching into `internal` packages via reflection is Grade D** and rejected by the loader. + +## Adding to `api.v1` is a one-way door — don't + +`api.v1` is the only semver-governed surface in the codebase. Once a class, method, or behavior is in `api.v1`, removing or renaming it requires a major version bump and a deprecation window of at least one major release. + +When in doubt, **keep things out of `api.v1`**. It is much easier to grow the API deliberately later than to maintain a regretted symbol forever. + +Adding to `api.v1` is OK when: + +- The reference printing-shop plug-in (or a plausible second customer plug-in) cannot express its requirement without it. +- The addition is a new type or a default-implemented method on an existing interface (binary-compatible). +- The addition has a written KDoc contract and at least one consuming test. + +Adding to `api.v1` is **not** OK when: + +- It exists only to make one PBC easier to write — that belongs in `platform.*`. +- It leaks an implementation detail (Hibernate type, Spring annotation, JPA entity). +- It is "we might need it later" — wait until you do. + +## Commit message convention + +Conventional Commits, scoped by module: + +``` +feat(pbc-identity): add SCIM provisioning endpoint +fix(api-v1): tolerate missing locale on Translator.format +chore(platform-plugins): bump PF4J to 3.x +docs(architecture): clarify outbox seam +test(pbc-catalog): cover JSONB ext field round-trip +refactor(platform-persistence): extract tenant filter +``` + +Allowed types: `feat`, `fix`, `chore`, `docs`, `test`, `refactor`, `build`, `ci`, `perf`, `revert`. Scope is the module name. Breaking changes use `!` after the scope (e.g. `feat(api-v1)!: …`) and require a `BREAKING CHANGE:` footer. + +## Pull request checklist + +Every PR must satisfy all of these: + +- [ ] Tests cover the new behavior. New PBC code has unit tests; new endpoints have integration tests against a real Postgres. +- [ ] Every public `api.v1` type has KDoc on the type and on every method, including parameter, return, and exception contracts. +- [ ] Liquibase changesets ship with **rollback blocks**. CI rejects PRs without them. +- [ ] Any user-facing string is referenced via an i18n key, never concatenated. Default `en-US` translation is added in the same PR. +- [ ] No printing-specific terminology in `pbc/*` or `platform/*`. Printing concepts live in `reference-customer/plugin-printing-shop/`. +- [ ] No new cross-PBC dependency. If you reached for one, design an `api.v1.ext.` interface or a `DomainEvent` instead. +- [ ] Commit messages follow Conventional Commits. +- [ ] Documentation under `docs/` is updated in the same PR if you added or changed a public seam. + +## Build, test, and plug-in load commands + +```bash +# Build everything +./gradlew build + +# Run the full test suite +./gradlew test + +# Run a single PBC's tests +./gradlew :pbc:pbc-identity:test + +# Build the api-v1 jar (the contract that plug-ins consume) +./gradlew :api:api-v1:jar + +# Build the reference plug-in +./gradlew :reference-customer:plugin-printing-shop:jar + +# Boot the distribution and load the reference plug-in +cp reference-customer/plugin-printing-shop/build/libs/*.jar /tmp/vibeerp/plugins/ +./gradlew :distribution:bootRun +``` + +The plug-in loader logs every plug-in it scans, accepts, and rejects, with the reason. If a plug-in fails to load, the cause is in the boot log under the `platform-plugins` logger. + +## Style notes + +- Kotlin: idiomatic Kotlin, no `!!`, no `lateinit` outside test fixtures, prefer data classes for value objects. +- Public API: KDoc is required, not optional. Internal helpers may skip KDoc but should still be self-explanatory. +- No printing terminology in core, ever. "Item", "document", "operation", "work order" are generic. "Plate", "ink", "press", "color proof" are not. diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..7c1528c --- /dev/null +++ b/LICENSE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for describing the origin of the Work and + reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied. See the License for the specific language governing + permissions and limitations under the License. diff --git a/README.md b/README.md new file mode 100644 index 0000000..ff9280b --- /dev/null +++ b/README.md @@ -0,0 +1,109 @@ +# vibe_erp + +vibe_erp is an ERP/EBC framework for the printing industry, sold worldwide and deployed self-hosted-first. It is not an ERP application: it is the substrate on which any printing shop's workflows, forms, roles, and rules can be assembled by configuration and plug-ins instead of by forking the core. The reference business under `raw/` is one example customer, used as an executable acceptance test, never as the spec. + +## Why a framework, not an app + +Printing shops differ in process, terminology, paperwork, and order of operations. An app that hard-codes one shop's workflow becomes a fork farm the moment a second customer signs up. vibe_erp follows the **Clean Core** philosophy borrowed from SAP S/4HANA: the core stays generic and upgrade-safe, and every customer-specific concept lives in metadata rows or plug-ins. The core never knows what a "plate" or a "press" is — those concepts are introduced by a plug-in. Upgrading the core does not touch the customer's extensions. + +## Architecture in one picture + +``` +┌──────────────────────────────────────────────────────────────────────┐ +│ Customer's network │ +│ │ +│ Browser (React SPA) ─┐ │ +│ AI agent (MCP, v1.1)─┼─► Reverse proxy ──► vibe_erp backend (1 image)│ +│ 3rd-party system ─┘ │ │ +│ │ │ +│ Inside the image (one Spring Boot process): │ │ +│ ┌─────────────────────────────────────┐ │ │ +│ │ HTTP layer (REST + OpenAPI + MCP) │ │ │ +│ ├─────────────────────────────────────┤ │ │ +│ │ Public Plug-in API (api.v1.*) │◄──┤ loaded from │ +│ │ — the only stable contract │ │ ./plugins/*.jar │ +│ ├─────────────────────────────────────┤ │ via PF4J │ +│ │ Core PBCs (modular monolith): │ │ │ +│ │ identity · catalog · partners · │ │ │ +│ │ inventory · warehousing · │ │ │ +│ │ orders-sales · orders-purchase · │ │ │ +│ │ production · quality · finance │ │ │ +│ ├─────────────────────────────────────┤ │ │ +│ │ Cross-cutting: │ │ │ +│ │ • Flowable (workflows-as-data) │ │ │ +│ │ • Metadata store (Doctype-style) │ │ │ +│ │ • i18n (ICU MessageFormat) │ │ │ +│ │ • Reporting (JasperReports) │ │ │ +│ │ • Job scheduler (Quartz) │ │ │ +│ │ • Audit, security, events │ │ │ +│ └─────────────────────────────────────┘ │ │ +│ ▼ │ +│ PostgreSQL (mandatory) │ +│ File store (local or S3) │ +└──────────────────────────────────────────────────────────────────────┘ + +Optional sidecars for larger deployments (off by default): + • Keycloak (OIDC) • Redis (cache + queue) + • OpenSearch (search) • SMTP relay +``` + +The full architecture lives at [`docs/superpowers/specs/2026-04-07-vibe-erp-architecture-design.md`](docs/superpowers/specs/2026-04-07-vibe-erp-architecture-design.md). + +## Stack + +- Kotlin on the JVM, Spring Boot, single fat-JAR / single Docker image +- PostgreSQL (the only mandatory external dependency) +- Embedded Flowable (BPMN 2.0) for workflows-as-data +- PF4J + Spring Boot child contexts for plug-in classloader isolation +- ICU4J + Spring `MessageSource` for i18n +- JasperReports for reporting +- React + TypeScript SPA for the web client + +## Repository layout + +``` +vibe-erp/ +├── api/ +│ └── api-v1/ ← THE CONTRACT (semver, published to Maven Central) +├── platform/ ← Framework runtime (internal) +├── pbc/ ← Core PBCs (one Gradle subproject each) +├── reference-customer/ +│ └── plugin-printing-shop/ ← Reference plug-in, built and CI-tested, not loaded by default +├── web/ ← React + TypeScript SPA +├── docs/ ← Framework documentation +└── distribution/ ← Bootable assembly: fat JAR + Docker image +``` + +## Building + +This is a v0.1 skeleton. Not all modules ship yet — expect commands below to work for the modules that exist and to be no-ops or stubs for the rest. + +```bash +# Build everything that currently exists +./gradlew build + +# Run the bootable distribution against a local Postgres +./gradlew :distribution:bootRun + +# Or bring everything (Postgres + vibe_erp) up with Docker +docker compose up +``` + +## Status + +**v0.1** — buildable skeleton, one PBC implemented end-to-end (`pbc-identity`), plug-in loading proven with a hello-world reference plug-in. Not production. See [`docs/superpowers/specs/2026-04-07-vibe-erp-architecture-design.md`](docs/superpowers/specs/2026-04-07-vibe-erp-architecture-design.md) for the full v1.0 plan. + +## Documentation + +- [Documentation index](docs/index.md) +- [Architecture overview](docs/architecture/overview.md) +- [Plug-in API overview](docs/plugin-api/overview.md) +- [Plug-in author getting started](docs/plugin-author/getting-started.md) +- [Workflow authoring guide](docs/workflow-authoring/guide.md) +- [Form authoring guide](docs/form-authoring/guide.md) +- [i18n guide](docs/i18n/guide.md) +- [Customer onboarding guide](docs/customer-onboarding/guide.md) + +## License + +Apache License 2.0. See [`LICENSE`](LICENSE). diff --git a/docs/architecture/overview.md b/docs/architecture/overview.md new file mode 100644 index 0000000..31a206c --- /dev/null +++ b/docs/architecture/overview.md @@ -0,0 +1,283 @@ +# Architecture overview + +This document distills the vibe_erp architecture for someone who has not read the full spec. The complete design lives at [`../superpowers/specs/2026-04-07-vibe-erp-architecture-design.md`](../superpowers/specs/2026-04-07-vibe-erp-architecture-design.md). Read that when you need the full reasoning, the foundational decisions table, the v1.0 cut line, or the risk register. + +## What vibe_erp is + +vibe_erp is an **ERP/EBC framework** — not an ERP application — targeting the **printing industry**, sold worldwide and deployed self-hosted-first. The whole point of the project is **reusability across customers**: any printing shop, with its own workflows, forms, roles, and rules, should be onboardable by configuration and plug-ins, without forking the core. + +The reference printing-shop business documented under `raw/业务流程设计文档/` is **one example customer**, treated as a fixture and an executable acceptance test, never as a specification. No part of its workflow is hard-coded into the core. + +## Clean Core philosophy + +vibe_erp adopts SAP S/4HANA's **Clean Core** vocabulary: **extensions never modify the core**. The core is stable, generic, upgrade-safe, and domain-agnostic. Everything customer-specific lives in metadata rows or in plug-ins. This is enforced, not aspirational: + +1. **Core stays domain-agnostic.** No printing-specific entity, field, status, or workflow step belongs in the framework core. "Plate", "ink", "press", "color proof" live in plug-ins or in configuration. +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. +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. +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. +5. **Multi-tenant in spirit from day one.** Even single-tenant deployments use the same multi-tenant code path. +6. **Global / i18n from day one.** No hard-coded user-facing strings, currencies, date formats, time zones, address shapes, or tax models. + +## Two-tier extensibility + +vibe_erp offers **two extension paths**, both first-class. The same outcome can often be reached through either path; Tier 1 is preferred when expressive enough. + +### Tier 1 — Key user, no-code + +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. + +| Capability | Stored in | +|---|---| +| Custom field on an existing entity | `metadata__custom_field` → JSONB `ext` column at runtime | +| Custom form layout | `metadata__form` (JSON Schema + UI Schema) | +| Custom list view, filter, column set | `metadata__list_view` | +| Custom workflow | `metadata__workflow` → deployed to Flowable as BPMN | +| Simple "if X then Y" automation | `metadata__rule` | +| Custom entity (Doctype-style) | `metadata__entity` → auto-generated table at apply time | +| Custom report | `metadata__report` | +| Translations override | `metadata__translation` | + +No build, no restart, no deploy. The OpenAPI spec, the AI-agent function catalog, and the REST API auto-update from the metadata. + +### Tier 2 — Developer, pro-code + +Software developers ship a **PF4J plug-in JAR**. The plug-in: + +- Sees only `org.vibeerp.api.v1.*` — the public, semver-governed contract. +- Cannot import `org.vibeerp.platform.*` or any PBC's internal classes. +- Lives in its own classloader, its own Spring child context, its own DB schema namespace (`plugin___*`), its own metadata-source tag. +- Can register: new entities, REST endpoints, workflow tasks, form widgets, report templates, event listeners, permissions, menu entries, and React micro-frontends. + +### Extension grading + +Borrowed from SAP. The plug-in loader knows these grades and acts on them. + +| Grade | Definition | Upgrade safety | +|---|---|---| +| **A** | Tier 1 only (metadata) | Always safe across any core version | +| **B** | Tier 2, uses only `api.v1` stable surface | Safe within a major version | +| **C** | Tier 2, uses deprecated-but-supported `api.v1` symbols | Safe until next major; loader emits warnings | +| **D** | Tier 2, reaches into internal classes via reflection | UNSUPPORTED; loader rejects unless explicitly overridden; will break | + +A guiding principle: **anything a Tier 2 plug-in does should also become possible as Tier 1 over time.** Tier 2 is the escape hatch where Tier 1 is not yet expressive enough. + +## Modular monolith of PBCs + +vibe_erp is a **modular monolith**: one Spring Boot process per instance, one Docker image per release, but internally divided into strict bounded contexts called **Packaged Business Capabilities (PBCs)**. Each PBC is its own Gradle subproject, its own table-name prefix, its own bounded context, its own public API surface. + +The v1.0 core PBCs: + +``` +pbc-identity pbc-catalog pbc-partners +pbc-inventory pbc-warehousing +pbc-orders-sales pbc-orders-purchase +pbc-production pbc-quality pbc-finance +``` + +These names are **illustrative core capabilities**. None is printing-specific. The reference printing-shop plug-in lives entirely outside `pbc/` under `reference-customer/plugin-printing-shop/`, built and CI-tested but **not loaded by default**. + +### Per-PBC layout + +Every PBC follows the same shape: + +``` +pbc-orders-sales/ +├── api/ ← service contracts re-exported by api.v1 +├── domain/ ← entities, value objects, domain services +├── application/ ← use cases / application services +├── infrastructure/ ← Hibernate mappings, repositories +├── http/ ← REST controllers +├── workflow/ ← BPMN files, task handlers +├── metadata/ ← seed metadata (default forms, rules) +├── i18n/ ← message bundles +└── migrations/ ← Liquibase changesets (own table prefix) +``` + +## The dependency rule + +The single rule that makes "modular monolith now, splittable later" real instead of aspirational. Enforced by the Gradle build; CI fails on violations. + +``` +api/api-v1 depends on: nothing (Kotlin stdlib + jakarta.validation only) +platform/* depends on: api/api-v1 + Spring + libs +pbc/* depends on: api/api-v1 + platform/* (NEVER another pbc) +plugins (incl. ref) depend on: api/api-v1 only +``` + +**PBCs never import each other.** Cross-PBC interaction goes through one of two channels: + +1. **The event bus.** A PBC publishes a typed `DomainEvent`. Any other PBC (or any plug-in) subscribes via `EventListener`. The default bus is in-process; an outbox table in Postgres is the seam where Kafka or NATS can plug in later without changing PBC code. +2. **Service interfaces declared in `api.v1.ext.`.** When a synchronous call is genuinely needed, it goes through a typed interface in `api.v1.ext`, not through a direct class reference. + +If you find yourself wanting to add a `pbc-foo → pbc-bar` Gradle dependency, the seam is wrong. Design an event or an `api.v1.ext.bar` interface instead. + +## The `api.v1` contract + +`api.v1` is the **only stable contract** in the entire codebase. Everything in `org.vibeerp.api.v1.*` is binary-stable within the `1.x` line. Everything not in `api.v1` is internal and may change in any release. + +Package layout: + +``` +org.vibeerp.api.v1 +├── core/ Tenant, Locale, Money, Quantity, Id, Result +├── entity/ Entity, Field, FieldType, EntityRegistry +├── persistence/ Repository, Query, Page, Transaction +├── workflow/ WorkflowTask, WorkflowEvent, TaskHandler +├── form/ FormSchema, UiSchema +├── http/ @PluginEndpoint, RequestContext, ResponseBuilder +├── event/ DomainEvent, EventListener, EventBus +├── security/ Principal, Permission, PermissionCheck +├── i18n/ MessageKey, Translator, LocaleProvider +├── reporting/ ReportTemplate, ReportContext +├── plugin/ Plugin, PluginManifest, ExtensionPoint +└── ext/ Typed extension interfaces a plug-in implements +``` + +`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. + +### Upgrade contract + +| Change | Allowed within 1.x? | +|---|---| +| Add a class to `api.v1` | yes | +| Add a method to an `api.v1` interface (with default impl) | yes | +| Remove or rename anything in `api.v1` | no — major bump | +| Change behavior of an `api.v1` symbol in a way plug-ins can observe | no — major bump | +| Anything in `platform.*` or `pbc.*.internal.*` | yes — that is why it is internal | + +When in doubt, **keep things out of `api.v1`**. + +## Topology + +One Spring Boot process per instance, one Docker image per release, one mounted volume (`/opt/vibe-erp/`) for `config/`, `plugins/`, `i18n-overrides/`, `files/`, `logs/`. Customer extensions live **outside** the image. Upgrading vibe_erp = swapping the image; extensions and config stay put. + +``` +┌──────────────────────────────────────────────────────────────────────┐ +│ Customer's network │ +│ │ +│ Browser (React SPA) ─┐ │ +│ AI agent (MCP, v1.1)─┼─► Reverse proxy ──► vibe_erp backend (1 image)│ +│ 3rd-party system ─┘ │ │ +│ │ │ +│ Inside the image (one Spring Boot process): │ │ +│ ┌─────────────────────────────────────┐ │ │ +│ │ HTTP layer (REST + OpenAPI + MCP) │ │ │ +│ ├─────────────────────────────────────┤ │ │ +│ │ Public Plug-in API (api.v1.*) │◄──┤ loaded from │ +│ │ — the only stable contract │ │ ./plugins/*.jar │ +│ ├─────────────────────────────────────┤ │ via PF4J │ +│ │ Core PBCs (modular monolith) │ │ │ +│ ├─────────────────────────────────────┤ │ │ +│ │ Cross-cutting: │ │ │ +│ │ • Flowable (workflows-as-data) │ │ │ +│ │ • Metadata store (Doctype-style) │ │ │ +│ │ • i18n (ICU MessageFormat) │ │ │ +│ │ • Reporting (JasperReports) │ │ │ +│ │ • Job scheduler (Quartz) │ │ │ +│ │ • Audit, security, events │ │ │ +│ └─────────────────────────────────────┘ │ │ +│ ▼ │ +│ PostgreSQL (mandatory) │ +│ File store (local or S3) │ +└──────────────────────────────────────────────────────────────────────┘ +``` + +The only mandatory external dependency is **PostgreSQL**. Optional sidecars for larger deployments — Keycloak, Redis, OpenSearch, SMTP relay — are off by default. + +## Multi-tenancy + +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. + +### Schema namespacing + +PBCs and plug-ins use **table name prefixes**, not Postgres schemas: + +``` +identity__user, identity__role +catalog__item, catalog__item_attribute +inventory__stock_item, inventory__movement +orders_sales__order, orders_sales__order_line +production__work_order, production__operation +plugin_printingshop__plate_spec (reference plug-in) +metadata__custom_field, metadata__form, metadata__workflow +flowable_* (Flowable's own tables, untouched) +``` + +This keeps Hibernate, RLS policies, and migrations all in one logical schema (`public`), avoids `search_path` traps, and gives clean uninstall semantics. + +### Tenant isolation — two independent walls + +- Every business table has `tenant_id`, NOT NULL. +- Hibernate `@TenantId` filters every query at the application layer. +- Postgres Row-Level Security policies filter every query at the database layer. + +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.** + +### Data sovereignty + +- Self-hosted is automatically compliant — the customer chose where Postgres lives. +- Hosted supports per-region tenant routing: each tenant row carries a region; connections are routed to the right regional Postgres cluster. +- PII tagging on field metadata drives auto-generated DSAR exports and erasure jobs (GDPR Articles 15/17). +- Append-only audit log records access to PII fields when audit-strict mode is on. + +## Custom fields via JSONB + +Every business table has: + +```sql +ext jsonb not null default '{}', +ext_meta text generated +``` + +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. + +Why JSONB and not EAV: one row, one read, indexable, no migrations needed for additions, no joins. EAV is the wrong tool. + +For the rare hot-path custom field, an operator can promote a JSON key to a real generated column via an auto-generated Liquibase changeset. This is an optimization, not the default. + +## The metadata store + +``` +metadata__entity metadata__form metadata__permission +metadata__custom_field metadata__list_view metadata__role_permission +metadata__workflow metadata__rule metadata__menu +metadata__report metadata__translation metadata__plugin_config +``` + +Every row carries `tenant_id`, `source` (`core` / `plugin:` / `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. + +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. + +## The workflow engine + +vibe_erp embeds **Flowable** (BPMN 2.0). Workflows are data: `.bpmn` files at design time, `flowable_*` tables at runtime. Two authoring paths exist, both first-class: + +- **Tier 1 (visual designer in the web UI, v1.0):** business analysts draw workflows in a BPMN designer. The result is stored as a row in `metadata__workflow`, deployed to Flowable, and tagged `source = 'user'`. +- **Tier 2 (`.bpmn` files in a plug-in JAR):** plug-in authors ship BPMN files in their JAR. The plug-in lifecycle deploys them on plug-in install. + +Workflows interact with the system through: + +- **Service tasks** that call typed `TaskHandler` implementations. A plug-in registers a `TaskHandler` via `@Extension(point = TaskHandler::class)`; the workflow references it by id. There is no scripting language to invent — the `TaskHandler` is just Kotlin code behind a typed interface. +- **User tasks** that render forms from `metadata__form` definitions. The form is referenced by id; rendering goes through the same code path used by Tier 1 forms. + +The temptation to invent a vibe_erp-only workflow language must be rejected. BPMN 2.0 via Flowable is the standard, and the standard is the contract. + +## Cross-cutting concerns + +| Concern | Approach | +|---|---| +| Security | `PermissionCheck` declared in `api.v1.security`; plug-ins register their own permissions, auto-listed in the role editor | +| Transactions | Spring `@Transactional` at the application-service layer; plug-ins use `api.v1.persistence.Transaction`, never Spring directly | +| Audit | `created_at`, `created_by`, `updated_at`, `updated_by`, `tenant_id` on every entity, applied by a JPA listener | +| 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 | +| 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 | +| Reporting | JasperReports; templates shipped by core or by plug-ins, customer-skinnable | +| i18n | ICU MessageFormat, locale-aware formatting for dates, numbers, and currencies; no string concatenation in user-facing code | + +## Where to read more + +- The full architecture spec: [`../superpowers/specs/2026-04-07-vibe-erp-architecture-design.md`](../superpowers/specs/2026-04-07-vibe-erp-architecture-design.md) +- The architectural guardrails (the rules that exist because reusability across customers is the entire point of the project): `CLAUDE.md` at the repo root. +- Plug-in API surface: [`../plugin-api/overview.md`](../plugin-api/overview.md) +- Building your first plug-in: [`../plugin-author/getting-started.md`](../plugin-author/getting-started.md) diff --git a/docs/customer-onboarding/guide.md b/docs/customer-onboarding/guide.md new file mode 100644 index 0000000..f6519fc --- /dev/null +++ b/docs/customer-onboarding/guide.md @@ -0,0 +1,135 @@ +# Customer onboarding guide + +This guide is for an **integrator** standing up vibe_erp for a new customer end-to-end. It is written from a Tier 1 perspective: no plug-in code is required to follow it. Where Tier 2 plug-ins are useful, the guide says so. + +For the architectural background, see [`../architecture/overview.md`](../architecture/overview.md). For the full v1.0 cut line, see section 11 of [`../superpowers/specs/2026-04-07-vibe-erp-architecture-design.md`](../superpowers/specs/2026-04-07-vibe-erp-architecture-design.md). + +## Honest scope note + +vibe_erp is a framework, not a turnkey product. **The Tier 1 customization UIs (custom field designer, form designer, BPMN designer, list view editor, role editor) are v1.0 deliverables.** v0.1 ships only the underlying API surface — the metadata tables, the REST endpoints, the seed data — without the polished UIs on top of them. Until v1.0 ships, the steps below that say "use the Customize UI" mean "POST to the metadata REST endpoints" or "edit a YAML seed file in your plug-in JAR". + +This guide is written against the v1.0 experience. Where v0.1 deviates, the step is annotated **(v0.1: API only)**. + +## 1. Install the host + +vibe_erp ships as a single Docker image with PostgreSQL as the only mandatory external dependency. + +```bash +docker run -d --name vibe-erp \ + -p 8080:8080 \ + -v /srv/vibeerp:/opt/vibe-erp \ + -e DB_URL=jdbc:postgresql://db.internal:5432/vibeerp \ + -e DB_USER=vibeerp \ + -e DB_PASSWORD=... \ + ghcr.io/vibeerp/vibe-erp:1.0.0 +``` + +What happens on first boot: + +1. The host connects to Postgres. +2. Liquibase runs every core PBC's migrations and creates the `flowable_*` tables. +3. A `default` tenant row is created in `identity__tenant`. +4. A bootstrap admin user is created and its one-time password is printed to the boot log. +5. The host is ready in under 30 seconds. + +The mounted volume `/srv/vibeerp` (mapped to `/opt/vibe-erp` inside the container) holds: + +``` +/opt/vibe-erp/ +├── config/vibe-erp.yaml single config file (closed key set) +├── plugins/ drop *.jar to install +├── i18n-overrides/ tenant-level translation overrides +├── files/ file store (if not using S3) +└── logs/ +``` + +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. + +## 2. Log in as the bootstrap admin + +Open `http://: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. + +## 3. Create a tenant (hosted only) + +For a self-hosted single-customer deployment, the `default` tenant is everything you need. Skip this step. + +For a hosted multi-tenant deployment, create one tenant row per customer. Each tenant carries: + +- A unique tenant id. +- A region (used by the per-region routing layer in hosted mode). +- A default locale, currency, and time zone. + +Tenant onboarding is an `INSERT` plus seed metadata, not a migration — sub-second per tenant. + +## 4. Use the Customize UI to model the customer's reality + +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. **(v0.1: API only — POST to the metadata endpoints; the UIs ship in v1.0.)** + +### Custom fields + +Add custom fields to existing entities (item, partner, order, work order, …). Each custom field is a row in `metadata__custom_field` and a JSON key in the entity's `ext` JSONB column at runtime. A GIN index on `ext` keeps custom fields queryable. The form designer, list views, OpenAPI spec, and AI-agent function catalog all auto-update from the metadata. + +For the rare hot-path field, an operator can promote a JSON key to a real generated column via an auto-generated Liquibase changeset. This is an optimization, not the default. + +### Custom forms + +Define how each entity is displayed and edited. Forms are JSON Schema (data shape) plus UI Schema (layout and widgets), stored in `metadata__form`. Forms can reference custom fields by their key. See the [form authoring guide](../form-authoring/guide.md). + +### Custom workflows + +Draw the customer's process flow in the BPMN designer. Workflows are stored in `metadata__workflow` and deployed to the embedded Flowable engine. User tasks render forms; service tasks call typed `TaskHandler` implementations registered by plug-ins. See the [workflow authoring guide](../workflow-authoring/guide.md). + +### Custom list views, rules, menus, reports + +- **List views** (`metadata__list_view`): which columns, which filters, which default sort. +- **Rules** (`metadata__rule`): simple "if X then Y" automations for the cases that do not warrant a full BPMN workflow. +- **Menus** (`metadata__menu`): the navigation tree the user sees. +- **Reports** (`metadata__report`): JasperReports templates the customer can run on demand or on a schedule. + +## 5. Import master data + +vibe_erp accepts master data through two paths: + +- **CSV import** through the Customize UI for one-off bulk loads (catalog items, partners, opening stock, chart of accounts). **(v0.1: API only.)** +- **REST API** for ongoing integration with the customer's existing systems. Every entity, including custom fields, is exposed through OpenAPI-documented REST endpoints. The OpenAPI spec auto-updates as you add custom fields and custom entities, so the integration code is never out of date. + +## 6. Configure roles and permissions + +Define the customer's roles (e.g. *sales clerk*, *production planner*, *warehouse operator*, *finance reviewer*) and assign permissions. Permissions are auto-discovered from: + +- Core PBCs. +- Plug-ins (each plug-in registers its own permissions through `api.v1.security.PermissionCheck`). +- Custom entities (each generates a standard CRUD permission set). + +Bind users to roles, bind roles to tenants. Hosted deployments can also bind roles via OIDC group claims. + +## 7. Add Tier 2 plug-ins as needed + +When Tier 1 metadata is not expressive enough — for example, a printing-specific entity like *plate spec* with its own validation logic, or a workflow service task that calls an external MIS — install a Tier 2 plug-in. + +To install a plug-in: + +1. Drop the JAR into `/opt/vibe-erp/plugins/`. +2. Restart the host (`docker restart vibe-erp`). Hot reload is on the v1.2+ roadmap. +3. The plug-in loader scans the JAR, validates the manifest, runs the plug-in linter, runs the plug-in's Liquibase migrations in `plugin___*`, seeds the plug-in's metadata, and starts the plug-in. +4. Verify the plug-in shows up under `/actuator/health` and in the boot log. + +The reference printing-shop plug-in (`reference-customer/plugin-printing-shop/`) is a worked example of a non-trivial Tier 2 plug-in. It expresses the workflows in `raw/业务流程设计文档/` using **only `api.v1`** and is built and CI-tested on every PR. It is **not** loaded by default — drop its JAR into `./plugins/` to use it. + +A guiding principle: **anything a Tier 2 plug-in does should also become possible as Tier 1 over time.** Tier 2 is the escape hatch where Tier 1 is not yet expressive enough. When you find yourself reaching for a plug-in to do something a metadata row could express, file an issue against the framework. + +## 8. Go-live checklist + +- [ ] Backups configured against the Postgres instance. Customer data lives there exclusively (plus the file store). +- [ ] Audit log enabled for PII fields if the customer is subject to GDPR or equivalent. +- [ ] DSAR export and erasure jobs tested against a non-production tenant. +- [ ] Locale, currency, time zone defaults set for the tenant. +- [ ] OIDC integration tested with the customer's identity provider. +- [ ] Health check (`/actuator/health`) wired into the customer's monitoring. +- [ ] Documented upgrade procedure for the operator: `docker rm` plus `docker run` with the new image tag, plug-ins and config stay put. + +## What this guide deliberately does not cover + +- **Plug-in development.** That is the [plug-in author getting-started guide](../plugin-author/getting-started.md). +- **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. +- **MCP / AI-agent endpoint.** v1.1 deliverable. The seam exists in v1.0; the endpoint does not. diff --git a/docs/form-authoring/guide.md b/docs/form-authoring/guide.md new file mode 100644 index 0000000..055d667 --- /dev/null +++ b/docs/form-authoring/guide.md @@ -0,0 +1,110 @@ +# Form authoring guide + +Forms in vibe_erp describe how an entity is displayed and edited. They are **data, not code** — stored as `metadata__form` rows, consumed by one renderer, and used identically inside and outside workflows. + +For where forms sit in the architecture, see [`../architecture/overview.md`](../architecture/overview.md). For the workflow side, see the [workflow authoring guide](../workflow-authoring/guide.md). + +## JSON Schema + UI Schema + +A vibe_erp form definition has two halves: + +- **JSON Schema** describes the **data shape**: which fields exist, their types, which are required, what their validation constraints are. +- **UI Schema** describes the **layout and widgets**: which field renders as a dropdown vs. a radio group, which fields are grouped under which tab, which fields are read-only, which are conditionally visible. + +The split matters. The JSON Schema is the contract — server-side validation, OpenAPI generation, and the AI-agent function catalog all read it. The UI Schema is presentation — only the renderer reads it. Two forms can share a JSON Schema but render differently. + +A form definition looks roughly like this (illustrative): + +```json +{ + "id": "orders_sales.order.create", + "schema": { + "type": "object", + "required": ["customerId", "lines"], + "properties": { + "customerId": { "type": "string", "format": "uuid" }, + "deliveryDate": { "type": "string", "format": "date" }, + "lines": { + "type": "array", + "minItems": 1, + "items": { + "type": "object", + "required": ["itemId", "quantity"], + "properties": { + "itemId": { "type": "string", "format": "uuid" }, + "quantity": { "type": "number", "exclusiveMinimum": 0 } + } + } + } + } + }, + "uiSchema": { + "type": "VerticalLayout", + "elements": [ + { "type": "Control", "scope": "#/properties/customerId", "options": { "widget": "partner-picker" } }, + { "type": "Control", "scope": "#/properties/deliveryDate" }, + { "type": "Control", "scope": "#/properties/lines", "options": { "widget": "line-items-grid" } } + ] + } +} +``` + +## Two authoring paths + +Both paths are first-class. The renderer does not know which one produced a given form. + +### Tier 1 — Form designer in the web UI (v1.0) + +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. + +This path is the right one for: + +- Tweaking the layout of an existing entity for a specific tenant. +- Adding a tenant-specific custom field to an existing form. +- Building a form for a Tier 1 custom entity. + +The form designer is a **v1.0 deliverable**. + +### Tier 2 — JSON files in a plug-in JAR (v0.1) + +Plug-in authors ship form definitions inside their JAR under `src/main/resources/metadata/forms/`. The plug-in lifecycle upserts them into `metadata__form` on plug-in install, tagged `source = 'plugin:'`. + +``` +src/main/resources/ +├── plugin.yml +└── metadata/ + └── forms/ + ├── plate_spec.json + └── job_card_review.json +``` + +```yaml +# plugin.yml +metadata: + forms: + - metadata/forms/plate_spec.json + - metadata/forms/job_card_review.json +``` + +This path is the right one for forms that ship as part of a vertical plug-in (the printing-shop plug-in is the canonical example). + +## Forms can reference custom fields + +A form can reference any custom field defined for the same entity by its key. Because custom fields live as JSON keys in the entity's `ext` JSONB column and are described by `metadata__custom_field` rows, the form designer and the form renderer both already know about them — no additional wiring required. + +The key is the same one the customer chose when creating the custom field. If a printing-shop integrator added a `coating_finish` custom field to the `production.work_order` entity, a form referencing `ext.coating_finish` will render and validate it correctly, whether the form was authored through the Tier 1 designer or shipped inside a plug-in JAR. + +## Validation runs on both sides + +Validation happens in two places, and both come from the same JSON Schema: + +- **Client-side** in the renderer, driven by the UI Schema's widgets and the JSON Schema constraints. This is the responsive, immediate-feedback layer. +- **Server-side** in the host, driven by the JSON Schema. This is the **authoritative** layer. The renderer's checks are a convenience; the server's checks are the contract. + +A form submission that bypasses the UI (a script, an integration, an AI agent calling through MCP) is validated against exactly the same JSON Schema as a form filled in through the SPA. There is no path that skips validation. + +## Where to go next + +- Workflows that render forms in user tasks: [`../workflow-authoring/guide.md`](../workflow-authoring/guide.md) +- Plug-in author walkthrough: [`../plugin-author/getting-started.md`](../plugin-author/getting-started.md) +- Plug-in API surface, including `api.v1.form`: [`../plugin-api/overview.md`](../plugin-api/overview.md) diff --git a/docs/i18n/guide.md b/docs/i18n/guide.md new file mode 100644 index 0000000..1a5d633 --- /dev/null +++ b/docs/i18n/guide.md @@ -0,0 +1,135 @@ +# i18n guide + +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. + +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). + +## ICU MessageFormat is the backbone + +vibe_erp uses **ICU MessageFormat** (via ICU4J) on top of Spring's `MessageSource`. ICU MessageFormat handles the things naive `printf`-style formatting cannot: + +- Plurals (`{count, plural, one {# item} other {# items}}`) +- Gender selection +- Locale-aware number, date, and currency formatting +- Nested arguments and select clauses + +A message in a bundle file looks like this: + +```properties +orders_sales.cart.summary = {count, plural, =0 {Your cart is empty.} one {# item, total {total, number, currency}} other {# items, total {total, number, currency}}} +``` + +The same key produces correct output in English, German, Japanese, Chinese, and Spanish without changing a line of calling code. + +## Plug-ins ship message bundles + +Each plug-in (and each PBC) ships its message bundles inside its JAR under `i18n/.properties`. The locale tag follows BCP 47: + +``` +src/main/resources/ +└── i18n/ + ├── en-US.properties + ├── zh-CN.properties + ├── de-DE.properties + ├── ja-JP.properties + └── es-ES.properties +``` + +The `plugin.yml` manifest lists the bundles so the loader knows to pick them up: + +```yaml +metadata: + i18n: + - i18n/en-US.properties + - i18n/de-DE.properties +``` + +## How the host merges bundles + +On boot, and on every plug-in install, the host builds a merged `MessageSource` per locale. The merge order, from lowest precedence to highest: + +1. **Core bundles** shipped inside the vibe_erp image. +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. +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. + +``` +tenant overrides ← highest precedence + plug-in bundles + core bundles ← lowest precedence +``` + +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. + +## Shipping locales for v1.0 + +vibe_erp v1.0 ships with five locales: + +| Locale tag | Language | +|---|---| +| `en-US` | English (United States) | +| `zh-CN` | Chinese (Simplified) | +| `de-DE` | German | +| `ja-JP` | Japanese | +| `es-ES` | Spanish (Spain) | + +Adding a sixth locale is a Tier 1 operation: drop a `.properties` file into `i18n-overrides/` on the mounted volume, or add `metadata__translation` rows through the customization UI. No rebuild. + +The reference business documentation under `raw/业务流程设计文档/` is in Chinese. **That does not make Chinese the default.** Chinese is one supported locale among the five. + +## Translation key naming + +Translation keys follow `..`: + +``` +identity.login.title +identity.login.error.invalid_credentials +catalog.item.field.sku.label +orders_sales.order.status.confirmed +plugin_printingshop.plate.field.thickness.label +``` + +Rules: + +- The first segment is the PBC table prefix (`identity`, `catalog`, `orders_sales`, …) or, for a plug-in, `plugin_`. This is the same prefix used in the database, the metadata `source` column, and the OpenAPI tag. +- Segments are `snake_case`. +- The last segment names the message itself, not the widget that renders it. +- Two plug-ins can never collide: their first segment differs by definition. + +## The `Translator` is the only sanctioned way + +The host injects a `Translator` (from `org.vibeerp.api.v1.i18n`) into every plug-in endpoint, every workflow task handler, every event listener. **There is no string concatenation in user-facing code, anywhere, ever.** + +```kotlin +val message = translator.format( + MessageKey("orders_sales.order.created.notification"), + mapOf( + "orderNumber" to order.number, + "customerName" to order.customer.name, + "total" to order.total, + ), +) +``` + +What this means in practice: + +- A reviewer who sees `"Order " + number + " was created"` in a PR rejects the PR. +- A reviewer who sees a `String.format` against a hard-coded format string rejects the PR. +- A reviewer who sees `if (locale == "en") "..." else "..."` rejects the PR with prejudice. + +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. + +## PII, dates, numbers, currency + +Locale handling does not stop at strings: + +- **Dates and times** are formatted through the locale, not through hard-coded patterns. The host exposes locale-aware formatters; plug-ins use them. +- **Numbers** use the locale's grouping and decimal separators. +- **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`). +- **Time zones** are per-tenant and per-user, not per-server. +- **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. +- **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. + +## Where to go next + +- Plug-in author walkthrough that uses `Translator`: [`../plugin-author/getting-started.md`](../plugin-author/getting-started.md) +- Plug-in API surface for i18n: [`../plugin-api/overview.md`](../plugin-api/overview.md) diff --git a/docs/index.md b/docs/index.md new file mode 100644 index 0000000..66723dd --- /dev/null +++ b/docs/index.md @@ -0,0 +1,15 @@ +# vibe_erp documentation + +vibe_erp is an ERP/EBC framework for the printing industry, designed so customer-specific workflows are assembled by configuration and plug-ins instead of by forking the core. This index is the entry point to the framework's own documentation. The full architecture spec lives at [`superpowers/specs/2026-04-07-vibe-erp-architecture-design.md`](superpowers/specs/2026-04-07-vibe-erp-architecture-design.md) and is the source of truth for everything in this tree. + +## Guides + +| Guide | What it covers | +|---|---| +| [Architecture overview](architecture/overview.md) | Clean Core philosophy, two-tier extensibility, PBCs, the dependency rule, multi-tenancy, custom fields, the workflow engine. | +| [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. | +| [Plug-in author getting started](plugin-author/getting-started.md) | A concrete walkthrough for building, packaging, and loading your first vibe_erp plug-in. | +| [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). | +| [Form authoring guide](form-authoring/guide.md) | JSON Schema + UI Schema forms: visual designer (Tier 1) and JSON files in plug-ins (Tier 2). | +| [i18n guide](i18n/guide.md) | ICU MessageFormat, message bundles, locale resolution, tenant overrides, shipping locales. | +| [Customer onboarding guide](customer-onboarding/guide.md) | Integrator's checklist for standing up a new vibe_erp customer end-to-end. | diff --git a/docs/plugin-api/overview.md b/docs/plugin-api/overview.md new file mode 100644 index 0000000..a8ad122 --- /dev/null +++ b/docs/plugin-api/overview.md @@ -0,0 +1,90 @@ +# Plug-in API overview + +`org.vibeerp.api.v1` (`api.v1` for short) is the **only stable contract** in vibe_erp. It is the surface that PF4J plug-ins compile against, and the only surface they are permitted to touch. Everything else in the codebase — `platform.*`, every PBC's internal package, every concrete Spring bean — is internal and may change in any release. + +For the architectural reasoning behind `api.v1`, 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). + +## What `api.v1` is + +- A Kotlin module published to **Maven Central** as `org.vibeerp:api-v1`. +- Depends on **only** Kotlin stdlib and `jakarta.validation`. No Spring, no Hibernate, no PF4J types leak through it. +- Semver-governed on the `1.x` line. Across a major version, `api.v1` and `api.v2` ship side by side for at least one major release window so plug-ins have time to migrate. +- The single import surface for plug-ins. The plug-in linter rejects any import outside `org.vibeerp.api.v1.*` at install time. + +## Package layout + +``` +org.vibeerp.api.v1 +├── core/ Tenant, Locale, Money, Quantity, Id, Result +├── entity/ Entity, Field, FieldType, EntityRegistry +├── persistence/ Repository, Query, Page, Transaction +├── event/ DomainEvent, EventListener, EventBus +├── security/ Principal, Permission, PermissionCheck +├── i18n/ MessageKey, Translator, LocaleProvider +├── http/ @PluginEndpoint, RequestContext, ResponseBuilder +├── plugin/ Plugin, PluginManifest, ExtensionPoint +├── ext/ Typed extension interfaces a plug-in implements +├── workflow/ WorkflowTask, WorkflowEvent, TaskHandler +└── form/ FormSchema, UiSchema +``` + +A short orientation: + +| Package | What it gives you | +|---|---| +| `core/` | The primitive value types every plug-in needs: `Tenant`, `Locale`, `Money`, `Quantity`, typed `Id`, `Result`. No printing concepts. | +| `entity/` | Declarative entity model. Plug-ins describe entities, fields, and field types through `EntityRegistry`; the platform handles persistence and OpenAPI. | +| `persistence/` | `Repository`, `Query`, `Page`, `Transaction`. Plug-ins never see Hibernate or Spring `@Transactional` directly. | +| `event/` | `DomainEvent`, `EventListener`, `EventBus`. The primary cross-PBC and cross-plug-in communication channel. | +| `security/` | `Principal`, `Permission`, `PermissionCheck`. Plug-ins register their own permissions; the role editor auto-discovers them. | +| `i18n/` | `MessageKey`, `Translator`, `LocaleProvider`. The only sanctioned way for a plug-in to produce user-facing text. | +| `http/` | `@PluginEndpoint` and the request/response abstractions for adding REST endpoints from a plug-in. | +| `plugin/` | `Plugin`, `PluginManifest`, `ExtensionPoint`, and the `@Extension` annotation. The plug-in lifecycle entry points. | +| `ext/` | Typed extension interfaces that PBCs declare and plug-ins implement (e.g. `api.v1.ext.inventory.StockReservationStrategy`). The cross-PBC interaction surface. | +| `workflow/` | `WorkflowTask`, `WorkflowEvent`, `TaskHandler`. The hooks BPMN service tasks call into. | +| `form/` | `FormSchema`, `UiSchema`. JSON Schema and UI Schema as Kotlin types, for plug-ins shipping form definitions. | + +## The stability contract + +| Change | Allowed within 1.x? | +|---|---| +| Add a class to `api.v1` | yes | +| Add a method to an `api.v1` interface (with default impl) | yes | +| Remove or rename anything in `api.v1` | no — major bump | +| Change behavior of an `api.v1` symbol in a way plug-ins can observe | no — major bump | +| Anything in `platform.*` or `pbc.*.internal.*` | yes — that is why it is internal | + +Practical consequences for plug-in authors: + +- A plug-in built against `api.v1` version `1.4.0` will load in any vibe_erp `1.x` release. +- A symbol that gets deprecated in `1.x` keeps working until `2.0`. The plug-in loader emits a warning when a plug-in uses a deprecated symbol — that is the **Grade C** signal in the extension grading. +- A plug-in that reaches into `platform.*` or `pbc.*.internal.*` via reflection is **Grade D**, unsupported, and rejected by the plug-in linter at install time. + +When in doubt about whether something belongs in `api.v1`: **keep it out**. Growing the API deliberately later is much cheaper than maintaining a regretted symbol forever. + +## The A/B/C/D extension grading + +From CLAUDE.md guardrail #7. Every extension to vibe_erp falls into one of four grades, ordered from safest to least safe. + +| Grade | What it is | Upgrade safety | +|---|---|---| +| **A** | Tier 1 metadata only — custom fields, forms, workflows, rules, list views, translations entered through the web UI. Stored as rows in `metadata__*` tables, tagged `source = 'user'`. | Always upgrade-safe across **any** core version. | +| **B** | Tier 2 plug-in using only the public `api.v1` surface. | Safe within a major version. Loads cleanly across every `1.x` release. | +| **C** | Tier 2 plug-in using deprecated-but-supported `api.v1` symbols. | Safe until the next major. The plug-in loader emits warnings; the plug-in author should migrate before the next major release. | +| **D** | Tier 2 plug-in reaching into `platform.*` or `pbc.*.internal.*` via reflection. | UNSUPPORTED. The plug-in linter rejects this at install time. Will break on the next core upgrade. | + +Two principles follow from the grading: + +1. Anything a Tier 2 plug-in does should also become possible as a Tier 1 customization over time. Tier 2 is the escape hatch, not the default. +2. If you find yourself wanting to do Grade D, the seam is wrong and `api.v1` needs to grow — deliberately, with a version bump consideration. + +## Reference + +The full reference for every type, method, and contract in `api.v1` is generated from KDoc by **Dokka** and published alongside each release. This document is the conceptual overview; the generated reference is the authoritative per-symbol documentation. (The Dokka site is wired up as part of the v1.0 documentation deliverable; until then, the source under `api/api-v1/src/main/kotlin/org/vibeerp/api/v1/` is the source of truth, and every public type carries a KDoc block — that is part of the PR checklist in [`../../CONTRIBUTING.md`](../../CONTRIBUTING.md).) + +## Where to go next + +- Build your first plug-in: [`../plugin-author/getting-started.md`](../plugin-author/getting-started.md) +- Author a workflow: [`../workflow-authoring/guide.md`](../workflow-authoring/guide.md) +- Author a form: [`../form-authoring/guide.md`](../form-authoring/guide.md) +- Localize a plug-in: [`../i18n/guide.md`](../i18n/guide.md) diff --git a/docs/plugin-author/getting-started.md b/docs/plugin-author/getting-started.md new file mode 100644 index 0000000..6350f33 --- /dev/null +++ b/docs/plugin-author/getting-started.md @@ -0,0 +1,204 @@ +# 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`](../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). + +## 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`): + +```kotlin +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. + +```kotlin +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. + +```yaml +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.`. + +```kotlin +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("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. + +```kotlin +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](../i18n/guide.md). +- 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: + +```bash +./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: + +```bash +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___*`, 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). + +## 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`](../superpowers/specs/2026-04-07-vibe-erp-architecture-design.md). diff --git a/docs/superpowers/specs/2026-04-07-vibe-erp-implementation-plan.md b/docs/superpowers/specs/2026-04-07-vibe-erp-implementation-plan.md new file mode 100644 index 0000000..7de6d4c --- /dev/null +++ b/docs/superpowers/specs/2026-04-07-vibe-erp-implementation-plan.md @@ -0,0 +1,373 @@ +# vibe_erp — Implementation Plan (post-v0.1) + +**Status:** Approved roadmap. v0.1 skeleton is built. This document is the source of truth for the work that turns v0.1 into v1.0. +**Date:** 2026-04-07 +**Companion document:** [Architecture spec](2026-04-07-vibe-erp-architecture-design.md) + +--- + +## What v0.1 (this commit) actually delivers + +A buildable, runnable, tested skeleton of the vibe_erp framework. Concretely: + +| Item | Status | +|---|---| +| Gradle multi-project with 7 subprojects | ✅ Builds | +| `api/api-v1` — public plug-in contract (~30 files, semver-governed) | ✅ Compiles, unit-tested | +| `platform/platform-bootstrap` — Spring Boot main, config, tenant filter | ✅ Compiles | +| `platform/platform-persistence` — multi-tenant JPA scaffolding, audit base | ✅ Compiles | +| `platform/platform-plugins` — PF4J host, lifecycle | ✅ Compiles | +| `pbc/pbc-identity` — User entity end-to-end (entity → repo → service → REST) | ✅ Compiles, unit-tested | +| `reference-customer/plugin-printing-shop` — hello-world PF4J plug-in | ✅ Compiles, packs into a real plug-in JAR with PF4J manifest | +| `distribution` — Spring Boot fat jar (`vibe-erp.jar`, ~59 MB) | ✅ `bootJar` succeeds | +| Liquibase changelogs (platform init + identity init) | ✅ Schema-valid, runs against Postgres on first boot | +| Dockerfile (multi-stage), docker-compose.yaml (app + Postgres), Makefile | ✅ Present, image build untested in this session | +| GitHub Actions workflows (build + architecture-rule check) | ✅ Present, untested in CI | +| Architecture-rule enforcement in `build.gradle.kts` | ✅ Active — fails the build if a PBC depends on another PBC, or if a plug-in depends on `platform/*` | +| README, CONTRIBUTING, LICENSE, 8 docs guides | ✅ Present | +| Architecture spec (this folder) | ✅ Present | +| Implementation plan (this file) | ✅ Present | + +**What v0.1 explicitly does NOT deliver:** + +- The 9 other core PBCs (catalog, partners, inventory, warehousing, orders-sales, orders-purchase, production, quality, finance) +- The metadata store machinery (custom-field application, form designer, list views, rules engine) +- Embedded Flowable workflow engine +- ICU4J `Translator` implementation +- Postgres Row-Level Security `SET LOCAL` hook (the policies exist; the hook that sets `vibeerp.current_tenant` per transaction is deferred) +- JasperReports +- OIDC integration +- React web SPA +- Plug-in linter (rejecting reflection into platform internals at install time) +- Plug-in Liquibase application +- Per-plug-in Spring child context +- MCP server +- Mobile app (v2 anyway) + +The architecture **accommodates** all of these — the seams are in `api.v1` already — they just aren't implemented. + +--- + +## How to read this plan + +Each work unit is sized to be a single coherent implementation pass: roughly 1–5 days of work, one developer, one PR. Units are grouped by theme, ordered roughly by dependency, but parallelizable wherever the prerequisites are satisfied. + +Each unit names: +- the **Gradle modules** it touches (or creates) +- the **api.v1 surface** it consumes (and never modifies, unless explicitly noted) +- the **acceptance test** that proves it landed correctly +- any **upstream dependencies** that must complete first + +The 11 architecture guardrails in `CLAUDE.md` and the 14-section design in `2026-04-07-vibe-erp-architecture-design.md` apply to every unit. This document does not restate them. + +--- + +## Phase 1 — Platform completion (the foundation everything else needs) + +These units finish the platform layer so PBCs can be implemented without inventing scaffolding. + +### P1.1 — Postgres RLS transaction hook +**Module:** `platform-persistence` +**Why:** v0.1 enables RLS policies but never sets `current_setting('vibeerp.current_tenant')`. Without the hook, RLS-enabled tables return zero rows. **Block on this for any read/write integration test.** +**What:** A Hibernate `StatementInspector` (or a Spring `TransactionSynchronization`) that runs `SET LOCAL vibeerp.current_tenant = ''` at the start of every transaction, reading from `TenantContext`. +**Acceptance:** integration test with Testcontainers Postgres, two tenants, one user per tenant, asserts that tenant A's queries return zero rows from tenant B's data even when the Hibernate filter is bypassed. + +### P1.2 — Plug-in linter +**Module:** `platform-plugins` +**Depends on:** — +**What:** At plug-in load time, scan the JAR's class files for any reference to `org.vibeerp.platform.*` or `org.vibeerp.pbc.*.internal.*` packages. If any are found, refuse to load and report a "grade D extension" error to the operator. Reuse ASM (already a transitive dep of Spring) for bytecode inspection. +**Acceptance:** unit test with a hand-crafted bad JAR that imports a fictitious `org.vibeerp.platform.foo.Bar` — the loader rejects it with a clear message. + +### P1.3 — Per-plug-in Spring child context +**Module:** `platform-plugins` +**Depends on:** P1.2 (linter must run first) +**What:** When a plug-in starts, create a Spring `AnnotationConfigApplicationContext` with the host context as parent. Register the plug-in's `@Component` classes (discovered via classpath scanning of its classloader). The plug-in's `Plugin.start(context: PluginContext)` is invoked with a `PluginContext` whose dependencies are wired from the parent context. +**Acceptance:** the reference printing-shop plug-in declares an `@Extension` and a `@PluginEndpoint` controller; the integration test confirms the endpoint is mounted under `/api/v1/plugins/printing-shop/...` and the extension is invocable. + +### P1.4 — Plug-in Liquibase application +**Module:** `platform-plugins` + `platform-persistence` +**Depends on:** P1.3 +**What:** When a plug-in starts, look for `db/changelog/master.xml` in its JAR; if present, run it through Liquibase against the host's datasource with the plug-in's id as the changelog `contexts` filter. Tables created live under the `plugin___*` prefix (enforced by lint of the changelog at load time, not runtime). +**Acceptance:** a test plug-in ships a changelog that creates `plugin_test__widget`; after load, the table exists. Uninstall removes the table after operator confirmation. + +### P1.5 — Metadata store seeding +**Module:** new `platform-metadata` module (or expand `platform-persistence`) +**Depends on:** — +**What:** A `MetadataLoader` Spring component that, on boot and on plug-in start, upserts rows into `metadata__entity`, `metadata__form`, `metadata__workflow`, etc. from YAML files in the classpath / plug-in JAR. Each row carries `source = 'core' | 'plugin:' | 'user'`. Idempotent; safe to run on every boot. +**Acceptance:** integration test confirms that booting twice doesn't duplicate rows; uninstalling a plug-in removes its `source = 'plugin:'` rows; user-edited rows (`source = 'user'`) are never touched. + +### P1.6 — `Translator` implementation backed by ICU4J +**Module:** new `platform-i18n` module +**Depends on:** P1.5 (so plug-in message bundles can be loaded as part of metadata) +**What:** Implement `org.vibeerp.api.v1.i18n.Translator` using ICU4J `MessageFormat` with locale fallback. Resolve message bundles in this order: `metadata__translation` (tenant overrides) → plug-in `i18n/messages_.properties` → core `i18n/messages_.properties` → fallback locale. +**Acceptance:** unit tests covering plurals, gender, number formatting in en-US, zh-CN, de-DE, ja-JP, es-ES; a test that a tenant override beats a plug-in default beats a core default. + +### P1.7 — Event bus + outbox +**Module:** new `platform-events` module +**Depends on:** — +**What:** Implement `org.vibeerp.api.v1.event.EventBus` with two parts: (a) an in-process Spring `ApplicationEventPublisher` for synchronous delivery to listeners in the same process; (b) an `event_outbox` table written in the same DB transaction as the originating change, scanned by a background poller, marked dispatched after delivery. The outbox is the seam where Kafka/NATS plugs in later. +**Acceptance:** integration test that an OrderCreated event survives a process crash between the original transaction commit and a downstream listener — the listener fires when the process restarts. + +### P1.8 — JasperReports integration +**Module:** new `platform-reporting` module +**Depends on:** P1.5 +**What:** Wrap JasperReports compilation/rendering behind a `ReportRenderer` Spring component. Reports live in `metadata__report` (path or content). API exposes `/api/v1/_meta/reports/{id}.pdf?param=...` for synchronous rendering, with a job-based async path for big reports. +**Acceptance:** ship one core report (a User list) and confirm it renders to PDF with the correct locale-aware date and number formatting. + +### P1.9 — File store (local + S3) +**Module:** new `platform-files` module +**Depends on:** — +**What:** A `FileStore` interface with `LocalFileStore` and `S3FileStore` implementations, configured via `vibeerp.files.backend`. Each stored file has a `tenant_id` namespace baked into its key, so a misconfigured S3 bucket can't accidentally serve cross-tenant files. +**Acceptance:** unit tests for both backends; integration test with MinIO Testcontainer for S3. + +### P1.10 — Job scheduler +**Module:** new `platform-jobs` module +**Depends on:** — +**What:** Quartz integration with the `JDBCJobStore` against the same Postgres. PBCs and plug-ins register jobs through a typed `JobScheduler` API in api.v1 (already has the seam, may need a tiny addition). Each job runs inside a `TenantContext.runAs(tenantId) { ... }` block — never inheriting a stale tenant from the trigger thread. +**Acceptance:** a recurring core job that prunes the audit log; integration test confirms the job runs in the right tenant context. + +--- + +## Phase 2 — Embedded workflow engine + +### P2.1 — Embedded Flowable +**Module:** new `platform-workflow` module +**Depends on:** P1.5 (metadata), P1.6 (i18n for task names), P1.7 (events) +**What:** Pull in `flowable-spring-boot-starter`. Configure Flowable to use the same Postgres datasource (so its tables live alongside ours, prefixed `flowable_*`). Wire `org.vibeerp.api.v1.workflow.TaskHandler` implementations registered by plug-ins as Flowable `JavaDelegate`s. Workflows are deployed from `metadata__workflow` rows (BPMN XML payload) on boot and on plug-in load. +**Acceptance:** the reference printing-shop plug-in registers a "quote-to-job-card" `TaskHandler`; a BPMN process that calls it can be started via REST and returns the expected output. + +### P2.2 — BPMN designer integration (web) +**Module:** the future React SPA +**Depends on:** P2.1, R1 (web bootstrap) +**What:** Embed `bpmn-js` (Camunda's BPMN modeler) in a "Workflow Designer" page. Save as a `metadata__workflow` row; deploy through the Tier 1 path. +**Acceptance:** end-to-end test where a Tier 1 user creates a workflow in the browser and starts an instance of it. + +### P2.3 — User task / form rendering +**Module:** `platform-workflow` + future React SPA +**Depends on:** P2.1, F1 (form designer), R1 +**What:** When a Flowable user task fires, look up the form id from the task definition, render it via the form renderer (P3.x), capture the user's input, and complete the task. +**Acceptance:** end-to-end test of an approval workflow where a human clicks a button. + +--- + +## Phase 3 — Metadata store: forms and rules (the Tier 1 user journeys) + +### P3.1 — JSON Schema form renderer (server) +**Module:** new `platform-forms` module +**Depends on:** P1.5 +**What:** A `FormRenderer` that, given an entity instance and a `FormSchema`, returns a server-side JSON describing what to render. The actual rendering happens in the SPA (P3.2). The server-side piece is needed for non-browser clients (CLI, integration tests, AI agents). +**Acceptance:** snapshot tests of the rendered JSON for a User form with an added custom field. + +### P3.2 — Form renderer (web) +**Module:** future React SPA +**Depends on:** P3.1, R1 +**What:** A React component that consumes JSON Schema + UI Schema and renders an interactive form. Use `react-jsonschema-form` as the base, customize the widget set for vibe_erp's design language. +**Acceptance:** Storybook for every widget type; e2e test that filling and submitting a form persists the data. + +### P3.3 — Form designer (web) +**Module:** future React SPA +**Depends on:** P3.2 +**What:** A drag-and-drop designer that produces JSON Schema + UI Schema and saves into `metadata__form`. This is the Tier 1 entry point for "add a field, change a layout". +**Acceptance:** e2e test where a Tier 1 user adds a "Customer PO Reference" field to the Sales Order form without writing code. + +### P3.4 — Custom field application +**Module:** `platform-metadata` +**Depends on:** P1.5 +**What:** A Hibernate interceptor (or JPA listener) that reads the entity's `metadata__custom_field` rows on save and validates the JSON in the `ext` column against them — required, type, max length, etc. Validation errors come back as 422 with a per-field map. +**Acceptance:** integration test that a User with a missing required custom field cannot be saved; one with a valid one round-trips. + +### P3.5 — Rules engine (simple "if X then Y") +**Module:** new `platform-rules` module +**Depends on:** P1.7 (events) +**What:** Listen to `DomainEvent`s, evaluate `metadata__rule` rows scoped to that event type, run the configured action (send email, set field, call workflow, publish another event). Use a simple expression language (something like `JEXL` or Spring SpEL — bias toward Spring SpEL since we already have it). +**Acceptance:** integration test where a rule "on UserCreated, set ext.greeting = 'Welcome'" actually fires. + +### P3.6 — List view designer (web) +**Module:** future React SPA +**Depends on:** R1 +**What:** A grid component backed by `metadata__list_view` rows. Tier 1 users pick columns, filters, sort, save as a named list view, share with their tenant. +**Acceptance:** e2e test where a user creates a "Active customers in Germany" list view and re-opens it later. + +--- + +## Phase 4 — Authentication and authorization (real) + +### P4.1 — Built-in JWT auth +**Module:** new `pbc-auth` (under `pbc/`) + `platform-security` (might already exist) +**Depends on:** — +**What:** Username/password login backed by `identity__user_credential` (separate table from `identity__user` so the User entity stays small). Argon2id for hashing. Issue a signed JWT with `sub`, `tenant`, `roles`, `iat`, `exp`. Validate on every request via a Spring Security filter. +**Acceptance:** integration test of full login/logout/refresh flow; assertion that the JWT carries `tenant` and that downstream PBC code sees it via `Principal`. + +### P4.2 — OIDC integration +**Module:** `pbc-auth` +**Depends on:** P4.1 +**What:** Plug Spring Security's OIDC client into the same `Principal` resolution. Configurable per tenant: which provider, which client id, which claim maps to `tenant`. Keycloak is the smoke-test target. +**Acceptance:** Keycloak Testcontainer + integration test of a full OIDC code flow; the same `UserController` endpoints work without changes. + +### P4.3 — Permission checking (real) +**Module:** `platform-security` +**Depends on:** P4.1 +**What:** Implement `org.vibeerp.api.v1.security.PermissionCheck`. Resolve permissions from `metadata__role_permission` rows for the current `Principal`'s roles. Plug-ins register their own permissions via `@Extension(point = PermissionRegistrar::class)` (api.v1 may need a tiny addition here — deliberate, with a minor version bump). +**Acceptance:** integration test that a user without `identity.user.create` gets 403 from `POST /api/v1/identity/users`; with the role, 201. + +--- + +## Phase 5 — Core PBCs (the meat of the framework) + +Each PBC follows the same 7-step recipe: + +1. Create the Gradle subproject under `pbc/` +2. Domain entities (extending `AuditedJpaEntity`, with `ext jsonb`) +3. Spring Data JPA repositories +4. Application services +5. REST controllers under `/api/v1//` +6. Liquibase changelog (`db/changelog//`) with rollback blocks AND RLS policies +7. Cross-PBC facade in `api.v1.ext.` + adapter in the PBC + +### P5.1 — `pbc-catalog` — items, units of measure, attributes +Foundation for everything that's bought, sold, made, or stored. Hold off on price lists and configurable products until P5.10. + +### P5.2 — `pbc-partners` — customers, suppliers, contacts +Companies, addresses, contacts, contact channels. PII-tagged from day one (CLAUDE.md guardrail #6 / DSAR). + +### P5.3 — `pbc-inventory` — stock items, lots, locations, movements +Movements as immutable events; on-hand quantity is a projection. Locations are hierarchical. + +### P5.4 — `pbc-warehousing` — receipts, picks, transfers, counts +Built on top of `inventory` via `api.v1.ext.inventory`. The first PBC that proves the cross-PBC dependency rule under load. + +### P5.5 — `pbc-orders-sales` — quotes, sales orders, deliveries +Workflow-heavy. The reference printing-shop plug-in's quote-to-job-card flow exercises this. + +### P5.6 — `pbc-orders-purchase` — RFQs, POs, receipts +Mirror image of sales. Shares the document/approval pattern. + +### P5.7 — `pbc-production` (basic) — work orders, routings, operations +Generic discrete-manufacturing primitives. NOT printing-specific. + +### P5.8 — `pbc-quality` (basic) — inspection plans, results, holds +Generic; the printing-specific QC steps live in the plug-in. + +### P5.9 — `pbc-finance` (basic) — GL, journal entries, AR/AP minimal +Basic double-entry. Tax engines, multi-currency revaluation, consolidation are v1.2+. + +### P5.10 — `pbc-catalog` — pricing, configurable products +Defer until P5.5 (orders-sales) is real, since pricing only matters when orders exist. + +--- + +## Phase 6 — Web SPA + +### R1 — React + TypeScript bootstrap +**Module:** new `web/` directory (Vite + React + TS) +**Depends on:** P4.1 (need a way to log in) +**What:** Vite scaffold, design system (likely Mantine or Radix), routing (React Router), data fetching (TanStack Query), generated TypeScript client from the backend's OpenAPI spec. +**Acceptance:** can log in, see a list of users, log out. + +### R2 — Identity screens +**Depends on:** R1 +**What:** User list, user detail, role list, role detail, permission editor. + +### R3 — Customize / metadata UIs +**Depends on:** R1, P3.3, P3.6 +**What:** The Tier 1 entry point. Form designer, list view designer, custom field designer, workflow designer. + +### R4 — One screen per core PBC +**Depends on:** R1, the matching PBC from Phase 5 +**What:** Per-PBC list / detail / create / edit screens. Generated from metadata wherever possible; hand-built where the metadata-driven approach falls short (and those gaps become future enhancements to the metadata store). + +--- + +## Phase 7 — Reference plug-in (the executable acceptance test) + +### REF.1 — Real workflow handler +**Module:** `reference-customer/plugin-printing-shop` +**Depends on:** P2.1 (Flowable) +**What:** Replace the v0.1 hello-world with a real `TaskHandler` that converts a Quote into a JobCard, registered as `acme.printingshop.quoteToJobCard`. A BPMN file in the plug-in JAR drives a workflow that calls this handler. +**Acceptance:** integration test starts the workflow with a real Quote and asserts the JobCard appears. + +### REF.2 — Plate / ink / press domain +**Module:** same plug-in +**Depends on:** P1.4 (plug-in Liquibase application) +**What:** The plug-in defines its own entities (`plugin_printingshop__plate`, `plugin_printingshop__ink_recipe`, `plugin_printingshop__press`) via its own Liquibase changelog. It registers REST endpoints for them via `@PluginEndpoint`. It registers permissions like `printingshop.plate.approve`. +**Acceptance:** integration test that a printing-shop user can CRUD plates against `/api/v1/plugins/printing-shop/plates`, scoped to their tenant. + +### REF.3 — Real reference forms and metadata +**Module:** same plug-in +**Depends on:** P3.3, P3.4 +**What:** The plug-in ships YAML metadata for: a custom field on Sales Order ("printing job type"), a custom form for plate approval, an automation rule "on plate approved, dispatch to press". All loaded at plug-in start, all tagged `source = 'plugin:printing-shop'`. +**Acceptance:** end-to-end smoke test that loads the plug-in into a fresh tenant and exercises the full quote-to-job-card-to-press flow without any code changes outside the plug-in. + +--- + +## Phase 8 — Hosted, AI agents, mobile (post-v1.0) + +### H1 — Per-region tenant routing (hosted) +Make `platform-persistence` look up the tenant's region from `identity__tenant` and route the connection accordingly. v1.0 doesn't ship hosted, but the routing seam should land in v1.0 so v2.0 can flip a switch. + +### H2 — Tenant provisioning UI +A per-instance "operator console" that creates tenants, manages quotas, watches health. Separate React app, separate URL. + +### A1 — MCP server +**Module:** new `platform-mcp` +**Depends on:** every PBC, every endpoint having a typed OpenAPI definition +**What:** Expose every REST endpoint as an MCP function. Permission-checked, locale-aware, tenant-aware. A Tier 2 plug-in can register additional MCP functions via a new `@McpFunction` annotation in api.v1 (an additive change, minor version bump). +**Acceptance:** an LLM-driven agent can call `vibe_erp.identity.users.create` and the result is identical to a REST call. + +### M1 — React Native skeleton +Shared TypeScript types with the web SPA; first screens cover shop-floor scanning and approvals. + +--- + +## Tracking and ordering + +A coarse dependency view: + +``` +P1.1 — RLS hook ─────┐ +P1.2 — linter ──┐ │ +P1.3 — child ctx ┘ │ +P1.4 — plug-in liquibase ─ depends on P1.3 +P1.5 — metadata seed ──┐ ──┐ +P1.6 — translator ──── (depends on P1.5) +P1.7 — events ─────────┘ │ +P1.8 — reports ── (depends on P1.5) +P1.9 — files +P1.10 — jobs +P2.1 — Flowable ── (depends on P1.5, P1.6, P1.7) +P3.x — metadata Tier 1 ── (depends on P1.5) +P4.x — auth +P5.x — core PBCs ── (depend on P1.x complete + P4.x for permission checks) +R1 — web bootstrap ── (depends on P4.1) +REF.x — reference plug-in ── (depends on P1.4 + P2.1 + P3.3) +``` + +Sensible ordering for one developer: +P1.1 → P1.5 → P1.7 → P1.6 → P1.10 → P1.4 → P1.3 → P1.2 → P2.1 → P3.4 → P3.1 → P4.1 → P5.1 → P5.2 → ... + +Sensible parallel ordering for a team of three: +- Dev A: P1.1, P1.5, P1.7, P1.6, P2.1 +- Dev B: P1.4, P1.3, P1.2, P3.4, P3.1 +- Dev C: P4.1, P4.3, P5.1 (catalog), P5.2 (partners) + +Then converge on R1 (web) and the rest of Phase 5 in parallel. + +--- + +## Definition of "v1.0 ready" + +v1.0 ships when, on a fresh Postgres, an operator can: + +1. `docker run` the image +2. Log in to the web SPA +3. Create a tenant (or use the default tenant in self-host mode) +4. Drop the printing-shop plug-in JAR into `./plugins/` +5. Restart the container +6. See the printing-shop's screens, custom fields, workflows, and permissions in the SPA +7. Walk a quote through the printing shop's full workflow without writing any code +8. Generate a PDF report for that quote in zh-CN +9. Export a DSAR for one of their customers + +…and the same image, against a fresh empty Postgres, also serves a customer who has loaded a *completely different* plug-in expressing a *completely different* business — without any change to the core image. + +That's the bar. Until that bar is met, the framework has not delivered on its premise. diff --git a/docs/workflow-authoring/guide.md b/docs/workflow-authoring/guide.md new file mode 100644 index 0000000..f479b03 --- /dev/null +++ b/docs/workflow-authoring/guide.md @@ -0,0 +1,118 @@ +# Workflow authoring guide + +Workflows in vibe_erp are **data, not code**. State machines, approval chains, and document routing are declarative, so a new customer is onboarded by editing definitions instead of editing source. This is guardrail #2 in `CLAUDE.md`, and it shapes everything below. + +For the architectural placement of the workflow engine, see [`../architecture/overview.md`](../architecture/overview.md). For the full reasoning, see [`../superpowers/specs/2026-04-07-vibe-erp-architecture-design.md`](../superpowers/specs/2026-04-07-vibe-erp-architecture-design.md). + +## BPMN 2.0 on embedded Flowable + +vibe_erp embeds the **Flowable** engine and uses **BPMN 2.0** as the workflow language. BPMN is a standard, and the standard is the contract: there is no vibe_erp-specific workflow DSL to learn or to invent. Anything a BPMN 2.0 modeling tool can produce, vibe_erp can run. + +Flowable runs in-process inside the Spring Boot host. Its tables (`flowable_*`) live in the same Postgres database as the core PBCs and are untouched by vibe_erp. + +## Two authoring paths + +Both paths are first-class. Tier 1 is preferred whenever it is expressive enough; Tier 2 is the escape hatch. + +### Tier 1 — Visual designer in the web UI (v1.0) + +Business analysts draw workflows in the BPMN designer that ships in the web SPA. The result is stored as a row in `metadata__workflow`, deployed to Flowable, and tagged `source = 'user'` so it survives plug-in install/uninstall and core upgrades. No build, no restart, no plug-in. + +This path is the right one for: + +- Approval chains that vary per tenant. +- Document routing rules that the customer wants to tune themselves. +- Workflows that compose existing typed task handlers in a new order. + +The visual designer is a **v1.0 deliverable**; v0.1 ships the underlying API only. + +### Tier 2 — `.bpmn` files in a plug-in JAR (v0.1) + +Plug-in authors ship `.bpmn` files inside their JAR under `src/main/resources/workflow/`. The plug-in lifecycle deploys them to Flowable on plug-in install. The `plugin.yml` manifest lists them so the loader picks them up. + +``` +src/main/resources/ +├── plugin.yml +└── workflow/ + ├── quote_to_job_card.bpmn + └── reprint_request.bpmn +``` + +```yaml +# plugin.yml +metadata: + workflows: + - workflow/quote_to_job_card.bpmn + - workflow/reprint_request.bpmn +``` + +This path is the right one for: + +- Workflows that need new typed task handlers shipped alongside them. +- Workflows that the plug-in author wants under version control with the rest of the plug-in code. +- Workflows that ship as part of a vertical-specific plug-in (the printing-shop plug-in is the canonical example). + +## Service tasks call typed `TaskHandler` implementations + +A BPMN service task in vibe_erp does not embed scripting code. It references a typed `TaskHandler` by id, and the host routes the call to the matching implementation registered by a plug-in. + +The plug-in registers a handler: + +```kotlin +@Extension(point = TaskHandler::class) +class ReserveStockHandler : TaskHandler { + + override val id: String = "printing.reserve_stock" + + override fun handle(task: WorkflowTask) { + val itemId = task.variable("itemId") + ?: error("itemId is required") + val quantity = task.variable("quantity") ?: 0 + + // ... call into the plug-in's services here ... + + task.setVariable("reservationId", reservationId) + task.complete() + } +} +``` + +The BPMN service task references the handler by its id (`printing.reserve_stock`). The host validates at deploy time that every referenced handler id exists and rejects the deployment otherwise — broken workflows fail at install, not at runtime. + +There is no scripting language to invent. The `TaskHandler` is just Kotlin code behind a typed interface, with the same testability, debuggability, and review surface as the rest of the codebase. + +## User tasks render forms from metadata + +A BPMN user task references a form definition by id. The host looks the id up in `metadata__form` and renders the form using the same code path as Tier 1 forms — there is no parallel form renderer for workflows. + +This means: + +- The same form can be used inside a workflow user task and outside a workflow. +- A custom field added through Tier 1 customization automatically appears on every workflow user task that uses the same form. +- Validation runs in exactly the same place whether the form is rendered inside a workflow or not. + +For details on how forms work, see the [form authoring guide](../form-authoring/guide.md). + +## Worked example: quote-to-job-card + +The reference printing-shop plug-in ships a `quote_to_job_card.bpmn` workflow that exercises every concept in this guide. In rough shape: + +1. **Start event:** a sales clerk creates a quote (user task; uses a form from `metadata__form`). +2. **Service task `printing.price_quote`:** calls a `TaskHandler` registered by the plug-in to compute the price from the customer's price list and the quote's line items. +3. **Exclusive gateway:** routes on whether the quote total exceeds the customer's pre-approved limit. +4. **User task (manager approval):** rendered from a form definition; the manager can approve, reject, or send back for revision. +5. **Service task `printing.reserve_stock`:** another `TaskHandler` that reserves raw materials. +6. **Service task `printing.create_job_card`:** materializes the approved quote as a production job card (a custom entity defined by the plug-in). +7. **End event:** publishes a `JobCardCreated` `DomainEvent` so other PBCs and plug-ins can react without coupling. + +What is worth noticing: + +- Every printing-specific concept — *quote*, *price list*, *job card*, *reserve stock for plates and inks* — lives in the **plug-in**, never in core PBCs. The core only knows about generic documents, workflows, forms, and events. +- The cross-PBC interaction in step 7 goes through a `DomainEvent`, not a direct call. PBCs never import each other. +- Steps 4 and 5 can be reordered, removed, or duplicated through the BPMN designer in the web UI without touching plug-in code, because the typed handlers are decoupled from the workflow shape. + +## Where to go next + +- Plug-in author walkthrough including a `TaskHandler` example: [`../plugin-author/getting-started.md`](../plugin-author/getting-started.md) +- Form authoring (the other half of any workflow that has user tasks): [`../form-authoring/guide.md`](../form-authoring/guide.md) +- Plug-in API surface, including `api.v1.workflow`: [`../plugin-api/overview.md`](../plugin-api/overview.md) -- libgit2 0.22.2