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.15 (post-event-driven cross-PBC integration) |
| Latest commit |
<pin after push> |
| Repo |
https://github.com/reporkey/vibe-erp |
| Modules |
16 |
| Unit tests |
192, all green |
| End-to-end smoke runs |
The full buy-and-sell loop works AND every state transition publishes a domain event end-to-end: PO confirm/receive/cancel emit orders_purchase.PurchaseOrder events, SO confirm/ship/cancel emit orders_sales.SalesOrder events. The wildcard EventAuditLogSubscriber logs each one and platform__event_outbox rows are persisted in the same transaction as the state change and dispatched by the OutboxPoller. Smoke verified: 6 events fired, 6 outbox rows DISPATCHED. |
| 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; event-driven cross-PBC integration live. All eight cross-cutting platform services are live plus the authorization layer. The framework now has six PBCs, both ends of the inventory loop work, and every state transition emits a typed domain event through the EventBus + transactional outbox. The 6 new events live in api.v1.event.orders (SalesOrderConfirmed/Shipped/Cancelled, PurchaseOrderConfirmed/Received/Cancelled) so any future PBC, plug-in, or subscriber can react without importing pbc-orders-sales or pbc-orders-purchase. Each publish runs inside the same @Transactional method as the state change and the ledger writes — a rollback on any line rolls the publish back too.
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) |
🔜 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. 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
|
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