CLAUDE.md
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
Project intent
vibe_erp is an ERP framework (not an ERP application) aimed at the printing industry. The goal is a pluggable platform on which any printing business — with its own workflows, forms, roles, and rules — can be assembled by configuration and plug-in modules, not by forking or rewriting the core.
The reference business in raw/业务流程设计文档/ is one example customer, not the spec. It must be treated as a fixture to validate that the framework can express such a workflow, never as a source of hard-coded entities or features in the core.
Top-level domains in the reference doc (use only as inspiration / test cases):
销售管理 (sales) · 采购管理 (purchasing) · 材料仓库管理 (raw material warehouse) · 成品仓库管理 (finished goods warehouse) · 生产管理 (production) · 工艺管理 (process/craft) · 车间管理 (workshop) · 设备管理 (equipment) · 品质管理 (quality) · 财务管理 (finance) · 项目管理 (project)
Architectural guardrails
These rules exist because the whole point of the project is reusability across customers. Violating them defeats the project.
- Core stays domain-agnostic. No printing-specific entity, field, status, or workflow step belongs in the framework core. "Plate", "ink", "press", "color proof", etc. live in plug-in modules or configuration — never in core tables, core types, or core code paths.
- Workflows are data, not code. State machines, approval chains, document routing, and form definitions must be declarative (config / DB / DSL) so a new customer can be onboarded by editing definitions, not by editing source.
- Extensibility seams come first. Before adding any feature, identify the extension point (hook, plug-in interface, event, custom field, scripting hook) it should hang off of. If no seam exists yet, design the seam first, then implement the reference customer's behavior on top of it.
-
The reference customer is a test, not a requirement. When implementing something inspired by
raw/业务流程设计文档/, ask: "Could a different printing shop with different rules also be expressed here without code changes?" If no, redesign. -
Single-tenant per instance, isolated database. vibe_erp is deliberately NOT multi-tenant. One running instance serves exactly one company against an isolated Postgres database. Hosting many customers means provisioning many independent instances (each with its own DB), not multiplexing them in one process. There are no
tenant_idcolumns, no row-level tenant filtering, no Row-Level Security, noTenantContext, and no per-request tenant resolution anywhere in the framework. Customer isolation is a deployment concern, not a code concern. Data models and APIs may freely assume "one company, one process, one DB" — but they must still allow per-customer customization through configuration, plug-ins, and metadata so the SAME image runs for any customer. - Global / i18n from day one. This codebase is the foundation of an ERP/EBC product intended to be sold worldwide. No hard-coded user-facing strings, no hard-coded currency, date format, time zone, number format, address shape, tax model, or language. All such concerns must go through localization, formatting, and configuration layers — even in the very first prototype. The reference docs being in Chinese does not mean Chinese is the primary or default locale; it is just one supported locale among many.
-
Clean Core. Borrowed from SAP S/4HANA's vocabulary: extensions never modify the core. The core is stable, generic, and upgrade-safe; everything customer-specific lives in plug-ins or in metadata rows. Extensions are graded A/B/C/D by upgrade safety:
- A = Tier-1 metadata only (custom fields, forms, workflows, rules) — always upgrade-safe
-
B = Tier-2 plug-in using only the public
api.v1.*surface — safe within a major version -
C = Tier-2 plug-in using deprecated
api.v1symbols — safe until next major; loader emits warnings -
D = Tier-2 plug-in reaching into
platform.*orpbc.*.internal.*via reflection — UNSUPPORTED, rejected by the plug-in linter at install time
-
Two-tier extensibility, both first-class. The framework offers two extension paths and both must be designed for, not bolted on:
-
Tier 1 — key user / no-code: business analysts add fields, forms, list views, workflows, automation rules, custom entities, reports, and translations through the web UI. Everything is stored as rows in the
metadata__*tables (Doctype-style), taggedsource = 'user', scoped to their tenant. No build, no restart. The OpenAPI spec, REST API, and AI-agent function catalog auto-update from the metadata. -
Tier 2 — developer / pro-code: PF4J JAR plug-ins. Plug-ins see only
org.vibeerp.api.v1.*. Importing anything fromplatform.*or any PBC's internal package fails the plug-in linter.
-
Tier 1 — key user / no-code: business analysts add fields, forms, list views, workflows, automation rules, custom entities, reports, and translations through the web UI. Everything is stored as rows in the
-
PBC boundaries are sacred. Each core capability (
pbc-identity,pbc-catalog,pbc-inventory,pbc-orders-sales,pbc-production, …) is its own Packaged Business Capability: own Gradle subproject, own table-name prefix, own bounded context, own public API surface. PBCs never import each other. Cross-PBC interaction goes through (a) the event bus or (b) service interfaces declared inapi.v1.ext.<pbc>. The Gradle build enforces this —pbc-orders-salescannot declarepbc-inventoryas a dependency. This is what makes "modular monolith now, splittable later" real instead of aspirational. -
api.v1is the only stable contract. The packageorg.vibeerp.api.v1.*is the single semver-governed surface that plug-ins consume. It is published asapi-v1.jarto Maven Central. Everything else in the codebase (platform.*,pbc.*.internal.*, every concrete Spring bean) is internal and may change in any release. Adding toapi.v1is allowed within a major version; renaming, removing, or behavior-changing anything inapi.v1is a major version bump. When in doubt, keep things OUT ofapi.v1; it is easier to grow the API deliberately than to maintain a regretted symbol forever. - AI agents are a first-class client. The framework is built so that the same business operations exposed via REST/OpenAPI are also callable by LLM-driven agents through an MCP server (or equivalent function-calling surface). The MCP endpoint itself is a v1.1 deliverable, but v1.0 must not architect it out — operations must be discoverable, typed, permission-checked, and locale-aware in a way that lets an agent call them safely. AI agents are listed alongside humans, web clients, mobile clients, and integrations as supported callers.
Working with the reference docs
-
raw/业务流程设计文档/is in Chinese. Module and process names are domain terms (e.g.工艺= process/craft,车间= workshop floor,品质= QC). Preserve original terminology when quoting; do not silently rename concepts when discussing them. - Treat these documents as read-only reference material. Do not move, restructure, or "clean up"
raw/. - When the user asks for a feature "from the doc", confirm whether they want it built as a reference plug-in / fixture on top of the framework, or as a core capability — the answer is almost always the former.
Documentation discipline
Because this is a framework that other developers (and future-you) will build on, documentation is part of "done", not an afterthought. As the repo grows:
- Maintain a top-level
docs/tree alongside the code. At minimum it should cover: architecture overview, extension points / plug-in API, workflow & form definition format, configuration reference, i18n/localization guide, and a getting-started guide for onboarding a new customer. - Every new extension point, plug-in interface, config key, or workflow primitive added to the core must ship with documentation in the same change. A PR that adds a public seam without docs is incomplete.
- Keep
raw/业务流程设计文档/separate fromdocs/.raw/is one customer's business reference (read-only fixture).docs/is the framework's own documentation, written in English by default with localization where appropriate. - When a doc and the code disagree, treat it as a bug — fix whichever is wrong, do not silently let docs rot.
Stack and shape (decided, not yet built)
The architecture has been brainstormed, validated against 2026 ERP/EBC SOTA, and approved. The full design lives at docs/superpowers/specs/2026-04-07-vibe-erp-architecture-design.md and is the source of truth for everything below.
- Backend: Kotlin on the JVM, Spring Boot, single fat-JAR / single Docker image
- Workflow engine: Embedded Flowable (BPMN 2.0)
- Persistence: PostgreSQL (the only mandatory external dependency)
- Multi-tenancy: intentionally none. Single-tenant per instance, one isolated Postgres database per customer (see guardrail #5)
-
Custom fields: JSONB
extcolumn on every business table, described bymetadata__custom_fieldrows; GIN-indexed - Plug-in framework: PF4J + Spring Boot child contexts (classloader isolation per plug-in)
-
i18n: ICU MessageFormat (ICU4J) + Spring
MessageSource - Reporting: JasperReports
- Auth: Built-in JWT + OIDC (Keycloak-compatible)
- Web client (v1): React + TypeScript SPA; mobile (React Native) is v2
- API: REST + OpenAPI; MCP server is v1.1 (seam exists in v1.0)
Topology: one Spring Boot process per instance, one Docker image per release, one mounted volume (/opt/vibe-erp/) for config/, plugins/, i18n-overrides/, files/, logs/. Customer extensions live outside the image. Upgrading vibe_erp = swapping the image; extensions and config stay put.
Module layout (Gradle multi-project):
vibe-erp/
├── api/api-v1/ ← THE CONTRACT (semver, published to Maven Central)
├── platform/* ← Framework runtime (internal, not exposed to plug-ins)
├── pbc/* ← Core PBCs: identity, catalog, partners, inventory,
│ warehousing, orders-sales, orders-purchase,
│ production, quality, finance
├── reference-customer/
│ └── plugin-printing-shop/ ← Reference plug-in expressing raw/业务流程设计文档/.
│ Built and CI-tested; NOT loaded by default.
├── web/ ← React + TypeScript SPA
└── docs/ ← Framework documentation
Dependency rule (enforced by the Gradle build):
api/api-v1 depends on: nothing (Kotlin stdlib + jakarta.validation only)
platform/* depends on: api/api-v1 + Spring + libs
pbc/* depends on: api/api-v1 + platform/* (NEVER another pbc)
plugins (incl. ref) depend on: api/api-v1 only
Repository state
Foundation complete; first business surface in place. As of the latest commit:
-
18 Gradle subprojects built and tested. The dependency rule (PBCs never import each other; plug-ins only see
api.v1) is enforced at configuration time by the rootbuild.gradle.kts. -
246 unit tests across 18 modules, all green.
./gradlew buildis the canonical full build. -
All 9 cross-cutting platform services live and smoke-tested end-to-end against real Postgres: auth (P4.1), authorization with
@RequirePermission+ JWT roles claim + AOP enforcement (P4.3), plug-in HTTP endpoints (P1.3), plug-in linter (P1.2), plug-in-owned Liquibase + typed SQL (P1.4), event bus + transactional outbox (P1.7), metadata loader + custom-field validation (P1.5 + P3.4), ICU4J translator with per-plug-in locale chain (P1.6), plus the audit/principal context bridge. -
8 of 10 core PBCs implemented end-to-end (
pbc-identity,pbc-catalog,pbc-partners,pbc-inventory,pbc-orders-sales,pbc-orders-purchase,pbc-finance,pbc-production). The full buy-sell-make loop works: a purchase order receives stock viaPURCHASE_RECEIPT, a sales order ships stock viaSALES_SHIPMENT, and a work order produces stock viaPRODUCTION_RECEIPT. All three PBCs feed the sameinventory__stock_movementledger via the sameInventoryApi.recordMovementfacade. -
Event-driven cross-PBC integration is live in BOTH directions and the consumer reacts to the full lifecycle. Six typed events under
org.vibeerp.api.v1.event.orders.*are published fromSalesOrderServiceandPurchaseOrderServiceinside the same@Transactionalmethod as their state changes. pbc-finance subscribes to ALL SIX of them via the api.v1EventBus.subscribe(eventType, listener)typed-class overload: confirm events POST AR/AP rows; ship/receive events SETTLE them; cancel events REVERSE them. Each transition is idempotent under at-least-once delivery — re-applying the same destination status is a no-op. Cancel-from-DRAFT is a clean no-op because no*ConfirmedEventwas ever published. pbc-finance has no source dependency on pbc-orders-*; it reaches them only through events. -
Reference printing-shop plug-in owns its own DB schema, CRUDs plates and ink recipes via REST through
context.jdbc, ships its own metadata YAML, and registers 7 HTTP endpoints. It is the executable acceptance test for the framework. -
Package root is
org.vibeerp. -
Build commands:
./gradlew build(full),./gradlew test(unit tests),./gradlew :distribution:bootRun(run dev profile, stages plug-in JAR automatically),docker compose up -d db(Postgres). The bootstrap admin password is printed to the application logs on first boot. -
Documentation: site under
docs/. Architecture spec and implementation plan underdocs/superpowers/specs/. Live progress tracker inPROGRESS.md. - Pushed to https://github.com/reporkey/vibe-erp with GitHub Actions CI on every commit.
For the per-unit status of every implementation plan item and the list of deferred work, see PROGRESS.md.
What to do when asked to "just add" a printing-specific feature
Stop and reframe. The correct shape is almost always:
- a generic mechanism in the core (e.g. "documents with configurable line items and a configurable approval workflow"), plus
- a plug-in / config bundle that uses that mechanism to express the specific printing-shop behavior.
If you cannot see how to split a request that way, ask the user before writing code.
The reference plug-in is the executable acceptance test
reference-customer/plugin-printing-shop/ is the framework's primary correctness test. It expresses the workflows in raw/业务流程设计文档/ using only api.v1. Two consequences:
- If a feature in
pbc/*exists only to make this plug-in work, the design has failed guardrail #1 — that feature must move into the plug-in. - If the plug-in needs to reach into a
platform.*orpbc.*internal class, the seam is wrong andapi.v1needs to grow (deliberately, with a version bump consideration).
CI must build and load this plug-in in an integration test environment against a real Postgres on every PR. A green core build with a red printing-shop integration test is a release-blocker.