diff --git a/cameleer3-server-app/src/main/java/com/cameleer3/server/app/config/RuntimeBeanConfig.java b/cameleer3-server-app/src/main/java/com/cameleer3/server/app/config/RuntimeBeanConfig.java
new file mode 100644
index 00000000..104d83b2
--- /dev/null
+++ b/cameleer3-server-app/src/main/java/com/cameleer3/server/app/config/RuntimeBeanConfig.java
@@ -0,0 +1,76 @@
+package com.cameleer3.server.app.config;
+
+import com.cameleer3.server.app.storage.PostgresAppRepository;
+import com.cameleer3.server.app.storage.PostgresAppVersionRepository;
+import com.cameleer3.server.app.storage.PostgresDeploymentRepository;
+import com.cameleer3.server.app.storage.PostgresEnvironmentRepository;
+import com.cameleer3.server.core.runtime.AppRepository;
+import com.cameleer3.server.core.runtime.AppService;
+import com.cameleer3.server.core.runtime.AppVersionRepository;
+import com.cameleer3.server.core.runtime.DeploymentRepository;
+import com.cameleer3.server.core.runtime.DeploymentService;
+import com.cameleer3.server.core.runtime.EnvironmentRepository;
+import com.cameleer3.server.core.runtime.EnvironmentService;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.jdbc.core.JdbcTemplate;
+import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
+
+import java.util.concurrent.Executor;
+
+/**
+ * Creates runtime management beans: repositories, services, and async executor.
+ *
+ * Follows the established pattern: core module plain class, app module bean config.
+ */
+@Configuration
+public class RuntimeBeanConfig {
+
+ @Bean
+ public EnvironmentRepository environmentRepository(JdbcTemplate jdbc) {
+ return new PostgresEnvironmentRepository(jdbc);
+ }
+
+ @Bean
+ public AppRepository appRepository(JdbcTemplate jdbc) {
+ return new PostgresAppRepository(jdbc);
+ }
+
+ @Bean
+ public AppVersionRepository appVersionRepository(JdbcTemplate jdbc) {
+ return new PostgresAppVersionRepository(jdbc);
+ }
+
+ @Bean
+ public DeploymentRepository deploymentRepository(JdbcTemplate jdbc) {
+ return new PostgresDeploymentRepository(jdbc);
+ }
+
+ @Bean
+ public EnvironmentService environmentService(EnvironmentRepository repo) {
+ return new EnvironmentService(repo);
+ }
+
+ @Bean
+ public AppService appService(AppRepository appRepo, AppVersionRepository versionRepo,
+ @Value("${cameleer.runtime.jar-storage-path:/data/jars}") String jarStoragePath) {
+ return new AppService(appRepo, versionRepo, jarStoragePath);
+ }
+
+ @Bean
+ public DeploymentService deploymentService(DeploymentRepository deployRepo, AppService appService, EnvironmentService envService) {
+ return new DeploymentService(deployRepo, appService, envService);
+ }
+
+ @Bean(name = "deploymentExecutor")
+ public Executor deploymentTaskExecutor() {
+ ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
+ executor.setCorePoolSize(4);
+ executor.setMaxPoolSize(4);
+ executor.setQueueCapacity(25);
+ executor.setThreadNamePrefix("deploy-");
+ executor.initialize();
+ return executor;
+ }
+}
diff --git a/cameleer3-server-app/src/main/java/com/cameleer3/server/app/storage/PostgresAppRepository.java b/cameleer3-server-app/src/main/java/com/cameleer3/server/app/storage/PostgresAppRepository.java
new file mode 100644
index 00000000..2d5ec3e9
--- /dev/null
+++ b/cameleer3-server-app/src/main/java/com/cameleer3/server/app/storage/PostgresAppRepository.java
@@ -0,0 +1,66 @@
+package com.cameleer3.server.app.storage;
+
+import com.cameleer3.server.core.runtime.App;
+import com.cameleer3.server.core.runtime.AppRepository;
+import org.springframework.jdbc.core.JdbcTemplate;
+
+import java.sql.ResultSet;
+import java.sql.SQLException;
+import java.util.List;
+import java.util.Optional;
+import java.util.UUID;
+
+public class PostgresAppRepository implements AppRepository {
+
+ private final JdbcTemplate jdbc;
+
+ public PostgresAppRepository(JdbcTemplate jdbc) {
+ this.jdbc = jdbc;
+ }
+
+ @Override
+ public List findByEnvironmentId(UUID environmentId) {
+ return jdbc.query(
+ "SELECT id, environment_id, slug, display_name, created_at FROM apps WHERE environment_id = ? ORDER BY created_at",
+ (rs, rowNum) -> mapRow(rs), environmentId);
+ }
+
+ @Override
+ public Optional findById(UUID id) {
+ var results = jdbc.query(
+ "SELECT id, environment_id, slug, display_name, created_at FROM apps WHERE id = ?",
+ (rs, rowNum) -> mapRow(rs), id);
+ return results.isEmpty() ? Optional.empty() : Optional.of(results.get(0));
+ }
+
+ @Override
+ public Optional findByEnvironmentIdAndSlug(UUID environmentId, String slug) {
+ var results = jdbc.query(
+ "SELECT id, environment_id, slug, display_name, created_at FROM apps WHERE environment_id = ? AND slug = ?",
+ (rs, rowNum) -> mapRow(rs), environmentId, slug);
+ return results.isEmpty() ? Optional.empty() : Optional.of(results.get(0));
+ }
+
+ @Override
+ public UUID create(UUID environmentId, String slug, String displayName) {
+ UUID id = UUID.randomUUID();
+ jdbc.update("INSERT INTO apps (id, environment_id, slug, display_name) VALUES (?, ?, ?, ?)",
+ id, environmentId, slug, displayName);
+ return id;
+ }
+
+ @Override
+ public void delete(UUID id) {
+ jdbc.update("DELETE FROM apps WHERE id = ?", id);
+ }
+
+ private App mapRow(ResultSet rs) throws SQLException {
+ return new App(
+ UUID.fromString(rs.getString("id")),
+ UUID.fromString(rs.getString("environment_id")),
+ rs.getString("slug"),
+ rs.getString("display_name"),
+ rs.getTimestamp("created_at").toInstant()
+ );
+ }
+}
diff --git a/cameleer3-server-app/src/main/java/com/cameleer3/server/app/storage/PostgresAppVersionRepository.java b/cameleer3-server-app/src/main/java/com/cameleer3/server/app/storage/PostgresAppVersionRepository.java
new file mode 100644
index 00000000..1ca89d17
--- /dev/null
+++ b/cameleer3-server-app/src/main/java/com/cameleer3/server/app/storage/PostgresAppVersionRepository.java
@@ -0,0 +1,66 @@
+package com.cameleer3.server.app.storage;
+
+import com.cameleer3.server.core.runtime.AppVersion;
+import com.cameleer3.server.core.runtime.AppVersionRepository;
+import org.springframework.jdbc.core.JdbcTemplate;
+
+import java.sql.ResultSet;
+import java.sql.SQLException;
+import java.util.List;
+import java.util.Optional;
+import java.util.UUID;
+
+public class PostgresAppVersionRepository implements AppVersionRepository {
+
+ private final JdbcTemplate jdbc;
+
+ public PostgresAppVersionRepository(JdbcTemplate jdbc) {
+ this.jdbc = jdbc;
+ }
+
+ @Override
+ public List findByAppId(UUID appId) {
+ return jdbc.query(
+ "SELECT id, app_id, version, jar_path, jar_checksum, jar_filename, jar_size_bytes, uploaded_at FROM app_versions WHERE app_id = ? ORDER BY version DESC",
+ (rs, rowNum) -> mapRow(rs), appId);
+ }
+
+ @Override
+ public Optional findById(UUID id) {
+ var results = jdbc.query(
+ "SELECT id, app_id, version, jar_path, jar_checksum, jar_filename, jar_size_bytes, uploaded_at FROM app_versions WHERE id = ?",
+ (rs, rowNum) -> mapRow(rs), id);
+ return results.isEmpty() ? Optional.empty() : Optional.of(results.get(0));
+ }
+
+ @Override
+ public int findMaxVersion(UUID appId) {
+ Integer max = jdbc.queryForObject(
+ "SELECT COALESCE(MAX(version), 0) FROM app_versions WHERE app_id = ?",
+ Integer.class, appId);
+ return max != null ? max : 0;
+ }
+
+ @Override
+ public UUID create(UUID appId, int version, String jarPath, String jarChecksum, String jarFilename, Long jarSizeBytes) {
+ UUID id = UUID.randomUUID();
+ jdbc.update("INSERT INTO app_versions (id, app_id, version, jar_path, jar_checksum, jar_filename, jar_size_bytes) VALUES (?, ?, ?, ?, ?, ?, ?)",
+ id, appId, version, jarPath, jarChecksum, jarFilename, jarSizeBytes);
+ return id;
+ }
+
+ private AppVersion mapRow(ResultSet rs) throws SQLException {
+ Long sizeBytes = rs.getLong("jar_size_bytes");
+ if (rs.wasNull()) sizeBytes = null;
+ return new AppVersion(
+ UUID.fromString(rs.getString("id")),
+ UUID.fromString(rs.getString("app_id")),
+ rs.getInt("version"),
+ rs.getString("jar_path"),
+ rs.getString("jar_checksum"),
+ rs.getString("jar_filename"),
+ sizeBytes,
+ rs.getTimestamp("uploaded_at").toInstant()
+ );
+ }
+}
diff --git a/cameleer3-server-app/src/main/java/com/cameleer3/server/app/storage/PostgresDeploymentRepository.java b/cameleer3-server-app/src/main/java/com/cameleer3/server/app/storage/PostgresDeploymentRepository.java
new file mode 100644
index 00000000..1aad0e38
--- /dev/null
+++ b/cameleer3-server-app/src/main/java/com/cameleer3/server/app/storage/PostgresDeploymentRepository.java
@@ -0,0 +1,95 @@
+package com.cameleer3.server.app.storage;
+
+import com.cameleer3.server.core.runtime.Deployment;
+import com.cameleer3.server.core.runtime.DeploymentRepository;
+import com.cameleer3.server.core.runtime.DeploymentStatus;
+import org.springframework.jdbc.core.JdbcTemplate;
+
+import java.sql.ResultSet;
+import java.sql.SQLException;
+import java.sql.Timestamp;
+import java.time.Instant;
+import java.util.List;
+import java.util.Optional;
+import java.util.UUID;
+
+public class PostgresDeploymentRepository implements DeploymentRepository {
+
+ private final JdbcTemplate jdbc;
+
+ public PostgresDeploymentRepository(JdbcTemplate jdbc) {
+ this.jdbc = jdbc;
+ }
+
+ @Override
+ public List findByAppId(UUID appId) {
+ return jdbc.query(
+ "SELECT id, app_id, app_version_id, environment_id, status, container_id, container_name, error_message, deployed_at, stopped_at, created_at FROM deployments WHERE app_id = ? ORDER BY created_at DESC",
+ (rs, rowNum) -> mapRow(rs), appId);
+ }
+
+ @Override
+ public List findByEnvironmentId(UUID environmentId) {
+ return jdbc.query(
+ "SELECT id, app_id, app_version_id, environment_id, status, container_id, container_name, error_message, deployed_at, stopped_at, created_at FROM deployments WHERE environment_id = ? ORDER BY created_at DESC",
+ (rs, rowNum) -> mapRow(rs), environmentId);
+ }
+
+ @Override
+ public Optional findById(UUID id) {
+ var results = jdbc.query(
+ "SELECT id, app_id, app_version_id, environment_id, status, container_id, container_name, error_message, deployed_at, stopped_at, created_at FROM deployments WHERE id = ?",
+ (rs, rowNum) -> mapRow(rs), id);
+ return results.isEmpty() ? Optional.empty() : Optional.of(results.get(0));
+ }
+
+ @Override
+ public Optional findActiveByAppIdAndEnvironmentId(UUID appId, UUID environmentId) {
+ var results = jdbc.query(
+ "SELECT id, app_id, app_version_id, environment_id, status, container_id, container_name, error_message, deployed_at, stopped_at, created_at FROM deployments WHERE app_id = ? AND environment_id = ? AND status IN ('STARTING', 'RUNNING') ORDER BY created_at DESC LIMIT 1",
+ (rs, rowNum) -> mapRow(rs), appId, environmentId);
+ return results.isEmpty() ? Optional.empty() : Optional.of(results.get(0));
+ }
+
+ @Override
+ public UUID create(UUID appId, UUID appVersionId, UUID environmentId, String containerName) {
+ UUID id = UUID.randomUUID();
+ jdbc.update("INSERT INTO deployments (id, app_id, app_version_id, environment_id, container_name) VALUES (?, ?, ?, ?, ?)",
+ id, appId, appVersionId, environmentId, containerName);
+ return id;
+ }
+
+ @Override
+ public void updateStatus(UUID id, DeploymentStatus status, String containerId, String errorMessage) {
+ jdbc.update("UPDATE deployments SET status = ?, container_id = ?, error_message = ? WHERE id = ?",
+ status.name(), containerId, errorMessage, id);
+ }
+
+ @Override
+ public void markDeployed(UUID id) {
+ jdbc.update("UPDATE deployments SET deployed_at = now() WHERE id = ?", id);
+ }
+
+ @Override
+ public void markStopped(UUID id) {
+ jdbc.update("UPDATE deployments SET stopped_at = now() WHERE id = ?", id);
+ }
+
+ private Deployment mapRow(ResultSet rs) throws SQLException {
+ Timestamp deployedAt = rs.getTimestamp("deployed_at");
+ Timestamp stoppedAt = rs.getTimestamp("stopped_at");
+ return new Deployment(
+ UUID.fromString(rs.getString("id")),
+ UUID.fromString(rs.getString("app_id")),
+ UUID.fromString(rs.getString("app_version_id")),
+ UUID.fromString(rs.getString("environment_id")),
+ DeploymentStatus.valueOf(rs.getString("status")),
+ rs.getString("container_id"),
+ rs.getString("container_name"),
+ rs.getString("error_message"),
+ deployedAt != null ? deployedAt.toInstant() : null,
+ stoppedAt != null ? stoppedAt.toInstant() : null,
+ rs.getTimestamp("created_at").toInstant()
+ );
+ }
+}
diff --git a/cameleer3-server-app/src/main/java/com/cameleer3/server/app/storage/PostgresEnvironmentRepository.java b/cameleer3-server-app/src/main/java/com/cameleer3/server/app/storage/PostgresEnvironmentRepository.java
new file mode 100644
index 00000000..4d46200c
--- /dev/null
+++ b/cameleer3-server-app/src/main/java/com/cameleer3/server/app/storage/PostgresEnvironmentRepository.java
@@ -0,0 +1,78 @@
+package com.cameleer3.server.app.storage;
+
+import com.cameleer3.server.core.runtime.Environment;
+import com.cameleer3.server.core.runtime.EnvironmentRepository;
+import com.cameleer3.server.core.runtime.EnvironmentStatus;
+import org.springframework.jdbc.core.JdbcTemplate;
+
+import java.sql.ResultSet;
+import java.sql.SQLException;
+import java.util.List;
+import java.util.Optional;
+import java.util.UUID;
+
+public class PostgresEnvironmentRepository implements EnvironmentRepository {
+
+ private final JdbcTemplate jdbc;
+
+ public PostgresEnvironmentRepository(JdbcTemplate jdbc) {
+ this.jdbc = jdbc;
+ }
+
+ @Override
+ public List findAll() {
+ return jdbc.query("SELECT id, slug, display_name, status, created_at FROM environments ORDER BY created_at",
+ (rs, rowNum) -> mapRow(rs));
+ }
+
+ @Override
+ public Optional findById(UUID id) {
+ var results = jdbc.query(
+ "SELECT id, slug, display_name, status, created_at FROM environments WHERE id = ?",
+ (rs, rowNum) -> mapRow(rs), id);
+ return results.isEmpty() ? Optional.empty() : Optional.of(results.get(0));
+ }
+
+ @Override
+ public Optional findBySlug(String slug) {
+ var results = jdbc.query(
+ "SELECT id, slug, display_name, status, created_at FROM environments WHERE slug = ?",
+ (rs, rowNum) -> mapRow(rs), slug);
+ return results.isEmpty() ? Optional.empty() : Optional.of(results.get(0));
+ }
+
+ @Override
+ public UUID create(String slug, String displayName) {
+ UUID id = UUID.randomUUID();
+ jdbc.update("INSERT INTO environments (id, slug, display_name) VALUES (?, ?, ?)",
+ id, slug, displayName);
+ return id;
+ }
+
+ @Override
+ public void updateDisplayName(UUID id, String displayName) {
+ jdbc.update("UPDATE environments SET display_name = ?, updated_at = now() WHERE id = ?",
+ displayName, id);
+ }
+
+ @Override
+ public void updateStatus(UUID id, EnvironmentStatus status) {
+ jdbc.update("UPDATE environments SET status = ?, updated_at = now() WHERE id = ?",
+ status.name(), id);
+ }
+
+ @Override
+ public void delete(UUID id) {
+ jdbc.update("DELETE FROM environments WHERE id = ?", id);
+ }
+
+ private Environment mapRow(ResultSet rs) throws SQLException {
+ return new Environment(
+ UUID.fromString(rs.getString("id")),
+ rs.getString("slug"),
+ rs.getString("display_name"),
+ EnvironmentStatus.valueOf(rs.getString("status")),
+ rs.getTimestamp("created_at").toInstant()
+ );
+ }
+}