Memgraph ZeroMemGQLUse CasesPublic-Private Data

Public-Private Data / Hybrid Graphs / Data Sovereignty

In many organizations, public data (open knowledge graphs, product catalogs, industry taxonomies) lives in a graph database like Memgraph, while private or regulated data (customer PII, financial records, health data) must remain in a controlled relational store such as PostgreSQL — often on-premise or in a sovereign cloud region.

MemGQL lets you query both from a single GQL endpoint without ever moving the private data out of its backend. The public graph provides shared context; the private data stays under its existing access controls, compliance boundaries, and encryption at rest.

Scenario

  • Public graph (Memgraph): An open product catalog with categories and pricing.
  • Private graph (PostgreSQL): Customer profiles and internal social relationships that must stay in a GDPR-compliant database.

Architecture

                          ┌──────────────┐ bolt  ┌──────────────┐
                          │              │──────▶│  Memgraph    │
                          │              │       │  (Public)    │
                          │    MemGQL    │       └──────────────┘
┌──────────────┐          │    :7688     │
│   mgconsole  │──bolt───▶│              │ sql   ┌──────────────┐
└──────────────┘          │              │──────▶│  PostgreSQL  │
                          └──────────────┘       │  (Private)   │
                                                 └──────────────┘

Clients connect to MemGQL on port 7688. MemGQL routes each sub-query to the appropriate backend and can materialize both sides locally for cross-backend joins.

Prerequisites

1. Create a project directory

Create a clean directory and change into it — everything lives here:

mkdir public-private && cd public-private

2. Create the Docker Compose file

Save a minimal stack with only the services we need: Memgraph, PostgreSQL, MemGQL, and a one-shot init container:

cat > docker-compose.yml << 'EOF'
services:
  memgql:
    image: ${MEMGQL_IMAGE:-memgraph/memgql:0.8.0}
    ports:
      - "7688:7688"
    environment:
      CONNECTOR_TYPE: multi
      MEMGRAPH_URI: memgraph:7687
      BOLT_LISTEN_ADDR: 0.0.0.0:7688
    volumes:
      - ./pg_mapping.json:/mappings/pg_mapping.json:ro
    depends_on:
      memgraph:
        condition: service_healthy
    healthcheck:
      test: ["CMD-SHELL", "bash -c 'echo > /dev/tcp/localhost/7688'"]
      interval: 2s
      timeout: 2s
      retries: 15
      start_period: 3s
 
  memgql-init:
    image: memgraph/mgconsole:1.6.0
    entrypoint:
      - sh
      - -c
      - |
        echo "ADD CONNECTOR mg TYPE memgraph URI 'memgraph:7687' GRAPH memgraph;" | mgconsole --host memgql --port 7688 &&
        echo "ADD CONNECTOR pg TYPE postgres URI 'host=postgres user=postgres password=postgres dbname=postgres';" | mgconsole --host memgql --port 7688 &&
        echo "MemGQL bootstrap complete."
    depends_on:
      memgql:
        condition: service_healthy
      postgres:
        condition: service_healthy
    restart: "no"
 
  memgraph:
    image: memgraph/memgraph-mage:3.12.0
    ports:
      - "7687:7687"
    command: --log-level=TRACE --also-log-to-stderr
    healthcheck:
      test: ["CMD-SHELL", "bash -c 'echo > /dev/tcp/localhost/7687'"]
      interval: 2s
      timeout: 2s
      retries: 15
      start_period: 3s
 
  postgres:
    image: postgres:18
    ports:
      - "5432:5432"
    environment:
      POSTGRES_PASSWORD: postgres
    volumes:
      - ./pg_init.sql:/docker-entrypoint-initdb.d/init.sql:ro
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U postgres"]
      interval: 2s
      timeout: 2s
      retries: 15
      start_period: 3s
EOF

3. Create the PostgreSQL schema

Create the tables that back the private graph:

cat > pg_init.sql << 'EOF'
DROP TABLE IF EXISTS friend_of CASCADE;
DROP TABLE IF EXISTS users CASCADE;
 
CREATE TABLE users (
    id SERIAL PRIMARY KEY,
    name TEXT NOT NULL,
    email TEXT
);
 
CREATE TABLE friend_of (
    id SERIAL PRIMARY KEY,
    user_id INTEGER NOT NULL REFERENCES users(id),
    friend_id INTEGER NOT NULL REFERENCES users(id),
    UNIQUE (user_id, friend_id)
);
EOF

4. Create the MemGQL mapping

Tell MemGQL how the relational tables map to graph labels:

