vibe_erp — Implementation Plan (post-v0.1)
Status: Approved roadmap. v0.1 skeleton is built. 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
What v0.1 (this commit) actually delivers
A buildable, runnable, tested skeleton of the vibe_erp framework. Concretely:
| Item | Status |
|---|---|
| Gradle multi-project with 7 subprojects | ✅ Builds |
api/api-v1 — public plug-in contract (~30 files, semver-governed) |
✅ Compiles, unit-tested |
platform/platform-bootstrap — Spring Boot main, config, tenant filter |
✅ Compiles |
platform/platform-persistence — multi-tenant JPA scaffolding, audit base |
✅ Compiles |
platform/platform-plugins — PF4J host, lifecycle |
✅ Compiles |
pbc/pbc-identity — User entity end-to-end (entity → repo → service → REST) |
✅ Compiles, unit-tested |
reference-customer/plugin-printing-shop — hello-world PF4J plug-in |
✅ Compiles, packs into a real plug-in JAR with PF4J manifest |
distribution — Spring Boot fat jar (vibe-erp.jar, ~59 MB) |
✅ bootJar succeeds |
| Liquibase changelogs (platform init + identity init) | ✅ Schema-valid, runs against Postgres on first boot |
| Dockerfile (multi-stage), docker-compose.yaml (app + Postgres), Makefile | ✅ Present, image build untested in this session |
| GitHub Actions workflows (build + architecture-rule check) | ✅ Present, untested in CI |
Architecture-rule enforcement in build.gradle.kts
|
✅ Active — fails the build if a PBC depends on another PBC, or if a plug-in depends on platform/*
|
| README, CONTRIBUTING, LICENSE, 8 docs guides | ✅ Present |
| Architecture spec (this folder) | ✅ Present |
| Implementation plan (this file) | ✅ Present |
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 - Postgres Row-Level Security
SET LOCALhook (the policies exist; the hook that setsvibeerp.current_tenantper transaction is deferred) - JasperReports
- OIDC integration
- 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.1 — Postgres RLS transaction hook
Module: platform-persistence
Why: v0.1 enables RLS policies but never sets current_setting('vibeerp.current_tenant'). Without the hook, RLS-enabled tables return zero rows. Block on this for any read/write integration test.
What: A Hibernate StatementInspector (or a Spring TransactionSynchronization) that runs SET LOCAL vibeerp.current_tenant = '<tenant_id>' at the start of every transaction, reading from TenantContext.
Acceptance: integration test with Testcontainers Postgres, two tenants, one user per tenant, asserts that tenant A's queries return zero rows from tenant B's data even when the Hibernate filter is bypassed.
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 (tenant 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 a tenant 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. Each stored file has a tenant_id namespace baked into its key, so a misconfigured S3 bucket can't accidentally serve cross-tenant files.
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 TenantContext.runAs(tenantId) { ... } block — never inheriting a stale tenant from the trigger thread.
Acceptance: a recurring core job that prunes the audit log; integration test confirms the job runs in the right tenant context.
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, F1 (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 (something like JEXL or Spring SpEL — bias toward Spring SpEL since we already have it).
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. Tier 1 users pick columns, filters, sort, save as a named list view, share with their tenant.
Acceptance: e2e test where a user creates a "Active customers in Germany" list view and re-opens it later.
Phase 4 — Authentication and authorization (real)
P4.1 — Built-in JWT auth
Module: new pbc-auth (under pbc/) + platform-security (might already exist)
Depends on: —
What: Username/password login backed by identity__user_credential (separate table from identity__user so the User entity stays small). Argon2id for hashing. Issue a signed JWT with sub, tenant, roles, iat, exp. Validate on every request via a Spring Security filter.
Acceptance: integration test of full login/logout/refresh flow; assertion that the JWT carries tenant and that downstream PBC code sees it via Principal.
P4.2 — OIDC integration
Module: pbc-auth
Depends on: P4.1
What: Plug Spring Security's OIDC client into the same Principal resolution. Configurable per tenant: which provider, which client id, which claim maps to tenant. 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 AND RLS policies - Cross-PBC facade in
api.v1.ext.<pbc>+ adapter in the PBC
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, scoped to their tenant.
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 tenant and exercises the full quote-to-job-card-to-press flow without any code changes outside the plug-in.
Phase 8 — Hosted, AI agents, mobile (post-v1.0)
H1 — Per-region tenant routing (hosted)
Make platform-persistence look up the tenant's region from identity__tenant and route the connection accordingly. v1.0 doesn't ship hosted, but the routing seam should land in v1.0 so v2.0 can flip a switch.
H2 — Tenant provisioning UI
A per-instance "operator console" that creates tenants, manages quotas, watches health. Separate React app, separate URL.
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, tenant-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:
P1.1 — RLS hook ─────┐
P1.2 — linter ──┐ │
P1.3 — child ctx ┘ │
P1.4 — plug-in liquibase ─ depends on P1.3
P1.5 — metadata seed ──┐ ──┐
P1.6 — translator ──── (depends on P1.5)
P1.7 — events ─────────┘ │
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)
P4.x — auth
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: P1.1 → P1.5 → P1.7 → P1.6 → P1.10 → P1.4 → P1.3 → P1.2 → P2.1 → P3.4 → P3.1 → P4.1 → P5.1 → P5.2 → ...
Sensible parallel ordering for a team of three:
- Dev A: P1.1, P1.5, P1.7, P1.6, P2.1
- Dev B: P1.4, P1.3, P1.2, P3.4, P3.1
- Dev C: P4.1, P4.3, P5.1 (catalog), P5.2 (partners)
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 - Log in to the web SPA
- Create a tenant (or use the default tenant in self-host mode)
- 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, also serves a customer who has loaded a completely different plug-in expressing a completely different business — without any change to the core image.
That's the bar. Until that bar is met, the framework has not delivered on its premise.