# 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. > > **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 `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 (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___*` 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:' | '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 (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 `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 (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: 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 (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: 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.