PROGRESS.md 18.4 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.14 (post-P5.6)
Latest commit 2861656 feat(pbc): P5.6 — pbc-orders-purchase + receive-driven inventory increase
Repo https://github.com/reporkey/vibe-erp
Modules 16
Unit tests 186, all green
End-to-end smoke runs The full buy-and-sell loop works: create supplier + customer + item + location, place a PO for 5000, confirm, receive (stock goes from 0 to 5000 via PURCHASE_RECEIPT ledger row tagged PO:PO-2026-0001), then place a SO for 50, confirm, ship (stock drops to 4950 via SALES_SHIPMENT tagged SO:SO-2026-0001). Both PBCs feed the same ledger.
Real PBCs implemented 6 of 10 (pbc-identity, pbc-catalog, pbc-partners, pbc-inventory, pbc-orders-sales, pbc-orders-purchase)
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. All eight cross-cutting platform services are live plus the authorization layer. The framework now has six PBCs and both ends of the inventory loop work: a purchase order receives stock via PURCHASE_RECEIPT ledger rows tagged PO:<code>, a sales order ships stock via SALES_SHIPMENT ledger rows tagged SO:<code>. Both PBCs feed the same inventory__stock_movement ledger via the same InventoryApi.recordMovement facade — the cross-PBC contract supports BOTH directions and BOTH consumers identically. The end-to-end demo "buy 5000 sheets, ship 50, see balance = 4950 with both ledger rows tagged correctly" runs in one smoke test.

The next phase continues building business surface area: pbc-production (the framework's first non-order/non-master-data PBC), the workflow engine (Flowable), event-driven cross-PBC integration (the event bus has been wired since P1.7 but no flow uses it yet), 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. 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) 🔜 Pending
P1.10 Job scheduler (Quartz) 🔜 Pending

Phase 2 — Embedded workflow engine

# Unit Status
P2.1 Embedded Flowable (BPMN 2.0) + TaskHandler wiring 🔜 Pending
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 🔜 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 🔜 Pending
P5.8 pbc-quality — inspection plans, results, holds 🔜 Pending
P5.9 pbc-finance — GL, journal entries, AR/AP minimal 🔜 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) 🔜 Pending — depends on P2.1
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.
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 Six 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". The framework's InventoryApi.recordMovement facade now has TWO callers — the same primitive feeds the same ledger from both directions.

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)

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

16 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