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.13 (post-ledger + ship) |
| Latest commit |
feat(inventory+orders): movement ledger + sales-order shipping (end-to-end demo) |
| Repo |
https://github.com/reporkey/vibe-erp |
| Modules |
15 |
| Unit tests |
175, all green |
| End-to-end smoke runs |
The killer demo works: create catalog item + partner + location, set stock to 1000, place an order for 50, confirm, ship, watch the balance drop to 950 and a SALES_SHIPMENT row appear in the ledger tagged SO:SO-2026-0001. Over-shipping rolls back atomically with a meaningful 400. |
| Real PBCs implemented |
5 of 10 (pbc-identity, pbc-catalog, pbc-partners, pbc-inventory, pbc-orders-sales) |
| 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; end-to-end order-to-shipment loop closed. All eight cross-cutting platform services are live plus the authorization layer. The biggest leap in this version: the inventory movement ledger is live (inventory__stock_movement append-only table; the framework's first append-only ledger), and sales orders can now ship (POST /sales-orders/{id}/ship) via a cross-PBC write — pbc-orders-sales injects the new InventoryApi.recordMovement to atomically debit stock for every line and update the order status, all in one transaction. Either the whole shipment commits or none of it does. This is the framework's first cross-PBC WRITE flow (every earlier cross-PBC call was a read). The sales-order state machine grows: DRAFT → CONFIRMED → SHIPPED (terminal). Cancelling a SHIPPED order is rejected with a meaningful "use a return / refund flow" message.
The next phase continues building business surface area: pbc-orders-purchase, pbc-production, 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 21 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 — RFQs, POs, receipts |
🔜 Pending |
| 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
|
Five real PBCs prove the recipe across five aggregate shapes (single-entity user, two-entity catalog, parent-with-children partners, master-data+facts inventory, header+lines sales orders with state machine): domain entity extending AuditedJpaEntity → Spring Data JPA repository → application service → REST controller under /api/v1/<pbc>/<resource> → cross-PBC facade in api.v1.ext.<pbc> → adapter implementation. Architecture rule enforced by the Gradle build: PBCs never import each other, never import platform-bootstrap. pbc-inventory is the first PBC to consume one cross-PBC facade (CatalogApi); pbc-orders-sales is the first to consume two simultaneously (PartnersApi + CatalogApi) in a single transaction, proving the modular monolith works under realistic workload. |
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 and orders-sales exist. Warehousing, orders-purchase, 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)
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
15 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