# vibe_erp — Project Progress > Snapshot of where the framework stands today, what's done, and what's next. > The detailed plan of record is > [`docs/superpowers/specs/2026-04-07-vibe-erp-implementation-plan.md`](docs/superpowers/specs/2026-04-07-vibe-erp-implementation-plan.md). > The architecture is locked in > [`docs/superpowers/specs/2026-04-07-vibe-erp-architecture-design.md`](docs/superpowers/specs/2026-04-07-vibe-erp-architecture-design.md). ## At a glance | | | |---|---| | **Latest version** | v0.28.0-SNAPSHOT (pbc-production v3 routings + shop-floor dashboard + OpenAPI / Swagger UI + real buildInfo) | | **Latest commit** | `944e273 feat(bootstrap): real buildInfo + framework version as single source of truth` | | **Repo** | https://github.com/reporkey/vibe-erp | | **Modules** | 24 | | **Unit tests** | 355, all green | | **Real PBCs implemented** | **10 of 10** (pbc-identity, pbc-catalog, pbc-partners, pbc-inventory, pbc-warehousing, pbc-orders-sales, pbc-orders-purchase, pbc-finance, pbc-production, pbc-quality) — P5.x row of the implementation plan complete at minimal v1 scope | | **End-to-end smoke runs** | An SO confirmed with 2 lines auto-spawns 2 draft work orders via `SalesOrderConfirmedSubscriber`; completing one credits the finished-good stock via `PRODUCTION_RECEIPT`, cancelling the other flips its status, and a manual WO can still be created with no source SO. All in one run: 6 outbox rows DISPATCHED across `orders_sales.SalesOrder` and `production.WorkOrder` topics; pbc-finance still writes its AR row for the underlying SO; the `inventory__stock_movement` ledger carries the production receipt tagged `WO:`. First PBC that REACTS to another PBC's events by creating new business state (not just derived reporting state). | | **Plug-ins serving HTTP** | 1 (reference printing-shop, 7 endpoints + own DB schema + own metadata + own i18n bundles) | | **Production-ready?** | **No.** Foundation is solid; many capabilities still deferred. | ## Current stage **Foundation complete; Tier 1 customization live; authorization enforced; full buy-and-sell loop closed; event-driven cross-PBC integration live in BOTH directions; consumer PBC now reacts to the full order lifecycle.** All eight cross-cutting platform services are live plus the authorization layer. **The framework now has seven PBCs, both ends of the inventory loop work, every state transition emits a typed domain event, and the consumer PBC reacts to all six lifecycle events** (confirm + ship/receive + cancel for both sales and purchase). The pbc-finance `OrderEventSubscribers` bean registers six typed-class subscriptions at boot. JournalEntry rows carry a lifecycle status (POSTED → SETTLED on fulfilment, POSTED → REVERSED on cancellation) and all transitions are idempotent under at-least-once delivery. Cancel-from-DRAFT is a clean no-op because no `*ConfirmedEvent` was ever published to create a row. The next phase continues **building business surface area**: pbc-production (the framework's first non-order/non-master-data PBC), the workflow engine (Flowable), and eventually the React SPA. ## Total scope (the v1.0 cut line) The framework reaches v1.0 when, on a fresh Postgres, an operator can `docker run` the image, log in, drop a customer plug-in JAR into `./plugins/`, restart, and walk a real workflow end-to-end without writing any code — and the **same** image, against a different Postgres pointed at a different company, also serves a different customer with a different plug-in. See the implementation plan for the full v1.0 acceptance bar. That target breaks down into roughly 30 work units across 8 phases. About **22 are done** as of today, plus the event-driven cross-PBC integration follow-up that completes the P1.7 story. Below is the full list with status. ### Phase 1 — Platform completion (foundation) | # | Unit | Status | |---|---|---| | ~~P1.1~~ | ~~Postgres RLS transaction hook~~ | **N/A** — removed by single-tenant refactor | | P1.2 | Plug-in linter (ASM bytecode scan) | ✅ DONE — `7af11f2` | | P1.3 | Plug-in lifecycle: HTTP endpoints, DefaultPluginContext, dispatcher | ✅ DONE — `20d7ddc` | | P1.4 | Plug-in Liquibase application + `PluginJdbc` | ✅ DONE — `7af11f2` | | P1.5 | Metadata store seeding | ✅ DONE — `1ead32d` | | P1.6 | ICU4J `Translator` implementation + locale resolution | ✅ DONE — `01c71a6` | | P1.7 | Event bus + transactional outbox | ✅ DONE — `c2f2314` | | P1.8 | JasperReports integration | ✅ DONE — `89f47a3` — new `platform-reports` module with JasperReports 6.21.3; api.v1 `ReportRenderer` + `ReportRenderException`; `JasperReportRenderer` @Component running JDK javac via `JRJavacCompiler`; `PluginContext.reports` wired with default-throw; `/api/v1/reports/ping` + `/api/v1/reports/render` endpoints; built-in `vibeerp-ping-report.jrxml` self-test | | P1.9 | File store (local + S3) | ✅ Partial — `f2156c5` — api.v1 `FileStorage` + `FileHandle` + `FileReadResult`; new `platform-files` subproject with `LocalDiskFileStorage` (sidecar metadata files, atomic `put`, path-traversal guards); `PluginContext.files` with default-throw; `FileController` with multipart upload/download/list/delete. S3 backend still pending. | | P1.10 | Job scheduler (Quartz) | ✅ DONE — `5d6a2f1` — new `platform-jobs` module with Quartz Spring Boot starter + JDBC job store against host Postgres; api.v1 `JobHandler`/`JobScheduler`/`JobContext`; owner-tagged `JobHandlerRegistry` parallel to `TaskHandlerRegistry`; `QuartzJobBridge` routes by key; `PrincipalContext.runAs("system:jobs:")` wraps every execution; HTTP surface at `/api/v1/jobs/**` (list/trigger/schedule/unschedule); built-in `vibeerp.jobs.ping` diagnostic handler | ### Phase 2 — Embedded workflow engine | # | Unit | Status | |---|---|---| | P2.1 | Embedded Flowable (BPMN 2.0) + `TaskHandler` wiring | ✅ DONE — `7bff422` core + `ef9e5b4` principal propagation + `a091d38` plug-in-loaded handler registration + `66ad87d` plug-in JAR BPMN auto-deployment — `platform-workflow` module shares host Postgres; dispatcher routes service-task execution to `TaskHandlerRegistry` beans by activity id; authenticated HTTP caller's principal flows through reserved `__vibeerp_*` process vars into `ctx.principal()`; plug-ins register handlers via `context.taskHandlers` + ship BPMNs under `processes/*.bpmn20.xml` in their JAR (host `PluginProcessDeployer` reads + deploys with `category=pluginId` + cascading undeploy on stop); `POST/GET /api/v1/workflow/**` endpoints; reference plug-in ships a live `plugin-printing-shop-plate-approval` BPMN driving `printing_shop.plate.approve` | | P2.2 | BPMN designer (web) | 🔜 Pending — depends on R1 | | P2.3 | User-task form rendering | 🔜 Pending | ### Phase 3 — Metadata store: forms and rules (Tier 1 customization) | # | Unit | Status | |---|---|---| | P3.1 | JSON Schema form renderer (server) | 🔜 Pending | | P3.2 | Form renderer (web) | 🔜 Pending — depends on R1 | | P3.3 | Form designer (web) | 🔜 Pending — depends on R1 | | P3.4 | Custom field application (JSONB `ext` validation) | ✅ DONE — `5bffbc4` | | P3.5 | Rules engine (event-driven) | 🔜 Pending | | P3.6 | List view designer (web) | 🔜 Pending — depends on R1 | ### Phase 4 — Authentication and authorization | # | Unit | Status | |---|---|---| | P4.1 | Built-in JWT auth + Argon2id + bootstrap admin | ✅ DONE — `540d916` | | P4.2 | OIDC integration (Keycloak-compatible) | 🔜 Pending | | P4.3 | Permission checking against `metadata__role_permission` | ✅ DONE — `75bf870` | ### Phase 5 — Core PBCs | # | Unit | Status | |---|---|---| | P5.1 | `pbc-catalog` — items, units of measure | ✅ DONE — `69f3daa` | | P5.2 | `pbc-partners` — customers, suppliers, contacts, addresses | ✅ DONE — `3e40cae` | | P5.3 | `pbc-inventory` — locations + stock balances (movements deferred) | ✅ DONE — `f63a73d` | | P5.4 | `pbc-warehousing` — receipts, picks, transfers, counts | ✅ Partial — `ba50780` — StockTransfer aggregate live (create/confirm/cancel), confirm writes atomic TRANSFER_OUT + TRANSFER_IN ledger pair via InventoryApi; transactional rollback verified; receipts (own-aggregate version), picks, counts still pending | | P5.5 | `pbc-orders-sales` — sales orders + lines (deliveries deferred) | ✅ DONE — `a8eb2a6` | | P5.6 | `pbc-orders-purchase` — POs + receive flow (RFQs deferred) | ✅ DONE — `2861656` | | P5.7 | `pbc-production` — work orders, routings, operations | ✅ Partial — `fa86718` v3 routings/ops + `35ad8a8` event-driven routings + `1b0f6d8` shop-floor dashboard. `WorkOrderOperation` child entity with per-op sequential state machine (PENDING → IN_PROGRESS → COMPLETED); `WorkOrder.complete()` gated behind every operation reaching COMPLETED; `startOperation` + `completeOperation` verbs stamp `startedAt`/`completedAt` and record operator-entered `actualMinutes` for variance reporting. `WorkOrderRequestedEvent` carries optional `RoutingOperationSpec` list so customer plug-ins can attach routings through api.v1 alone (reference plug-in ships a 4-step CUT → PRINT → FOLD → BIND default). `GET /work-orders/shop-floor` projects IN_PROGRESS work orders into a flat dashboard snapshot with current operation, planned/actual time totals, and operations-completed count. Scheduling / capacity planning / machine FKs still pending. | | P5.8 | `pbc-quality` — inspection plans, results, holds | ✅ Partial — `4835785` — InspectionRecord aggregate live (append-only record of QC decisions with APPROVED/REJECTED + inspected/rejected quantities + source reference + inspector). Plans/templates + cross-PBC reactions (quarantine on rejection, WO scrap) still pending. | | P5.9 | `pbc-finance` — GL, journal entries, AR/AP minimal | ✅ Partial — minimal AR/AP journal entries driven by events; full GL pending | ### Phase 6 — Web SPA (React + TS) | # | Unit | Status | |---|---|---| | R1 | Vite + React + TS bootstrap, login flow, OpenAPI client | 🔜 Pending | | R2 | Identity screens | 🔜 Pending | | R3 | Customize / metadata UIs | 🔜 Pending | | R4 | Per-PBC list/detail/create/edit screens | 🔜 Pending | ### Phase 7 — Reference printing-shop plug-in | # | Unit | Status | |---|---|---| | REF.0 | Hello-world plug-in (PF4J + Plugin lifecycle + endpoints + own DB) | ✅ DONE through v0.6 | | REF.1 | Real BPMN workflow handler (quote → job card) | ✅ DONE — `3027c1f` core + `35ad8a8` v3 routings — plug-in BPMN publishes `WorkOrderRequestedEvent` (new api.v1 event); pbc-production's `WorkOrderRequestedSubscriber` reacts by creating a DRAFT WorkOrder via `WorkOrderService.create`. Idempotent on event code. Plug-in has zero compile-time coupling to pbc-production; pbc-production has zero knowledge the plug-in exists. **v3 follow-up**: event now carries optional `RoutingOperationSpec` list; the plug-in's handler ships a default 4-step CUT → PRINT → FOLD → BIND routing that reaches the created WO as v3 operations, end-to-end through api.v1 alone. | | REF.2 | Plate / ink / press CRUD as customer-style domain | ✅ Partial — plate + ink CRUD live (`7af11f2`) | | REF.3 | Reference forms + automation rules in plug-in metadata | 🔜 Pending — depends on P3.3, P3.4 | ### Phase 8 — Hosted, AI agents, mobile (post-v1.0) | # | Unit | Status | |---|---|---| | H1 | Instance provisioning console (one process per customer) | 🔜 Post-v1.0 | | A1 | MCP server (REST endpoints exposed as AI-agent functions) | 🔜 v1.1 | | M1 | React Native skeleton | 🔜 v2 | ## What's live right now These are the cross-cutting platform services already wired into the running framework. Each was built with a real end-to-end smoke test against a Postgres container. | Service | Module | What it does | |---|---|---| | **Auth** (P4.1) | `platform-security`, `pbc-identity` | Username/password login → HMAC-SHA256 JWT (15min access + 7d refresh). Bootstrap admin printed to logs on first boot. Spring Security filter chain enforces auth on every endpoint except `/actuator/health`, `/api/v1/_meta/**`, `/api/v1/auth/login`, `/api/v1/auth/refresh`. Principal bridges into the audit listener so `created_by` columns carry real user UUIDs. | | **Authorization** (P4.3) | `platform-security.authz` | `@RequirePermission("partners.partner.deactivate")` on controller methods, enforced by a Spring AOP `@Aspect`. JWT access tokens carry a `roles` claim populated from `identity__user_role` at login time. `PrincipalContextFilter` populates a per-request `AuthorizationContext` with the principal's role set; the aspect consults `PermissionEvaluator.has(roles, key)` which special-cases the wildcard `admin` role and otherwise reads `metadata__role_permission`. Failed checks throw `PermissionDeniedException` → 403 with the offending key in the body. The bootstrap admin gets the `admin` role on first boot; non-admin users with no role assignments get 403 on every protected endpoint. | | **Plug-in HTTP** (P1.3) | `platform-plugins` | Plug-ins call `context.endpoints.register(method, path, handler)` to mount lambdas under `/api/v1/plugins//`. Path templates with `{var}` extraction via Spring's `AntPathMatcher`. Plug-in code never imports Spring MVC types. | | **Plug-in linter** (P1.2) | `platform-plugins` | At plug-in load time (before any plug-in code runs), ASM-walks every `.class` entry for references to `org.vibeerp.platform.*` or `org.vibeerp.pbc.*`. Forbidden references unload the plug-in with a per-class violation report. | | **Plug-in DB schemas** (P1.4) | `platform-plugins` | Each plug-in ships its own `META-INF/vibe-erp/db/changelog.xml`. The host's `PluginLiquibaseRunner` applies it against the shared host datasource at plug-in start. Plug-ins query their own tables via the api.v1 `PluginJdbc` typed-SQL surface — no Spring or Hibernate types ever leak. | | **Event bus + outbox** (P1.7) | `platform-events` | Synchronous in-process delivery PLUS a transactional outbox row in the same DB transaction. `Propagation.MANDATORY` so the bus refuses to publish outside an active transaction (no publish-and-rollback leaks). `OutboxPoller` flips PENDING → DISPATCHED every 5s. Wildcard `**` topic for the audit subscriber; topic-string and class-based subscribe. **Now exercised end-to-end:** `SalesOrderService.confirm/ship/cancel` and `PurchaseOrderService.confirm/receive/cancel` each publish a typed event from `api.v1.event.orders.*` inside the same `@Transactional` method as their state change and ledger writes. Smoke test confirms the wildcard `EventAuditLogSubscriber` logs every one and `platform__event_outbox` rows are persisted + dispatched. | | **Metadata loader** (P1.5) | `platform-metadata` | Walks the host classpath and each plug-in JAR for `META-INF/vibe-erp/metadata/*.yml`, upserts entities/permissions/menus into `metadata__*` tables tagged by source. Idempotent (delete-by-source then insert). User-edited metadata (`source='user'`) is never touched. Public `GET /api/v1/_meta/metadata` returns the full set for SPA + AI-agent + OpenAPI introspection. | | **i18n / Translator** (P1.6) | `platform-i18n` | ICU4J-backed `Translator` with named placeholders, plurals, gender, locale-aware number/date formatting. Per-plug-in instance scoped via a `(classLoader, baseName)` chain — plug-in's `META-INF/vibe-erp/i18n/messages_.properties` resolves before the host's `messages_.properties` for shared keys. `RequestLocaleProvider` reads `Accept-Language` from the active HTTP request via the servlet container, falling back to `vibeerp.i18n.defaultLocale` outside an HTTP context. JVM-default locale fallback explicitly disabled to prevent silent locale leaks. | | **Custom field application** (P3.4) | `platform-metadata.customfield` | `CustomFieldRegistry` reads `metadata__custom_field` rows into an in-memory index keyed by entity name, refreshed at boot and after every plug-in load. `ExtJsonValidator` validates the JSONB `ext` map of any entity against the declared `FieldType`s — String maxLength, Integer/Decimal numeric coercion with precision/scale enforcement, Boolean, Date/DateTime ISO-8601 parsing, Enum allowed values, UUID format. Unknown keys are rejected; required missing fields are rejected; ALL violations are returned in a single 400 so a form submitter fixes everything in one round-trip. `Partner` is the first PBC entity to wire ext through `PartnerService.create/update`; the public `GET /api/v1/_meta/metadata/custom-fields/{entityName}` endpoint serves the api.v1 runtime view of declarations to the SPA / OpenAPI / AI agent. | | **PBC pattern** (P5.x recipe) | `pbc-identity`, `pbc-catalog`, `pbc-partners`, `pbc-inventory`, `pbc-orders-sales`, `pbc-orders-purchase`, `pbc-finance` | Seven real PBCs prove the recipe. **pbc-orders-purchase** is the buying-side mirror of pbc-orders-sales: same recipe, same three cross-PBC seams (PartnersApi + CatalogApi + InventoryApi), same line-with-recompute pattern, but the partner role check is inverted (must be SUPPLIER or BOTH), the state machine ends in RECEIVED instead of SHIPPED, and the cross-PBC inventory write uses positive deltas with `reason="PURCHASE_RECEIPT"`. **pbc-finance** is the framework's first CONSUMER PBC — it has no service of its own (yet), no cross-PBC facade in `api.v1.ext.*`, and no write endpoint. It exists ONLY to react to `SalesOrderConfirmedEvent` and `PurchaseOrderConfirmedEvent` from the api.v1 event surface and produce derived AR/AP rows. This validates the consumer side of the cross-PBC seam: a brand-new PBC subscribes to existing PBCs' events through `EventBus.subscribe(eventType, listener)` without any source dependency on the producers. | ## What the reference plug-in proves end-to-end The printing-shop reference plug-in is the framework's executable acceptance test. As of `1ead32d` it demonstrates **everything a real customer plug-in needs** except workflow handling: ``` Plug-in JAR drop → host bootstrap: 1. PF4J discovers the JAR and reads plugin.yml 2. PluginLinter ASM-walks every class — passes 3. PluginLiquibaseRunner applies META-INF/vibe-erp/db/changelog.xml → creates plugin_printingshop__plate, plugin_printingshop__ink_recipe 4. MetadataLoader.loadFromPluginJar reads META-INF/vibe-erp/metadata/printing-shop.yml → 2 entities, 5 permissions, 2 menus tagged source='plugin:printing-shop' 5. VibeErpPluginManager calls Plugin.start(context) with a real PluginContext (logger + endpoints + eventBus + jdbc all wired) 6. The plug-in's start() lambda registers 7 HTTP endpoints 7. Boot completes, app is serving traffic Live HTTP traffic: POST /api/v1/auth/login (admin) → 200, JWT POST /api/v1/plugins/printing-shop/plates (Bearer) → 201, plate created GET /api/v1/plugins/printing-shop/plates → list of plates GET /api/v1/plugins/printing-shop/plates/{id} → fetch by id POST /api/v1/plugins/printing-shop/inks → 201, ink created GET /api/v1/_meta/metadata → all 6 entities + 18 permissions GET /api/v1/identity/users (Bearer) → still works GET /api/v1/catalog/uoms (Bearer) → 15 seeded UoMs ``` This is **real**: a JAR file dropped into a directory, loaded by the framework, executing customer-specific business logic against its own database tables, exposed via REST. The plug-in code never imports a single internal framework class — and the linter would refuse to load it if it tried. ## What's not yet live (the deferred list) - **Workflow engine.** No Flowable yet. The api.v1 `TaskHandler` interface exists; the runtime that calls it doesn't. - **Forms.** No JSON Schema form renderer (server or client). No form designer. - **Reports.** No JasperReports. - **File store.** No abstraction; no S3 backend. - **Job scheduler.** No Quartz. Periodic jobs don't have a home. - **OIDC.** Built-in JWT only. OIDC client (Keycloak-compatible) is P4.2. - **More PBCs.** Identity, catalog, partners, inventory, orders-sales and orders-purchase exist. Warehousing, production, quality, finance are all pending. - **Web SPA.** No React app. The framework is API-only today. - **MCP server.** The architecture leaves room for it; the implementation is v1.1. - **Mobile.** v2. ## How to run what exists today ```bash # Bring up Postgres + the plug-in JAR (in background) docker compose up -d db ./gradlew :reference-customer:plugin-printing-shop:installToDev # Boot the framework against it ./gradlew :distribution:bootRun # In another shell: curl -s localhost:8080/api/v1/_meta/info curl -s localhost:8080/api/v1/_meta/metadata | jq # Read the bootstrap admin password from the boot logs, then: ACCESS=$(curl -s -H 'Content-Type: application/json' \ -X POST localhost:8080/api/v1/auth/login \ -d '{"username":"admin","password":""}' | jq -r .accessToken) curl -s -H "Authorization: Bearer $ACCESS" localhost:8080/api/v1/identity/users curl -s -H "Authorization: Bearer $ACCESS" localhost:8080/api/v1/catalog/uoms curl -s -H "Authorization: Bearer $ACCESS" localhost:8080/api/v1/plugins/printing-shop/plates ``` ## Module map ``` api/api-v1 PUBLIC CONTRACT — semver-governed platform/platform-bootstrap Spring Boot main, props, web filters platform/platform-persistence Audit base, JPA entities, PrincipalContext platform/platform-security JWT issuer/verifier, Spring Security config, password encoder platform/platform-events EventBus impl, transactional outbox, poller platform/platform-metadata MetadataLoader, MetadataController platform/platform-i18n IcuTranslator, RequestLocaleProvider (P1.6) platform/platform-plugins PF4J host, linter, Liquibase runner, endpoint dispatcher, PluginJdbc pbc/pbc-identity User entity end-to-end + auth + bootstrap admin pbc/pbc-catalog Item + Uom entities + cross-PBC CatalogApi facade pbc/pbc-partners Partner + Address + Contact entities + cross-PBC PartnersApi facade pbc/pbc-inventory Location + StockBalance + cross-PBC InventoryApi facade (first PBC to CONSUME another PBC's facade — CatalogApi) pbc/pbc-orders-sales SalesOrder + SalesOrderLine + cross-PBC SalesOrdersApi facade (first PBC to CONSUME TWO facades simultaneously — PartnersApi + CatalogApi and the first cross-PBC WRITE flow via InventoryApi.recordMovement) pbc/pbc-orders-purchase PurchaseOrder + PurchaseOrderLine + cross-PBC PurchaseOrdersApi facade (the buying-side mirror; receives via InventoryApi.recordMovement with positive PURCHASE_RECEIPT deltas) pbc/pbc-finance JournalEntry + read-only controller; first CONSUMER PBC. Subscribes to SalesOrderConfirmedEvent + PurchaseOrderConfirmedEvent via api.v1 EventBus.subscribe(eventType, listener) and writes idempotent AR/AP rows. No outbound facade. reference-customer/plugin-printing-shop Reference plug-in: own DB schema (plate, ink_recipe), own REST endpoints, own metadata YAML distribution Bootable Spring Boot fat-jar assembly ``` 17 Gradle subprojects. Architectural dependency rule (PBCs never import each other; plug-ins only see api.v1) is enforced by the root `build.gradle.kts` at configuration time. ## Where to look next - **Detailed roadmap:** [`docs/superpowers/specs/2026-04-07-vibe-erp-implementation-plan.md`](docs/superpowers/specs/2026-04-07-vibe-erp-implementation-plan.md) — every unit, every dependency, every acceptance test. - **Architecture rationale:** [`docs/superpowers/specs/2026-04-07-vibe-erp-architecture-design.md`](docs/superpowers/specs/2026-04-07-vibe-erp-architecture-design.md) — 14 sections, validated against 2026 ERP/EBC SOTA. - **Project guardrails:** [`CLAUDE.md`](CLAUDE.md) — the 11 rules every change is held to. - **Git history:** every chunk is one commit with a detailed message describing what landed, what was deferred, and any bug caught by the smoke test.