# 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-vibe-erp-architecture-design.md) > **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_id` columns, no Hibernate tenant > filter, no Postgres Row-Level Security policies, no `TenantContext`. 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 `Translator` implementation - 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___*` 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:' | '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:'` 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_.properties` → core `i18n/messages_.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 `JavaDelegate`s. 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 `DomainEvent`s, 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/login` with admin → 200 + access + refresh tokens - `GET /api/v1/identity/users` with `Authorization: Bearer …` → 200 - new user created via REST has `created_by = `, 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: 1. Create the Gradle subproject under `pbc/` 2. Domain entities (extending `AuditedJpaEntity`, with `ext jsonb`) 3. Spring Data JPA repositories 4. Application services 5. REST controllers under `/api/v1//` 6. Liquibase changelog (`db/changelog//`) with rollback blocks 7. Cross-PBC facade in `api.v1.ext.` + adapter in the PBC > No `tenant_id` columns. 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: 1. `docker run` the image with a connection string and a company name 2. Read the bootstrap admin password from the logs 3. Log in to the web SPA 4. Drop the printing-shop plug-in JAR into `./plugins/` 5. Restart the container 6. See the printing-shop's screens, custom fields, workflows, and permissions in the SPA 7. Walk a quote through the printing shop's full workflow without writing any code 8. Generate a PDF report for that quote in zh-CN 9. 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.