diff --git a/cameleer3-server-app/src/main/java/com/cameleer3/server/app/storage/PostgresDiagramStore.java b/cameleer3-server-app/src/main/java/com/cameleer3/server/app/storage/PostgresDiagramStore.java
new file mode 100644
index 00000000..0c7dbbf8
--- /dev/null
+++ b/cameleer3-server-app/src/main/java/com/cameleer3/server/app/storage/PostgresDiagramStore.java
@@ -0,0 +1,128 @@
+package com.cameleer3.server.app.storage;
+
+import com.cameleer3.common.graph.RouteGraph;
+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 org.springframework.stereotype.Repository;
+
+import java.nio.charset.StandardCharsets;
+import java.security.MessageDigest;
+import java.security.NoSuchAlgorithmException;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HexFormat;
+import java.util.List;
+import java.util.Map;
+import java.util.Optional;
+
+/**
+ * PostgreSQL implementation of {@link DiagramStore}.
+ *
+ * Stores route graphs as JSON with SHA-256 content-hash deduplication.
+ * Uses {@code ON CONFLICT (content_hash) DO NOTHING} for idempotent inserts.
+ */
+@Repository
+public class PostgresDiagramStore implements DiagramStore {
+
+ private static final Logger log = LoggerFactory.getLogger(PostgresDiagramStore.class);
+
+ private static final String INSERT_SQL = """
+ INSERT INTO route_diagrams (content_hash, route_id, agent_id, definition)
+ VALUES (?, ?, ?, ?::jsonb)
+ ON CONFLICT (content_hash) DO NOTHING
+ """;
+
+ private static final String SELECT_BY_HASH = """
+ SELECT definition FROM route_diagrams WHERE content_hash = ? LIMIT 1
+ """;
+
+ private static final String SELECT_HASH_FOR_ROUTE = """
+ SELECT content_hash FROM route_diagrams
+ WHERE route_id = ? AND agent_id = ?
+ ORDER BY created_at DESC LIMIT 1
+ """;
+
+ private final JdbcTemplate jdbcTemplate;
+ private final ObjectMapper objectMapper;
+
+ public PostgresDiagramStore(JdbcTemplate jdbcTemplate) {
+ this.jdbcTemplate = jdbcTemplate;
+ 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 json = objectMapper.writeValueAsString(graph);
+ String contentHash = sha256Hex(json);
+ String routeId = graph.getRouteId() != null ? graph.getRouteId() : "";
+
+ jdbcTemplate.update(INSERT_SQL, contentHash, routeId, agentId, json);
+ 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