Contributing to vibe_erp
vibe_erp is a framework that other developers will build on. The rules below exist so the framework stays reusable across customers and upgrade-safe across releases. They are not negotiable.
For the full architectural reasoning, see docs/superpowers/specs/2026-04-07-vibe-erp-architecture-design.md and CLAUDE.md.
The dependency rule
The Gradle build enforces this. CI fails on violations.
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
Consequences:
-
PBCs never import each other.
pbc-orders-salescannot declarepbc-inventoryas a dependency. Cross-PBC interaction goes through (a) the event bus or (b) service interfaces declared inapi.v1.ext.<pbc>. -
Plug-ins only see
api.v1. Importing anything fromplatform.*or any PBC's internal package fails the plug-in linter at install time. -
Reaching into
internalpackages via reflection is Grade D and rejected by the loader.
Adding to api.v1 is a one-way door — don't
api.v1 is the only semver-governed surface in the codebase. Once a class, method, or behavior is in api.v1, removing or renaming it requires a major version bump and a deprecation window of at least one major release.
When in doubt, keep things out of api.v1. It is much easier to grow the API deliberately later than to maintain a regretted symbol forever.
Adding to api.v1 is OK when:
- The reference printing-shop plug-in (or a plausible second customer plug-in) cannot express its requirement without it.
- The addition is a new type or a default-implemented method on an existing interface (binary-compatible).
- The addition has a written KDoc contract and at least one consuming test.
Adding to api.v1 is not OK when:
- It exists only to make one PBC easier to write — that belongs in
platform.*. - It leaks an implementation detail (Hibernate type, Spring annotation, JPA entity).
- It is "we might need it later" — wait until you do.
Commit message convention
Conventional Commits, scoped by module:
feat(pbc-identity): add SCIM provisioning endpoint
fix(api-v1): tolerate missing locale on Translator.format
chore(platform-plugins): bump PF4J to 3.x
docs(architecture): clarify outbox seam
test(pbc-catalog): cover JSONB ext field round-trip
refactor(platform-persistence): extract tenant filter
Allowed types: feat, fix, chore, docs, test, refactor, build, ci, perf, revert. Scope is the module name. Breaking changes use ! after the scope (e.g. feat(api-v1)!: …) and require a BREAKING CHANGE: footer.
Pull request checklist
Every PR must satisfy all of these:
- Tests cover the new behavior. New PBC code has unit tests; new endpoints have integration tests against a real Postgres.
- Every public
api.v1type has KDoc on the type and on every method, including parameter, return, and exception contracts. - Liquibase changesets ship with rollback blocks. CI rejects PRs without them.
- Any user-facing string is referenced via an i18n key, never concatenated. Default
en-UStranslation is added in the same PR. - No printing-specific terminology in
pbc/*orplatform/*. Printing concepts live inreference-customer/plugin-printing-shop/. - No new cross-PBC dependency. If you reached for one, design an
api.v1.ext.<pbc>interface or aDomainEventinstead. - Commit messages follow Conventional Commits.
- Documentation under
docs/is updated in the same PR if you added or changed a public seam.
Build, test, and plug-in load commands
# Build everything
./gradlew build
# Run the full test suite
./gradlew test
# Run a single PBC's tests
./gradlew :pbc:pbc-identity:test
# Build the api-v1 jar (the contract that plug-ins consume)
./gradlew :api:api-v1:jar
# Build the reference plug-in
./gradlew :reference-customer:plugin-printing-shop:jar
# Boot the distribution and load the reference plug-in
cp reference-customer/plugin-printing-shop/build/libs/*.jar /tmp/vibeerp/plugins/
./gradlew :distribution:bootRun
The plug-in loader logs every plug-in it scans, accepts, and rejects, with the reason. If a plug-in fails to load, the cause is in the boot log under the platform-plugins logger.
Style notes
- Kotlin: idiomatic Kotlin, no
!!, nolateinitoutside test fixtures, prefer data classes for value objects. - Public API: KDoc is required, not optional. Internal helpers may skip KDoc but should still be self-explanatory.
- No printing terminology in core, ever. "Item", "document", "operation", "work order" are generic. "Plate", "ink", "press", "color proof" are not.