cat > pg_mapping.json << 'EOF'
{
  "vertices": [
    {
      "label": "User",
      "mappedTableSource": {
        "connector": "pg",
        "table": "users",
        "metaFields": {
          "id": "id"
        }
      },
      "attributes": [
        {
          "name": "name"
        },
        {
          "name": "email"
        }
      ]
    }
  ],
  "edges": [
    {
      "label": "FRIEND_OF",
      "from": "User",
      "to": "User",
      "mappedTableSource": {
        "connector": "pg",
        "table": "friend_of",
        "metaFields": {
          "id": "id",
          "from": "user_id",
          "to": "friend_id"
        }
      }
    }
  ]
}
EOF

5. Start the stack

docker compose up -d

Wait for the memgql-init container to finish bootstrapping the connections:

docker compose logs memgql-init
# → MemGQL bootstrap complete.

6. Connect with mgconsole

mgconsole --port 7688

Verify both connections are registered:

SHOW CONNECTIONS;

Register graphs in the catalog so they can be referenced by name in composite queries:

CREATE GRAPH public FROM '{
  "vertices": [
    { "label": "Product",  "mappedGraphSource": { "connector": "mg" } },
    { "label": "Category", "mappedGraphSource": { "connector": "mg" } }
  ],
  "edges": [
    { "label": "IN_CATEGORY", "from": "Product", "to": "Category", "mappedGraphSource": { "connector": "mg" } }
  ]
}';
CREATE GRAPH private FROM FILE '/mappings/pg_mapping.json';

Verify the graphs:

SHOW GRAPHS;

7. Seed public data in Memgraph

Insert a public product catalog; the Product / Category labels route to public:

INSERT
  (p1:`Product` {sku: "MGX-100", name: "Graph Database", public_price: 5000, contact: "Alice"}),
  (p2:`Product` {sku: "MGX-200", name: "Analytics Suite", public_price: 3000, contact: "Bob"}),
  (p3:`Product` {sku: "MGX-300", name: "Managed Cloud", public_price: 2000, contact: "Charlie"}),
  (sw:Category {name: "Software"}),
  (cl:Category {name: "Cloud Services"}),
  (p1)-[:IN_CATEGORY]->(sw),
  (p2)-[:IN_CATEGORY]->(sw),
  (p3)-[:IN_CATEGORY]->(cl);

Query the catalog:

MATCH (p:`Product`) RETURN p.sku, p.name, p.public_price;
MATCH (p:`Product`)-[:IN_CATEGORY]->(c:Category) RETURN p.name, c.name;

8. Seed private data in PostgreSQL

Insert customer profiles; the User label routes to private:

INSERT (alice:User {name: "Alice", email: "alice@acme.com"});
INSERT (bob:User {name: "Bob", email: "bob@acme.com"});
INSERT (charlie:User {name: "Charlie", email: "charlie@startup.io"});
INSERT (diana:User {name: "Diana", email: "diana@enterprise.com"});

Add private relationship edges:

MATCH (a:User {name: "Alice"}), (b:User {name: "Bob"}) INSERT (a)-[:FRIEND_OF]->(b);
MATCH (a:User {name: "Alice"}), (b:User {name: "Charlie"}) INSERT (a)-[:FRIEND_OF]->(b);
MATCH (a:User {name: "Bob"}), (b:User {name: "Diana"}) INSERT (a)-[:FRIEND_OF]->(b);
MATCH (a:User {name: "Charlie"}), (b:User {name: "Diana"}) INSERT (a)-[:FRIEND_OF]->(b);

Query the private graph:

MATCH (u:User) RETURN u.name, u.email;
MATCH (u:User)-[:FRIEND_OF]->(f:User) RETURN u.name, f.name;

9. Cross-backend query

Because both backends are wired into the same MemGQL session, you can run a single composite query that combines results from the public product catalog and the private customer directory:

MATCH (p:`Product`) RETURN p.name AS name
UNION ALL
MATCH (u:User) RETURN u.name AS name;

Each branch is routed to its respective backend and the results are combined locally. Standard GQL composite operators (UNION, UNION ALL, INTERSECT, EXCEPT) all work across backends.

10. Why this matters

ConcernHow MemGQL addresses it
Data sovereigntyPrivate data stays in PostgreSQL; no ETL or replication required.
ComplianceOnly filtered, joined results cross the wire. Raw PII never moves to Memgraph.
Single query surfaceAnalysts and applications write one GQL query instead of orchestrating multiple clients.
Backend independenceSwap PostgreSQL for MySQL, Oracle, DuckDB, or ClickHouse without changing the GQL schema.

Cleanup

Clear the seeded data (through the graphs, before dropping them):

USE public  MATCH (n) DETACH DELETE n;
USE private MATCH (n) DETACH DELETE n;

Remove the graph catalog entries:

DROP GRAPH public;
DROP GRAPH private;

Stop and remove the stack:

docker compose down -v
cd .. && rm -rf public-private

This pattern extends to any public-private split: open knowledge graphs vs. internal CRM, shared medical ontologies vs. patient records, or industry-wide supply chains vs. proprietary pricing.