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.
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
Module: platform-plugins
Depends on: —
What: At plug-in load time, scan the JAR's class files for any reference to org.vibeerp.platform.* or org.vibeerp.pbc.*.internal.* packages. If any are found, refuse to load and report a "grade D extension" error to the operator. Reuse ASM (already a transitive dep of Spring) for bytecode inspection.
Acceptance: unit test with a hand-crafted bad JAR that imports a fictitious org.vibeerp.platform.foo.Bar — the loader rejects it with a clear message.
P1.3 — Per-plug-in Spring child context
Module: platform-plugins
Depends on: P1.2 (linter must run first)
What: When a plug-in starts, create a Spring AnnotationConfigApplicationContext with the host context as parent. Register the plug-in's @Component classes (discovered via classpath scanning of its classloader). The plug-in's Plugin.start(context: PluginContext) is invoked with a PluginContext whose dependencies are wired from the parent context.
Acceptance: the reference printing-shop plug-in declares an @Extension and a @PluginEndpoint controller; the integration test confirms the endpoint is mounted under /api/v1/plugins/printing-shop/... and the extension is invocable.
P1.4 — Plug-in Liquibase application
Module: platform-plugins + platform-persistence
Depends on: P1.3
What: When a plug-in starts, look for db/changelog/master.xml in its JAR; if present, run it through Liquibase against the host's datasource with the plug-in's id as the changelog contexts filter. Tables created live under the plugin_<id>__* prefix (enforced by lint of the changelog at load time, not runtime).
Acceptance: a test plug-in ships a changelog that creates plugin_test__widget; after load, the table exists. Uninstall removes the table after operator confirmation.
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
Module: new platform-events module
Depends on: —
What: Implement org.vibeerp.api.v1.event.EventBus with two parts: (a) an in-process Spring ApplicationEventPublisher for synchronous delivery to listeners in the same process; (b) an event_outbox table written in the same DB transaction as the originating change, scanned by a background poller, marked dispatched after delivery. The outbox is the seam where Kafka/NATS plugs in later.
Acceptance: integration test that an OrderCreated event survives a process crash between the original transaction commit and a downstream listener — the listener fires when the process restarts.
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
Module: pbc-identity (extended) + platform-bootstrap (Spring Security config)
Depends on: —
What: Username/password login backed by identity__user_credential (separate table from identity__user so the User entity stays free of secrets). Argon2id for hashing via Spring Security's Argon2PasswordEncoder. Issue an HMAC-SHA256-signed JWT with sub (user id), username, iss = vibe-erp, iat, exp. Validate on every request via a Spring Security filter, bind the resolved Principal to a per-request PrincipalContext, and have the audit listener read from it (replacing today's __system__ placeholder). On first boot of an empty identity__user table, create a bootstrap admin with a random one-time password printed to the application logs.
Acceptance: end-to-end smoke test against the running app:
- unauthenticated
GET /api/v1/identity/users→ 401 -
POST /api/v1/auth/loginwith admin → 200 + access + refresh tokens -
GET /api/v1/identity/userswithAuthorization: Bearer …→ 200 - new user created via REST has
created_by = <admin user id>, not__system__
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, attributes
Foundation for everything that's bought, sold, made, or stored. Hold off on price lists and configurable products until 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 → P1.5 → P1.7 → P1.6 → P1.2 → P1.3 → P1.4 → P1.10 → P2.1 → P3.4 → P3.1 → P5.1 → P5.2 → R1 → ...
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.