PROGRESS.md 22.8 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.26.0 (quality→warehousing auto-quarantine + plug-in JobHandler registration)
Latest commit 1a45a4b feat(jobs+plugins): plug-in loader JobHandler registration + reference PlateCleanupJobHandler
Repo https://github.com/reporkey/vibe-erp
Modules 23
Unit tests 334, 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 🔜 Pending
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 — v2 state machine (DRAFT → IN_PROGRESS → COMPLETED), WorkOrderInput BOM child entity with per-unit consumption auto-issuing MATERIAL_ISSUE at complete time, scrap verb writing negative ADJUSTMENT. Auto-spawn from SO confirm still works with empty BOM. Routings / operations / scheduling 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 — 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.
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 what exists today

# 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":"<from-logs>"}' | 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