vibe_erp — Implementation Plan (post-v0.1)
Status: Approved roadmap. v0.1 skeleton is built, tested end-to-end against a real Postgres, and pushed. This document is the source of truth for the work that turns v0.1 into v1.0. Date: 2026-04-07 Companion document: Architecture spec
2026-04-07 update — single-tenant refactor. vibe_erp is now deliberately single-tenant per instance: one running process serves exactly one company against an isolated Postgres database. All multi-tenancy work has been removed from the framework — no
tenant_idcolumns, no Hibernate tenant filter, no Postgres Row-Level Security policies, noTenantContext. The previously-deferred P1.1 (RLS transaction hook) and H1 (per-region tenant routing) units are gone. See CLAUDE.md guardrail #5 for the rationale.2026-04-08 update — P4.1, P5.1, P1.3, P1.7, P1.4, P1.2 all landed. Auth (Argon2id + JWT + bootstrap admin), pbc-catalog (items + UoMs), plug-in HTTP endpoints (registry + dispatcher + DefaultPluginContext), event bus + transactional outbox + audit subscriber, plug-in-owned Liquibase changelogs (the reference plug-in now has its own database tables), and the plug-in linter (ASM bytecode scan rejecting forbidden imports) are all done and smoke-tested end-to-end. The reference printing-shop plug-in has graduated from "hello world" to a real customer demonstration: it owns its own DB schema, CRUDs plates and ink recipes via REST through
context.jdbc, and would be rejected at install time if it tried to import internal framework classes. The framework is now at 81 unit tests across 10 modules, all green.
What v0.1 actually delivers (verified on a running app)
| Item | Status |
|---|---|
| Gradle multi-project, 7 subprojects | ✅ Builds end-to-end |
api/api-v1 — public plug-in contract |
✅ Compiles, 12 unit tests |
platform/platform-bootstrap — Spring Boot main, properties, @EnableJpaRepositories, GlobalExceptionHandler
|
✅ |
platform/platform-persistence — JPA, audit listener (no tenant scaffolding) |
✅ |
platform/platform-plugins — PF4J host, lifecycle |
✅ |
pbc/pbc-identity — User entity end-to-end |
✅ Compiles, 6 unit tests, smoke-tested via REST against real Postgres |
reference-customer/plugin-printing-shop — hello-world PF4J plug-in |
✅ Compiles, packs into a real PF4J JAR |
distribution — Spring Boot fat jar (vibe-erp.jar) |
✅ bootJar succeeds, bootRun boots cleanly in dev profile |
| Liquibase changelogs (platform init + identity init) | ✅ Run cleanly against fresh Postgres |
| Dockerfile, docker-compose.yaml, Makefile | ✅ Present; docker compose up -d db verified |
| GitHub Actions workflows | ✅ Present, running in CI on every push to https://github.com/reporkey/vibe-erp |
Architecture-rule enforcement in build.gradle.kts
|
✅ Active — fails on cross-PBC deps, on plug-ins importing internals, and on PBCs depending on platform-bootstrap
|
| README, CONTRIBUTING, LICENSE, 8 docs guides | ✅ |
| Architecture spec + this implementation plan | ✅ |
| End-to-end smoke test against real Postgres | ✅ POST/GET/PATCH/DELETE on /api/v1/identity/users; duplicate-username → 400; bogus id → 404; disable flow works |
What v0.1 explicitly does NOT deliver:
- The 9 other core PBCs (catalog, partners, inventory, warehousing, orders-sales, orders-purchase, production, quality, finance)
- The metadata store machinery (custom-field application, form designer, list views, rules engine)
- Embedded Flowable workflow engine
- ICU4J
Translatorimplementation - JasperReports
- Real authentication (anonymous access; the bootstrap admin lands in P4.1)
- React web SPA
- Plug-in linter (rejecting reflection into platform internals at install time)
- Plug-in Liquibase application
- Per-plug-in Spring child context
- MCP server
- Mobile app (v2 anyway)
The architecture accommodates all of these — the seams are in api.v1 already — they just aren't implemented.
How to read this plan
Each work unit is sized to be a single coherent implementation pass: roughly 1–5 days of work, one developer, one PR. Units are grouped by theme, ordered roughly by dependency, but parallelizable wherever the prerequisites are satisfied.
Each unit names:
- the Gradle modules it touches (or creates)
- the api.v1 surface it consumes (and never modifies, unless explicitly noted)
- the acceptance test that proves it landed correctly
- any upstream dependencies that must complete first
The 11 architecture guardrails in CLAUDE.md and the 14-section design in 2026-04-07-vibe-erp-architecture-design.md apply to every unit. This document does not restate them.
Phase 1 — Platform completion (the foundation everything else needs)
These units finish the platform layer so PBCs can be implemented without inventing scaffolding.
✅ P1.2 — Plug-in linter (DONE 2026-04-08)
Module: platform-plugins
What landed: ASM-based bytecode scanner that walks every .class entry in a plug-in JAR, checks every type/method/field reference for forbidden internal package prefixes (org/vibeerp/platform/, org/vibeerp/pbc/), and rejects the plug-in at install time with a per-class violation report. Wired into VibeErpPluginManager.afterPropertiesSet() between PF4J's loadPlugins() and startPlugins(). 5 unit tests with synthesized in-memory JARs cover clean plug-ins, forbidden platform refs, forbidden pbc refs, allowed api.v1 refs, and multiple violations being reported together.
✅ P1.3 — Plug-in lifecycle: HTTP endpoints (DONE 2026-04-07, v0.5)
Module: platform-plugins + api.v1
What landed (v0.5 cut): Real Plugin.start(context) lifecycle. DefaultPluginContext provides a real PluginLogger (SLF4J + per-plugin tag), a real EventBus (P1.7), real PluginEndpointRegistrar, and real PluginJdbc (P1.4). Single Spring @RestController PluginEndpointDispatcher at /api/v1/plugins/{pluginId}/** dispatches via PluginEndpointRegistry (Spring AntPathMatcher for {var} extraction). Reference plug-in registers 7 endpoints. Deferred: per-plug-in Spring child contexts — v0.6 instantiates the plug-in via PF4J's classloader without its own Spring context. Auto-scoping subscriptions on plug-in stop is also deferred.
✅ P1.4 — Plug-in Liquibase application (DONE 2026-04-08)
Module: platform-plugins + api.v1 (added PluginJdbc/PluginRow)
What landed: PluginLiquibaseRunner looks for META-INF/vibe-erp/db/changelog.xml inside the plug-in JAR via the PF4J classloader, applies it via Liquibase against the host's shared DataSource. Convention: tables use plugin_<id>__* prefix (linter will enforce in a future version). The reference printing-shop plug-in now has two real tables (plugin_printingshop__plate, plugin_printingshop__ink_recipe) and CRUDs them via REST through the new context.jdbc typed-SQL surface in api.v1. Plug-ins never see Spring's JdbcTemplate. Deferred: per-plug-in datasource isolation, table-prefix linter, rollback on uninstall.
P1.5 — Metadata store seeding
Module: new platform-metadata module (or expand platform-persistence)
Depends on: —
What: A MetadataLoader Spring component that, on boot and on plug-in start, upserts rows into metadata__entity, metadata__form, metadata__workflow, etc. from YAML files in the classpath / plug-in JAR. Each row carries source = 'core' | 'plugin:<id>' | 'user'. Idempotent; safe to run on every boot.
Acceptance: integration test confirms that booting twice doesn't duplicate rows; uninstalling a plug-in removes its source = 'plugin:<id>' rows; user-edited rows (source = 'user') are never touched.
P1.6 — Translator implementation backed by ICU4J
Module: new platform-i18n module
Depends on: P1.5 (so plug-in message bundles can be loaded as part of metadata)
What: Implement org.vibeerp.api.v1.i18n.Translator using ICU4J MessageFormat with locale fallback. Resolve message bundles in this order: metadata__translation (operator overrides) → plug-in i18n/messages_<locale>.properties → core i18n/messages_<locale>.properties → fallback locale.
Acceptance: unit tests covering plurals, gender, number formatting in en-US, zh-CN, de-DE, ja-JP, es-ES; a test that an operator override beats a plug-in default beats a core default.
✅ P1.7 — Event bus + outbox (DONE 2026-04-08)
Module: new platform-events module
What landed: Real EventBus implementation with the transactional outbox pattern. publish() writes the event row to platform__event_outbox AND synchronously delivers to in-process subscribers in the SAME database transaction (Propagation.MANDATORY so the bus refuses to publish outside a transaction). OutboxPoller (@Scheduled every 5s, pessimistic SELECT FOR UPDATE) drains PENDING / FAILED rows and marks them DISPATCHED. ListenerRegistry indexes by both class and topic string, supports a ** wildcard. EventAuditLogSubscriber logs every event as the v0.6 demo subscriber. pbc-identity publishes UserCreatedEvent after UserService.create(). 13 unit tests + verified end-to-end against Postgres: created users produce outbox rows that flip from PENDING to DISPATCHED inside one poller cycle. Deferred: external dispatcher (Kafka/NATS bridge), exponential backoff, dead-letter queue, async publish.
P1.8 — JasperReports integration
Module: new platform-reporting module
Depends on: P1.5
What: Wrap JasperReports compilation/rendering behind a ReportRenderer Spring component. Reports live in metadata__report (path or content). API exposes /api/v1/_meta/reports/{id}.pdf?param=... for synchronous rendering, with a job-based async path for big reports.
Acceptance: ship one core report (a User list) and confirm it renders to PDF with the correct locale-aware date and number formatting.
P1.9 — File store (local + S3)
Module: new platform-files module
Depends on: —
What: A FileStore interface with LocalFileStore and S3FileStore implementations, configured via vibeerp.files.backend. Files are stored under a flat key namespace — there is exactly one company per instance, so no per-tenant prefixing is needed.
Acceptance: unit tests for both backends; integration test with MinIO Testcontainer for S3.
P1.10 — Job scheduler
Module: new platform-jobs module
Depends on: —
What: Quartz integration with the JDBCJobStore against the same Postgres. PBCs and plug-ins register jobs through a typed JobScheduler API in api.v1 (already has the seam, may need a tiny addition). Each job runs inside a PrincipalContext.runAs(systemPrincipal) block so audit columns get a sensible value instead of inheriting whatever the trigger thread had.
Acceptance: a recurring core job that prunes the audit log; integration test confirms the job runs with the expected system principal in audit columns.
Phase 2 — Embedded workflow engine
P2.1 — Embedded Flowable
Module: new platform-workflow module
Depends on: P1.5 (metadata), P1.6 (i18n for task names), P1.7 (events)
What: Pull in flowable-spring-boot-starter. Configure Flowable to use the same Postgres datasource (so its tables live alongside ours, prefixed flowable_*). Wire org.vibeerp.api.v1.workflow.TaskHandler implementations registered by plug-ins as Flowable JavaDelegates. Workflows are deployed from metadata__workflow rows (BPMN XML payload) on boot and on plug-in load.
Acceptance: the reference printing-shop plug-in registers a "quote-to-job-card" TaskHandler; a BPMN process that calls it can be started via REST and returns the expected output.
P2.2 — BPMN designer integration (web)
Module: the future React SPA
Depends on: P2.1, R1 (web bootstrap)
What: Embed bpmn-js (Camunda's BPMN modeler) in a "Workflow Designer" page. Save as a metadata__workflow row; deploy through the Tier 1 path.
Acceptance: end-to-end test where a Tier 1 user creates a workflow in the browser and starts an instance of it.
P2.3 — User task / form rendering
Module: platform-workflow + future React SPA
Depends on: P2.1, P3.3 (form designer), R1
What: When a Flowable user task fires, look up the form id from the task definition, render it via the form renderer (P3.x), capture the user's input, and complete the task.
Acceptance: end-to-end test of an approval workflow where a human clicks a button.
Phase 3 — Metadata store: forms and rules (the Tier 1 user journeys)
P3.1 — JSON Schema form renderer (server)
Module: new platform-forms module
Depends on: P1.5
What: A FormRenderer that, given an entity instance and a FormSchema, returns a server-side JSON describing what to render. The actual rendering happens in the SPA (P3.2). The server-side piece is needed for non-browser clients (CLI, integration tests, AI agents).
Acceptance: snapshot tests of the rendered JSON for a User form with an added custom field.
P3.2 — Form renderer (web)
Module: future React SPA
Depends on: P3.1, R1
What: A React component that consumes JSON Schema + UI Schema and renders an interactive form. Use react-jsonschema-form as the base, customize the widget set for vibe_erp's design language.
Acceptance: Storybook for every widget type; e2e test that filling and submitting a form persists the data.
P3.3 — Form designer (web)
Module: future React SPA
Depends on: P3.2
What: A drag-and-drop designer that produces JSON Schema + UI Schema and saves into metadata__form. This is the Tier 1 entry point for "add a field, change a layout".
Acceptance: e2e test where a Tier 1 user adds a "Customer PO Reference" field to the Sales Order form without writing code.
P3.4 — Custom field application
Module: platform-metadata
Depends on: P1.5
What: A Hibernate interceptor (or JPA listener) that reads the entity's metadata__custom_field rows on save and validates the JSON in the ext column against them — required, type, max length, etc. Validation errors come back as 422 with a per-field map.
Acceptance: integration test that a User with a missing required custom field cannot be saved; one with a valid one round-trips.
P3.5 — Rules engine (simple "if X then Y")
Module: new platform-rules module
Depends on: P1.7 (events)
What: Listen to DomainEvents, evaluate metadata__rule rows scoped to that event type, run the configured action (send email, set field, call workflow, publish another event). Use a simple expression language (Spring SpEL — already on the classpath).
Acceptance: integration test where a rule "on UserCreated, set ext.greeting = 'Welcome'" actually fires.
P3.6 — List view designer (web)
Module: future React SPA
Depends on: R1
What: A grid component backed by metadata__list_view rows. Operators pick columns, filters, sort, save as a named list view.
Acceptance: e2e test where a user creates a "Active customers in Germany" list view and re-opens it later.
Phase 4 — Authentication and authorization (PROMOTED to next priority)
Promoted from "after the platform layer" to "do this next." With the single-tenant refactor, there is no second wall protecting data. Auth is the only security. Until P4.1 lands, vibe_erp is only safe to run on
localhost.
✅ P4.1 — Built-in JWT auth (DONE 2026-04-07)
Module: pbc-identity (extended) + new platform-security module
What landed: Username/password login backed by identity__user_credential (separate table). Argon2id via Spring Security's Argon2PasswordEncoder. HMAC-SHA256 JWTs with sub/username/iss/iat/exp/type. JwtIssuer mints, JwtVerifier decodes back to a typed DecodedToken. PrincipalContext (ThreadLocal) bridges Spring Security → audit listener so created_by/updated_by carry real user UUIDs. BootstrapAdminInitializer creates admin with a random 16-char password printed to logs on first boot of an empty identity__user. Spring Security filter chain in platform-security makes everything except /actuator/health, /api/v1/_meta/**, /api/v1/auth/login, /api/v1/auth/refresh require a valid Bearer token. GlobalExceptionHandler maps AuthenticationFailedException → 401 RFC 7807. 38 unit tests + 10 end-to-end smoke assertions all green.
P4.2 — OIDC integration
Module: pbc-identity
Depends on: P4.1
What: Plug Spring Security's OIDC client into the same Principal resolution. Configurable per instance (not per tenant — vibe_erp is single-tenant): which provider, which client id, which claim is the username. Keycloak is the smoke-test target.
Acceptance: Keycloak Testcontainer + integration test of a full OIDC code flow; the same UserController endpoints work without changes.
P4.3 — Permission checking (real)
Module: platform-security
Depends on: P4.1
What: Implement org.vibeerp.api.v1.security.PermissionCheck. Resolve permissions from metadata__role_permission rows for the current Principal's roles. Plug-ins register their own permissions via @Extension(point = PermissionRegistrar::class) (api.v1 may need a tiny addition here — deliberate, with a minor version bump).
Acceptance: integration test that a user without identity.user.create gets 403 from POST /api/v1/identity/users; with the role, 201.
Phase 5 — Core PBCs (the meat of the framework)
Each PBC follows the same 7-step recipe:
- Create the Gradle subproject under
pbc/ - Domain entities (extending
AuditedJpaEntity, withext jsonb) - Spring Data JPA repositories
- Application services
- REST controllers under
/api/v1/<pbc>/<resource> - Liquibase changelog (
db/changelog/<pbc>/) with rollback blocks - Cross-PBC facade in
api.v1.ext.<pbc>+ adapter in the PBC
No
tenant_idcolumns. No RLS policies. vibe_erp is single-tenant per instance — everything in the database belongs to the one company that owns the instance.
✅ P5.1 — pbc-catalog — items, units of measure (DONE 2026-04-08)
Uom and Item entities + JpaRepositories + Services (with cross-PBC validation: ItemService rejects unknown UoM codes) + REST controllers under /api/v1/catalog/uoms and /api/v1/catalog/items + CatalogApi facade in api.v1.ext + CatalogApiAdapter (filters inactive items at the boundary). Liquibase seeds 15 canonical UoMs at first boot (kg/g/t, m/cm/mm/km, m2, l/ml, ea/sheet/pack, h/min). 11 unit tests + end-to-end smoke. Pricing and configurable products are deferred to P5.10.
P5.2 — pbc-partners — customers, suppliers, contacts
Companies, addresses, contacts, contact channels. PII-tagged from day one (CLAUDE.md guardrail #6 / DSAR).
P5.3 — pbc-inventory — stock items, lots, locations, movements
Movements as immutable events; on-hand quantity is a projection. Locations are hierarchical.
P5.4 — pbc-warehousing — receipts, picks, transfers, counts
Built on top of inventory via api.v1.ext.inventory. The first PBC that proves the cross-PBC dependency rule under load.
P5.5 — pbc-orders-sales — quotes, sales orders, deliveries
Workflow-heavy. The reference printing-shop plug-in's quote-to-job-card flow exercises this.
P5.6 — pbc-orders-purchase — RFQs, POs, receipts
Mirror image of sales. Shares the document/approval pattern.
P5.7 — pbc-production (basic) — work orders, routings, operations
Generic discrete-manufacturing primitives. NOT printing-specific.
P5.8 — pbc-quality (basic) — inspection plans, results, holds
Generic; the printing-specific QC steps live in the plug-in.
P5.9 — pbc-finance (basic) — GL, journal entries, AR/AP minimal
Basic double-entry. Tax engines, multi-currency revaluation, consolidation are v1.2+.
P5.10 — pbc-catalog — pricing, configurable products
Defer until P5.5 (orders-sales) is real, since pricing only matters when orders exist.
Phase 6 — Web SPA
R1 — React + TypeScript bootstrap
Module: new web/ directory (Vite + React + TS)
Depends on: P4.1 (need a way to log in)
What: Vite scaffold, design system (likely Mantine or Radix), routing (React Router), data fetching (TanStack Query), generated TypeScript client from the backend's OpenAPI spec.
Acceptance: can log in, see a list of users, log out.
R2 — Identity screens
Depends on: R1 What: User list, user detail, role list, role detail, permission editor.
R3 — Customize / metadata UIs
Depends on: R1, P3.3, P3.6 What: The Tier 1 entry point. Form designer, list view designer, custom field designer, workflow designer.
R4 — One screen per core PBC
Depends on: R1, the matching PBC from Phase 5 What: Per-PBC list / detail / create / edit screens. Generated from metadata wherever possible; hand-built where the metadata-driven approach falls short (and those gaps become future enhancements to the metadata store).
Phase 7 — Reference plug-in (the executable acceptance test)
REF.1 — Real workflow handler
Module: reference-customer/plugin-printing-shop
Depends on: P2.1 (Flowable)
What: Replace the v0.1 hello-world with a real TaskHandler that converts a Quote into a JobCard, registered as acme.printingshop.quoteToJobCard. A BPMN file in the plug-in JAR drives a workflow that calls this handler.
Acceptance: integration test starts the workflow with a real Quote and asserts the JobCard appears.
REF.2 — Plate / ink / press domain
Module: same plug-in
Depends on: P1.4 (plug-in Liquibase application)
What: The plug-in defines its own entities (plugin_printingshop__plate, plugin_printingshop__ink_recipe, plugin_printingshop__press) via its own Liquibase changelog. It registers REST endpoints for them via @PluginEndpoint. It registers permissions like printingshop.plate.approve.
Acceptance: integration test that a printing-shop user can CRUD plates against /api/v1/plugins/printing-shop/plates.
REF.3 — Real reference forms and metadata
Module: same plug-in
Depends on: P3.3, P3.4
What: The plug-in ships YAML metadata for: a custom field on Sales Order ("printing job type"), a custom form for plate approval, an automation rule "on plate approved, dispatch to press". All loaded at plug-in start, all tagged source = 'plugin:printing-shop'.
Acceptance: end-to-end smoke test that loads the plug-in into a fresh instance and exercises the full quote-to-job-card-to-press flow without any code changes outside the plug-in.
Phase 8 — Hosted operations, AI agents, mobile (post-v1.0)
H1 — Instance provisioning console (hosted)
What: A separate operator console that, given a "create customer" request, provisions a fresh vibe_erp instance: spin up a dedicated Postgres database, deploy a vibe_erp container pointed at it, register DNS, hand the customer their bootstrap admin credentials. The vibe_erp framework itself is not changed for this — provisioning is a layer on top of the framework. Each customer gets their own process, their own database, their own backups, their own upgrade cadence. Acceptance: can provision and tear down a customer instance via a single CLI command.
A1 — MCP server
Module: new platform-mcp
Depends on: every PBC, every endpoint having a typed OpenAPI definition
What: Expose every REST endpoint as an MCP function. Permission-checked, locale-aware. A Tier 2 plug-in can register additional MCP functions via a new @McpFunction annotation in api.v1 (an additive change, minor version bump).
Acceptance: an LLM-driven agent can call vibe_erp.identity.users.create and the result is identical to a REST call.
M1 — React Native skeleton
Shared TypeScript types with the web SPA; first screens cover shop-floor scanning and approvals.
Tracking and ordering
A coarse dependency view (after the single-tenant refactor):
P4.1 — auth (PROMOTED) ─────────┐
P1.5 — metadata seed ──┐ ──┐ │
P1.6 — translator ──── (depends on P1.5)
P1.7 — events ─────────┘ │
P1.2 — linter ──┐
P1.3 — child ctx ┘
P1.4 — plug-in liquibase ── depends on P1.3
P1.8 — reports ── depends on P1.5
P1.9 — files
P1.10 — jobs
P2.1 — Flowable ── depends on P1.5, P1.6, P1.7
P3.x — metadata Tier 1 ── depends on P1.5
P5.x — core PBCs ── depend on P1.x complete + P4.x for permission checks
R1 — web bootstrap ── depends on P4.1
REF.x — reference plug-in ── depends on P1.4 + P2.1 + P3.3
Sensible ordering for one developer (single-tenant world):
P4.1 ✅ → P5.1 ✅ → P1.3 ✅ → P1.7 ✅ → P1.4 ✅ → P1.2 ✅ → P1.5 → P1.6 → P5.2 → P2.1 → P5.3 → P3.4 → R1 → ...
Next up after this commit: P1.5 (metadata seeder) and P5.2 (pbc-partners) are the strongest candidates. Metadata seeder unblocks the entire Tier 1 customization story; pbc-partners adds another business surface (customers / suppliers) that sales orders will reference. Either is a clean ~half-day chunk.
Sensible parallel ordering for a team of three:
- Dev A: P4.1, P1.5, P1.7, P1.6, P2.1
- Dev B: P1.4, P1.3, P1.2, P3.4, P3.1
- Dev C: P5.1 (catalog), P5.2 (partners), P4.3
Then converge on R1 (web) and the rest of Phase 5 in parallel.
Definition of "v1.0 ready"
v1.0 ships when, on a fresh Postgres, an operator can:
-
docker runthe image with a connection string and a company name - Read the bootstrap admin password from the logs
- Log in to the web SPA
- Drop the printing-shop plug-in JAR into
./plugins/ - Restart the container
- See the printing-shop's screens, custom fields, workflows, and permissions in the SPA
- Walk a quote through the printing shop's full workflow without writing any code
- Generate a PDF report for that quote in zh-CN
- Export a DSAR for one of their customers
…and the same image, against a fresh empty Postgres pointed at a different company, also serves a customer who has loaded a completely different plug-in expressing a completely different business — without any change to the core image. (Hosting both customers in one process is explicitly NOT a goal — they are two independent instances.)
That's the bar. Until that bar is met, the framework has not delivered on its premise.