Docs

Docs / Migrate

Migrating from MongoDB

Documents become nodes; references and embedded structures become relationships. The work is mostly deciding what stays a property and what becomes its own node, then a short driver script loads it, turning the connections your app tracked by hand into first-class, queryable edges.

With a Tetra account, your dashboard gives you the connection string and API key the script below needs.

Decide your model first

For a typical blog schema (orgs, users, posts):

  • Collection → label. A document in users becomes a (:User); top-level scalar fields become properties.
  • Reference → relationship. user.orgId becomes (:User)-[:MEMBER_OF]->(:Org).
  • Embedded array of objects → nodes + edges. A post's comments[] becomes (:Post)-[:HAS_COMMENT]->(:Comment): now queryable and shareable, not buried in a document.
  • Array of scalars → property or nodes. Keep tags[] as a list property, or promote to (:Tag) nodes if you query by tag (recommended).
  • Simple embedded object → properties. An address sub-document with no identity stays as flattened properties.

Before you start

  • A Tetra database + your connection string and API key (free DEMO MODE works).
  • mongoexport (ships with the MongoDB tools) and Node with neo4j-driver installed.

Steps

  1. Export each collection

    mongoexport writes newline-delimited JSON, one file per collection.

    mongoexport --collection=orgs  --out=orgs.json
    mongoexport --collection=users --out=users.json
    mongoexport --collection=posts --out=posts.json
  2. Load with a driver script

    A short script gives you full control over nested data: MERGE keeps shared entities (a user referenced by many posts) de-duplicated, and UNWIND turns embedded arrays into their own nodes and edges. Load referenced collections first.

    import neo4j from 'neo4j-driver';
    import { readFileSync } from 'node:fs';
    
    const driver = neo4j.driver(
      'bolt://tetra-ca.com?db=YOUR_DB',
      neo4j.auth.basic('apikey', '<YOUR_API_KEY>'));
    const session = driver.session();
    
    // mongoexport writes newline-delimited JSON; ObjectIds arrive as { "$oid": "…" }.
    const load = (f) => readFileSync(f, 'utf8').trim().split('\n').map(JSON.parse);
    const oid  = (v) => (v && v.$oid) || v;
    
    // orgs → (:Org), users → (:User)-[:MEMBER_OF]->(:Org)
    for (const o of load('orgs.json'))
      await session.run('MERGE (:Org {id: $id, name: $name})',
        { id: oid(o._id), name: o.name });
    
    for (const u of load('users.json'))
      await session.run(
        `MERGE (user:User {id: $id}) SET user.email = $email
          WITH user MATCH (org:Org {id: $orgId})
          MERGE (user)-[:MEMBER_OF]->(org)`,
        { id: oid(u._id), email: u.email, orgId: oid(u.orgId) });
    
    // posts → author ref + embedded comments[] + tags[] become nodes & edges
    for (const p of load('posts.json'))
      await session.run(
        `MERGE (post:Post {id: $id}) SET post.title = $title
          MERGE (author:User {id: $authorId})
          MERGE (post)-[:AUTHORED_BY]->(author)
          WITH post UNWIND $comments AS c
            MERGE (cm:Comment {id: c.id}) SET cm.body = c.body
            MERGE (post)-[:HAS_COMMENT]->(cm)
            MERGE (cu:User {id: c.userId}) MERGE (cm)-[:BY]->(cu)
          WITH DISTINCT post UNWIND $tags AS t
            MERGE (tag:Tag {name: t}) MERGE (post)-[:TAGGED]->(tag)`,
        { id: oid(p._id), title: p.title, authorId: oid(p.authorId),
          comments: (p.comments || []).map((c) => ({ ...c, id: oid(c._id) })),
          tags: p.tags || [] });
    
    await session.close();
    await driver.close();
  3. Verify

    Reconcile node counts against your collection counts, then spot-check a few relationships.

    // Compare to db.<collection>.countDocuments() on the Mongo side:
    MATCH (n) RETURN labels(n)[0] AS label, count(*) AS n ORDER BY label;
    MATCH ()-[r]->() RETURN type(r) AS rel, count(*) AS n ORDER BY rel;

Tips & gotchas

  • ObjectIds: mongoexport wraps them as {"$oid": "…"}: extract the string and use it as the node id (the oid() helper above).
  • Use MERGE, not CREATE, on ids so re-running the script is idempotent and shared references collapse to one node.
  • Dates: Mongo ISODate exports as {"$date": "…"}: wrap with datetime() on load.
  • Big collections: batch the loop (e.g. 5–10k docs per transaction) rather than one giant write.
  • The payoff: joins MongoDB couldn't do natively (multi-hop, "friends of friends", recommendation paths) are now single Cypher queries.

Next steps

Connect a driver or your AI assistant from the dashboard, or see Migrating from Neo4j · PostgreSQL.

Want to see it on your own graph?

We'll walk you through TETRA on a graph shaped like yours. Or just tell us who you are and we'll keep you in the loop as we get closer to public launch.