diff --git a/distribution/build.gradle.kts b/distribution/build.gradle.kts
new file mode 100644
index 0000000..45dea42
--- /dev/null
+++ b/distribution/build.gradle.kts
@@ -0,0 +1,52 @@
+// distribution — assembles the runnable vibe_erp fat-jar.
+//
+// This module is the only one that pulls every PBC and platform module
+// together into a bootable Spring Boot application. It is referenced by
+// the Dockerfile (stage 1) which produces the shipping image documented
+// in the architecture spec, sections 10 and 11.
+
+plugins {
+ alias(libs.plugins.kotlin.jvm)
+ alias(libs.plugins.kotlin.spring)
+ alias(libs.plugins.spring.boot)
+ alias(libs.plugins.spring.dependency.management)
+}
+
+java {
+ toolchain {
+ languageVersion.set(JavaLanguageVersion.of(21))
+ }
+}
+
+dependencies {
+ implementation(project(":platform:platform-bootstrap"))
+ implementation(project(":platform:platform-persistence"))
+ implementation(project(":platform:platform-plugins"))
+ implementation(project(":pbc:pbc-identity"))
+
+ implementation(libs.spring.boot.starter)
+ implementation(libs.spring.boot.starter.web)
+ implementation(libs.spring.boot.starter.data.jpa)
+ implementation(libs.spring.boot.starter.actuator)
+ implementation(libs.kotlin.stdlib)
+ implementation(libs.kotlin.reflect)
+ implementation(libs.jackson.module.kotlin)
+ runtimeOnly(libs.postgres)
+ runtimeOnly(libs.liquibase.core)
+
+ testImplementation(libs.spring.boot.starter.test)
+}
+
+// The fat-jar produced here is what the Dockerfile copies into the
+// runtime image as /app/vibe-erp.jar.
+tasks.bootJar {
+ mainClass.set("org.vibeerp.platform.bootstrap.VibeErpApplicationKt")
+ archiveFileName.set("vibe-erp.jar")
+}
+
+// `./gradlew :distribution:bootRun` — used by `make run` for local dev.
+// Activates the dev profile so application-dev.yaml on the classpath is
+// layered on top of application.yaml.
+tasks.bootRun {
+ systemProperty("spring.profiles.active", "dev")
+}
diff --git a/distribution/src/main/resources/application-dev.yaml b/distribution/src/main/resources/application-dev.yaml
new file mode 100644
index 0000000..0d66f6f
--- /dev/null
+++ b/distribution/src/main/resources/application-dev.yaml
@@ -0,0 +1,21 @@
+# vibe_erp — developer overrides (active under -Dspring.profiles.active=dev).
+#
+# Activated by `./gradlew :distribution:bootRun` (see distribution/build.gradle.kts).
+# These values point at a local Postgres started via `make up` or any other
+# locally-running Postgres on port 5432.
+
+spring:
+ datasource:
+ url: jdbc:postgresql://localhost:5432/vibeerp
+ username: vibeerp
+ password: vibeerp
+
+vibeerp:
+ plugins:
+ directory: ./plugins-dev
+ files:
+ local-path: ./files-dev
+
+logging:
+ level:
+ org.vibeerp: DEBUG
diff --git a/distribution/src/main/resources/application.yaml b/distribution/src/main/resources/application.yaml
new file mode 100644
index 0000000..f6cb728
--- /dev/null
+++ b/distribution/src/main/resources/application.yaml
@@ -0,0 +1,56 @@
+# vibe_erp — production-ish defaults.
+#
+# This is the baseline configuration baked into the shipping image. It is
+# deliberately non-secret: every sensitive value is read from an environment
+# variable so the same image works for self-hosted and (eventually) hosted
+# deployments. See architecture spec sections 10 and 11.
+#
+# Customer overrides live in /opt/vibe-erp/config/vibe-erp.yaml on the
+# mounted volume. Plug-in configuration lives in metadata__plugin_config,
+# never here.
+
+spring:
+ application:
+ name: vibe-erp
+ datasource:
+ url: ${VIBEERP_DB_URL}
+ username: ${VIBEERP_DB_USER}
+ password: ${VIBEERP_DB_PASSWORD}
+ driver-class-name: org.postgresql.Driver
+ jpa:
+ # Liquibase owns the schema; Hibernate must never touch DDL.
+ hibernate:
+ ddl-auto: validate
+ open-in-view: false
+ liquibase:
+ change-log: classpath:db/changelog/master.xml
+
+server:
+ port: 8080
+ shutdown: graceful
+
+management:
+ endpoints:
+ web:
+ exposure:
+ include: health,info,prometheus
+
+vibeerp:
+ instance:
+ mode: ${VIBEERP_INSTANCE_MODE:self-hosted}
+ default-tenant: ${VIBEERP_DEFAULT_TENANT:default}
+ plugins:
+ directory: ${VIBEERP_PLUGINS_DIR:/opt/vibe-erp/plugins}
+ auto-load: true
+ i18n:
+ default-locale: en-US
+ fallback-locale: en-US
+ available-locales: en-US,zh-CN,de-DE,ja-JP,es-ES
+ files:
+ backend: local
+ local-path: ${VIBEERP_FILES_DIR:/opt/vibe-erp/files}
+
+logging:
+ level:
+ org.vibeerp: INFO
+ org.springframework: WARN
diff --git a/distribution/src/main/resources/db/changelog/master.xml b/distribution/src/main/resources/db/changelog/master.xml
new file mode 100644
index 0000000..32931c9
--- /dev/null
+++ b/distribution/src/main/resources/db/changelog/master.xml
@@ -0,0 +1,15 @@
+
+
+
+
+
+
diff --git a/distribution/src/main/resources/db/changelog/pbc-identity/001-identity-init.xml b/distribution/src/main/resources/db/changelog/pbc-identity/001-identity-init.xml
new file mode 100644
index 0000000..c2284f5
--- /dev/null
+++ b/distribution/src/main/resources/db/changelog/pbc-identity/001-identity-init.xml
@@ -0,0 +1,125 @@
+
+
+
+
+
+
+ Create identity__user table
+
+ CREATE TABLE identity__user (
+ id uuid PRIMARY KEY,
+ tenant_id varchar(64) NOT NULL,
+ username varchar(128) NOT NULL,
+ display_name varchar(256) NOT NULL,
+ email varchar(320),
+ enabled boolean NOT NULL DEFAULT true,
+ ext jsonb NOT NULL DEFAULT '{}'::jsonb,
+ created_at timestamptz NOT NULL,
+ created_by varchar(128) NOT NULL,
+ updated_at timestamptz NOT NULL,
+ updated_by varchar(128) NOT NULL,
+ version bigint NOT NULL DEFAULT 0
+ );
+ CREATE UNIQUE INDEX identity__user_tenant_username_uk
+ ON identity__user (tenant_id, username);
+ CREATE INDEX identity__user_ext_gin
+ ON identity__user USING GIN (ext jsonb_path_ops);
+
+
+ DROP TABLE identity__user;
+
+
+
+
+ Enable Row-Level Security on identity__user (advisory until RlsTransactionHook lands)
+
+ ALTER TABLE identity__user ENABLE ROW LEVEL SECURITY;
+ CREATE POLICY identity__user_tenant_isolation ON identity__user
+ USING (tenant_id = current_setting('vibeerp.current_tenant', true));
+
+
+ DROP POLICY IF EXISTS identity__user_tenant_isolation ON identity__user;
+ ALTER TABLE identity__user DISABLE ROW LEVEL SECURITY;
+
+
+
+
+ Create identity__role table
+
+ CREATE TABLE identity__role (
+ id uuid PRIMARY KEY,
+ tenant_id varchar(64) NOT NULL,
+ code varchar(64) NOT NULL,
+ name varchar(256) NOT NULL,
+ description text,
+ ext jsonb NOT NULL DEFAULT '{}'::jsonb,
+ created_at timestamptz NOT NULL,
+ created_by varchar(128) NOT NULL,
+ updated_at timestamptz NOT NULL,
+ updated_by varchar(128) NOT NULL,
+ version bigint NOT NULL DEFAULT 0
+ );
+ CREATE UNIQUE INDEX identity__role_tenant_code_uk
+ ON identity__role (tenant_id, code);
+ CREATE INDEX identity__role_ext_gin
+ ON identity__role USING GIN (ext jsonb_path_ops);
+ ALTER TABLE identity__role ENABLE ROW LEVEL SECURITY;
+ CREATE POLICY identity__role_tenant_isolation ON identity__role
+ USING (tenant_id = current_setting('vibeerp.current_tenant', true));
+
+
+ DROP TABLE identity__role;
+
+
+
+
+ Create identity__user_role join table
+
+ CREATE TABLE identity__user_role (
+ id uuid PRIMARY KEY,
+ tenant_id varchar(64) NOT NULL,
+ user_id uuid NOT NULL REFERENCES identity__user(id) ON DELETE CASCADE,
+ role_id uuid NOT NULL REFERENCES identity__role(id) ON DELETE CASCADE,
+ created_at timestamptz NOT NULL,
+ created_by varchar(128) NOT NULL,
+ updated_at timestamptz NOT NULL,
+ updated_by varchar(128) NOT NULL,
+ version bigint NOT NULL DEFAULT 0
+ );
+ CREATE UNIQUE INDEX identity__user_role_uk
+ ON identity__user_role (tenant_id, user_id, role_id);
+ ALTER TABLE identity__user_role ENABLE ROW LEVEL SECURITY;
+ CREATE POLICY identity__user_role_tenant_isolation ON identity__user_role
+ USING (tenant_id = current_setting('vibeerp.current_tenant', true));
+
+
+ DROP TABLE identity__user_role;
+
+
+
+
diff --git a/distribution/src/main/resources/db/changelog/platform/000-platform-init.xml b/distribution/src/main/resources/db/changelog/platform/000-platform-init.xml
new file mode 100644
index 0000000..536378a
--- /dev/null
+++ b/distribution/src/main/resources/db/changelog/platform/000-platform-init.xml
@@ -0,0 +1,411 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+