PROGRESS.md 26.5 KB

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. The architecture is locked in docs/superpowers/specs/2026-04-07-vibe-erp-architecture-design.md.

At a glance

Latest version v0.29.0-SNAPSHOT (R1 Web SPA bootstrap + demo seed — click-through end-to-end buy-sell loop from the browser)
Latest commit 0add76b ci(docker): install nodejs+npm in build stage for :web SPA build
Repo https://github.com/reporkey/vibe-erp
Modules 24 JVM subprojects + :web SPA
Unit tests 356, 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:<code>. 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:<key>") 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, typed REST client ✅ DONE — fc62d6d — new :web Gradle subproject wraps Vite/React 18/TypeScript/Tailwind 3.4 via Exec tasks; :distribution consumes the dist via an outgoing/incoming Gradle configuration and stages it into classpath:/static/ so a single fat-jar serves both API and SPA. Hand-written typed fetch client over /api/v1/** (211 KB JS gzipped, no codegen toolchain). AuthContext stores the JWT in localStorage with a 401 handler. SpaController forwards every known SPA route prefix to /index.html so React Router deep links work on hard refresh. SecurityConfiguration reordered: /api/** stays .authenticated() before the SPA permitAll rules so the "API is always authenticated" invariant holds even with the SPA bundled in the same image. Dockerfile build stage adds apk add nodejs npm so ./gradlew :distribution:bootJar produces a self-contained image with the SPA inside. End-to-end smoke verified on fresh Postgres: log in, walk SO DRAFT → CONFIRMED → SHIPPED, watch stock balances drop + AR journal entry settle — all in a real browser.
R2 Identity screens (user admin, role admin) 🔜 Pending
R3 Customize / metadata UIs 🔜 Pending
R4 Per-PBC create/edit screens (list/detail are live in R1) 🔜 Pending — list + detail pages for all 10 PBCs landed in R1 along with the ship/receive/confirm/cancel action verbs on the order + work-order detail pages. Create/edit forms and richer per-PBC screens (BOM editor, routing editor) will follow when a real consumer asks for them.

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/<plugin-id>/<path>. 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_<locale>.properties resolves before the host's messages_<locale>.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 FieldTypes — 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 the end-to-end demo

# Bring up Postgres + stage the reference plug-in JAR
docker compose up -d db
./gradlew :reference-customer:plugin-printing-shop:installToDev

# Boot the framework (serves both the REST API and the React SPA
# from the same fat-jar on http://localhost:8080)
./gradlew :distribution:bootRun

# Open the browser:
open http://localhost:8080

The dev profile opts in to the one-shot DemoSeedRunner which populates a starter dataset on first boot (5 items, 2 warehouses with opening stock, 4 partners, one open sales order, one open purchase order, all prefixed DEMO-). The bootstrap admin password is printed to the boot log.

Walk the demo:

  1. Log in as admin (password from the boot log).
  2. Sales Orders → DEMO-SO-0001Confirm → watch the AR POSTED row appear in the on-page journal-entry panel.
  3. Pick DEMO-WH-FGShip → order flips to SHIPPED, stock balances drop, the journal entry settles to AR SETTLED, and the SALES_SHIPMENT rows appear in the inventory movements panel. All from one button click.
  4. Purchase Orders → DEMO-PO-0001ConfirmReceive into DEMO-WH-RAW for the mirror-image flow.
  5. Shop Floor polls every 5 seconds; create or start a work order and watch it appear with a progress bar.

Or for the API-only version:

# In another shell:
curl -s localhost:8080/api/v1/_meta/info
curl -s localhost:8080/api/v1/_meta/metadata | jq

ACCESS=$(curl -s -H 'Content-Type: application/json' \
  -X POST localhost:8080/api/v1/auth/login \
  -d '{"username":"admin","password":"<from-logs>"}' | jq -r .accessToken)

curl -s -H "Authorization: Bearer $ACCESS" localhost:8080/api/v1/catalog/items
curl -s -H "Authorization: Bearer $ACCESS" localhost:8080/api/v1/orders/sales-orders
curl -s -H "Authorization: Bearer $ACCESS" localhost:8080/api/v1/plugins/printing-shop/plates

OpenAPI spec at /v3/api-docs and Swagger UI at /swagger-ui/index.html.

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