diff --git a/cameleer3-server-app/src/main/java/com/cameleer3/server/app/storage/ClickHouseDiagramStore.java b/cameleer3-server-app/src/main/java/com/cameleer3/server/app/storage/ClickHouseDiagramStore.java
new file mode 100644
index 00000000..922b3a2b
--- /dev/null
+++ b/cameleer3-server-app/src/main/java/com/cameleer3/server/app/storage/ClickHouseDiagramStore.java
@@ -0,0 +1,193 @@
+package com.cameleer3.server.app.storage;
+
+import com.cameleer3.common.graph.RouteGraph;
+import com.cameleer3.common.graph.RouteNode;
+import com.cameleer3.server.core.ingestion.TaggedDiagram;
+import com.cameleer3.server.core.storage.DiagramStore;
+import com.fasterxml.jackson.core.JsonProcessingException;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.jdbc.core.JdbcTemplate;
+
+import java.nio.charset.StandardCharsets;
+import java.security.MessageDigest;
+import java.security.NoSuchAlgorithmException;
+import java.sql.Timestamp;
+import java.time.Instant;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.HexFormat;
+import java.util.List;
+import java.util.Map;
+import java.util.Optional;
+
+/**
+ * ClickHouse implementation of {@link DiagramStore}.
+ *
+ * Stores route graphs as JSON with SHA-256 content-hash deduplication.
+ * Uses ReplacingMergeTree — duplicate inserts are deduplicated on merge.
+ *
+ * {@code findProcessorRouteMapping} fetches all definitions for the application
+ * and deserializes them in Java because ClickHouse has no equivalent of
+ * PostgreSQL's {@code jsonb_array_elements()}.
+ */
+public class ClickHouseDiagramStore implements DiagramStore {
+
+ private static final Logger log = LoggerFactory.getLogger(ClickHouseDiagramStore.class);
+
+ private static final String TENANT = "default";
+
+ private static final String INSERT_SQL = """
+ INSERT INTO route_diagrams
+ (tenant_id, content_hash, route_id, agent_id, application_name, definition, created_at)
+ VALUES (?, ?, ?, ?, ?, ?, ?)
+ """;
+
+ private static final String SELECT_BY_HASH = """
+ SELECT definition FROM route_diagrams
+ WHERE tenant_id = ? AND content_hash = ?
+ LIMIT 1
+ """;
+
+ private static final String SELECT_HASH_FOR_ROUTE = """
+ SELECT content_hash FROM route_diagrams
+ WHERE tenant_id = ? AND route_id = ? AND agent_id = ?
+ ORDER BY created_at DESC LIMIT 1
+ """;
+
+ private static final String SELECT_DEFINITIONS_FOR_APP = """
+ SELECT DISTINCT route_id, definition FROM route_diagrams
+ WHERE tenant_id = ? AND application_name = ?
+ """;
+
+ private final JdbcTemplate jdbc;
+ private final ObjectMapper objectMapper;
+
+ public ClickHouseDiagramStore(JdbcTemplate jdbc) {
+ this.jdbc = jdbc;
+ this.objectMapper = new ObjectMapper();
+ this.objectMapper.registerModule(new JavaTimeModule());
+ }
+
+ @Override
+ public void store(TaggedDiagram diagram) {
+ try {
+ RouteGraph graph = diagram.graph();
+ String agentId = diagram.agentId() != null ? diagram.agentId() : "";
+ String applicationName = diagram.applicationName() != null ? diagram.applicationName() : "";
+ String json = objectMapper.writeValueAsString(graph);
+ String contentHash = sha256Hex(json);
+ String routeId = graph.getRouteId() != null ? graph.getRouteId() : "";
+
+ jdbc.update(INSERT_SQL,
+ TENANT,
+ contentHash,
+ routeId,
+ agentId,
+ applicationName,
+ json,
+ Timestamp.from(Instant.now()));
+ log.debug("Stored diagram for route={} agent={} with hash={}", routeId, agentId, contentHash);
+ } catch (JsonProcessingException e) {
+ throw new RuntimeException("Failed to serialize RouteGraph to JSON", e);
+ }
+ }
+
+ @Override
+ public Optional findByContentHash(String contentHash) {
+ List