Search docs.molo17.com
    Configuration guide Connectors

    Set up a MongoDB target for snapshots and CDC

    MongoDB stores documents, not rows. Gluesync turns each source row into one document, builds a string _id from the table and the key of the row, and writes it with an upsert. A delete removes the document with that _id. This guide creates a MongoDB user with only the roles Gluesync needs, copies two PostgreSQL tables with a snapshot, follows inserts, updates and deletes with CDC, shows the documents that result, changes the _id, tunes the writes and lists the errors you are most likely to meet.

    In this guide

    You will create a role and a user in MongoDB, connect the MongoDB target, map two source tables to collections and read the documents with mongosh. You will run a snapshot, follow an insert, an update and a delete, and see that every update replaces the whole document. You will define your own document key, tune the writers, and see what happens on a unique index conflict, on a missing role and on a TRUNCATE. A table at the end lists the errors that were reproduced for this guide.

    Quick information

    Goal

    Replicate PostgreSQL tables to MongoDB collections with a snapshot, keep them in sync with change data capture (CDC), and know what the documents and their _id look like

    Audience

    Data engineers and MongoDB administrators

    Level

    Intermediate

    Time required

    About 40 minutes

    Area

    mongosh; Control Plane → Data pipelines → MongoDB target → Base settings; entity → Edit settings → Target

    Applies to

    Gluesync 2.3

    Before you begin

    Make sure that:

    1. You have a MongoDB server that Core Hub can reach, and you can run mongosh on it as a user who can create roles and users. The Control Plane shows 3.6 as the minimum version, for Atlas, Enterprise and Community. This guide was tested with MongoDB 8.0. A standalone server is enough for a target: Gluesync does not need a replica set to write. A replica set is needed only when MongoDB is a source, see Set up a MongoDB change streams source;

    2. You have a source that Core Hub can read with CDC. This guide uses PostgreSQL 16 with wal_level = logical, set up as in Set up a PostgreSQL WAL source;

    3. You can sign in to the Control Plane with a role that can create pipelines;

    4. You have two source tables. The guide uses public.orders, with 20,000 rows, and public.order_lines, with 300 rows. orders has id bigint as primary key, customer varchar(50), amount numeric(12,2), status varchar(20), created_at timestamp, ship_date date, paid boolean, tags text[] and note text. order_lines has the composite primary key (order_id bigint, line_no int), plus sku and qty.

    The statements and the messages quoted in this guide were run and captured against MongoDB 8.0 and the Control Plane that ships with Gluesync 2.3. In the lab, the database is shop_mirror, and Core Hub and MongoDB run in containers on one network.

    Guide steps

    Create the role and the user

    CLI

    Gluesync does not need an administrator. It needs a user that can read the collection names, find, insert, update and delete documents in one database. Run the commands in mongosh, connected as an administrator. Replace the password.

    1. The simplest choice is the built-in role readWrite on the target database:

      use shop_mirror
      db.createUser({
        user: "gluesync",
        pwd: "<a strong password>",
        roles: [ { role: "readWrite", db: "shop_mirror" } ]
      })
    2. If you want fewer rights, create a role with only the actions that the writes use, and give it to the user. This role was tested end to end, with snapshot, insert, update and delete:

      db.getSiblingDB("admin").createRole({
        role: "GluesyncTargetWriter",
        privileges: [ {
          resource: { db: "shop_mirror", collection: "" },
          actions: [ "find", "insert", "update", "remove", "listCollections" ]
        } ],
        roles: []
      })
      db.getSiblingDB("shop_mirror").createUser({
        user: "gluesync",
        pwd: "<a strong password>",
        roles: [ { role: "GluesyncTargetWriter", db: "admin" } ]
      })

      With this role, the user can create a collection by writing the first document into it, and it cannot create an index. A new collection therefore has only the _id index.

    3. Check the user:

      db.getSiblingDB("shop_mirror").getUser("gluesync")
    Expected result

    The command returns the user with db: 'shop_mirror' and the role you gave. The user lives in shop_mirror, the same database that Gluesync writes to, so the connection in the next step needs no extra property. If you create the user in admin, you must set Auth Database in step 2.

    The role read is not enough. The connection test passes, and the first write fails with error 13 (Unauthorized). Step 8 shows the message.

    Create the pipeline and the MongoDB target connection

    UI

    1. In the Control Plane, select Create your first pipeline, or the + button of the pipeline list if you already have pipelines. The Agents setup wizard opens;

    2. In 1. Setup source, choose the PostgreSQL CDC agent tile and fill Base settings with the Host name / Address, Port, Database name, Username and Password of your source. Select Test and continue, then Next;

    3. In 2. Setup target(s), search for mongo and choose the MongoDB CDC agent tile. The tile is the same for a source and for a target;

    4. Fill Base settings:

      • Host name / Address: your MongoDB server;

      • Port: 27017 is the default;

      • Database name: shop_mirror. This is the database that receives the collections;

      • Username and Password: the user of step 1;

    5. Select Test and continue. The target appears as <user>@<host>:27017 with a green check mark. Select Next, enter a Pipeline name, and select Finish.

    Edit target page of the MongoDB agent with host mongodb
    Figure 1. The MongoDB target connection. Under Advanced connection properties, the Key list offers Connection Timeout Seconds, Read Timeout Seconds, Auth Database, Use Dns Seed List, Replica Set and Direct connection. The Security section below has Disable auth and the upload of the trust store, key store and certificate store.

    The properties you are most likely to need:

    • Auth Database, when the user was created in a database other than the one you write to, usually admin. Select Add property, pick Auth Database in Key and enter admin in Value;

    • Use Dns Seed List, for an Atlas address that uses an SRV record;

    • Replica Set and Direct connection, to talk to one node of a replica set. In the test, the connection to a standalone server worked with none of them;

    • the Security section, for TLS. See Enable HTTPS and TLS to databases.

    The MongoDB target reference lists every option, the Atlas notes and the REST API setup.

    Expected result

    The pipeline appears in the HEALTHY group of the pipeline list with the PostgreSQL and MongoDB icons. A wrong password fails here with Failed to save and Exception authenticating MongoCredential{mechanism=SCRAM-SHA-256, userName='gluesync', source='shop_mirror', password=<hidden>, mechanismProperties=<hidden>}. The source in the message is the database where Gluesync looked for the user.

    Add the entities and read the mapping

    UI

    1. In the pipeline, select Entities. The Add entities page opens with the Object browser of the source. Expand the schema public and select orders and order_lines. Each table gets a check mark and a gear icon;

    2. Select Confirm and continue. Both entities appear in the pipeline on Hold;

    3. Open public.orders and select Edit, then the mapping icon next to the table. The Field mapping dialog opens.

    Field mapping dialog with the source table public.orders and the target collection orders
    Figure 2. The mapping of orders. The Target Collection is orders, with no schema in front of it, and the key column id is marked in both tables.

    Read three things in the dialog and on the entity page:

    • the Target Collection. Gluesync proposes the name of the table, here orders, without the schema. You can type another name. The collection does not need to exist: the first write creates it;

    • the target types. They follow the source, for example bigint becomes INT64 and numeric(12, 2) becomes DECIMAL128 (12, 2). See Data types;

    • the Document Key Preview on the entity page, public :: orders :: id. This is how the _id is built, see step 5.

    Every entity needs a key. A table without a primary key is rejected, and the dialog shows No key on the source and No key on the target, with the Confirm and continue button disabled. Tick the key columns in the key column of the source side of the dialog, and the target inherits them. In the test, ticking a in a table nopk(a int, b text) fixed both errors.

    Expected result

    The entities public.orders and public.order_lines are on Hold. The Field mapping dialog shows Error (0) and Warning (0).

    Run the snapshot

    UI + CLI

    1. Select the play button of public.orders. The Sync changes (CDC) dialog opens;

    2. Keep Full copy under Start from and Keep and merge under Data already in the target. Rows with the same key are overwritten and the others stay. This is the choice that never deletes a document;

    3. Select Start entity. Do the same for public.order_lines.

    Sync changes dialog with Full copy and Keep and merge selected
    Figure 3. The start dialog with the Custom speed open. The Write method is Standard and carries the label This target has no bulk load. Writers during the copy and Parallel reads go from 1 to 10.

    When the snapshot ends, read the collections in mongosh:

    use shop_mirror
    db.orders.countDocuments()
    db.order_lines.countDocuments()
    db.orders.findOne({ id: 1 })
    db.order_lines.findOne({ order_id: 51, line_no: 1 })
    Expected result

    orders has 20000 documents and order_lines has 300. Each row is one document. The _id is a string, and the columns of the row are fields:

    {
      _id: 'public::orders::1',
      id: Long('1'),
      customer: 'customer-1',
      amount: Decimal128('947.49'),
      status: 'paid',
      created_at: ISODate('2026-11-10T06:14:34.280Z'),
      ship_date: ISODate('2026-11-11T00:00:00.000Z'),
      paid: false,
      tags: [ 'a', 'b' ],
      note: 'note 1'
    }

    The composite key of order_lines gives _id: 'public::order_lines::51::1'. The key columns stay in the document as fields too. In the test, the snapshot of the 20,000 rows took 315 ms with the default two writers.

    Capture live changes and see how updates and deletes land

    CLI

    To see what an update does to a document, first add a field in MongoDB that does not exist in the source:

    db.orders.updateOne({ _id: "public::orders::1" }, { $set: { review_score: 5 } })

    Then, on the source, insert, update and delete one row each in orders, and update one row of order_lines:

    INSERT INTO public.orders VALUES (100001, 'customer-new', 19.90, 'new', now(), current_date, false, '{x}', 'hello');
    UPDATE public.orders SET status = 'paid', amount = 25.00 WHERE id = 1;
    DELETE FROM public.orders WHERE id = 2;
    UPDATE public.order_lines SET qty = 99 WHERE order_id = 1 AND line_no = 1;

    After a few seconds, read the result:

    db.orders.countDocuments()
    db.orders.findOne({ _id: "public::orders::1" })
    db.orders.countDocuments({ _id: "public::orders::2" })
    db.order_lines.findOne({ _id: "public::order_lines::1::1" })
    Expected result

    The count stays at 20000: one insert and one delete. The document public::orders::1 has status: 'paid' and amount: Decimal128('25.00'), and the field review_score is gone. The document public::orders::2 does not exist. The line has qty: 99.

    In short:

    • an insert or an update replaces the whole document with the current image of the row, and creates it if it does not exist (an upsert). Fields that Gluesync does not map, like review_score, do not survive an update. Keep your own fields in another collection;

    • a delete removes the document with that _id, and nothing else;

    • a second run of the snapshot with Keep and merge writes every row again and leaves the count unchanged. In the test it ran in 214 ms with the Faster speed;

    • a TRUNCATE on the source does not clear the collection. The Core Hub log shows Received a transaction message with a truncate operation. This operation is not supported by MongoDB. Skipped.. Delete the documents yourself if you need an empty collection.

    Control the document key

    UI

    The _id is the identifier that Gluesync uses to find the document for every insert, update and delete. By default it is the scope, the collection and the key values, joined with ::. You can change it for an entity.

    1. Open the entity and select Edit. Select the gear icon of the table, then the tab with the name of the MongoDB target. You can do this while the entity runs, and the entity then stops;

    2. Under Document key, turn on Customize document key. The switch reads User defined key;

    3. Set a Prefix, a Separator (::, -, _, / or none), the Fields and the Suffix. Key order sets the order of the key fields. The Preview shows the result;

    4. Close the dialog and select Confirm and continue. The entity shows a Warning. Select its play button and keep Where it stopped, which continues from the last checkpoint.

    Settings dialog of the entity order_lines with the Target tab open
    Figure 4. A user defined key for order_lines. The prefix is line, the separator is a hyphen and the suffix is v1.

    Update a row and read the collection:

    UPDATE public.order_lines SET qty = 7 WHERE order_id = 2 AND line_no = 2;
    db.order_lines.countDocuments()
    db.order_lines.find({ qty: 7 })
    Expected result

    The count goes from 301 to 302. The new document has _id: 'line-2-2-v1'. The old document public::order_lines::2::2 is still there, with qty: 2.

    The new key applies from the next write of each row. Gluesync does not rename or delete the documents it wrote with the old key. Run a Full copy so that every row has a document with the new _id, then remove the old ones, for example:

    db.order_lines.deleteMany({ _id: /^public::order_lines::/ })

    Two rows that give the same _id write the same document, and the last one wins. Choose key fields that identify a row.

    Tune the writes

    UI

    This target has no bulk loader. Gluesync sends the rows of a transaction to MongoDB with one bulkWrite of upserts, and one of deletes for a delete transaction, with the write concern majority. The settings that change the speed are on the entity and in the start dialog:

    1. Writers. In the start dialog, under Speed, choose Faster, or choose Custom and set Writers during the copy (1 to 10) and Parallel reads (1 to 10, split by a column such as the key). The entity setting Snapshot writing concurrency (the Fine tuning tab) has the same range and a default of 2;

    2. Batch size. In Fine tuning, Max items per iteration on fetch (10000 by default) is the window of rows that the source reads and sends. Max CDC message size (KB) (4096) and Max snapshot page size (KB) (8192) cap the size of a message;

    3. Indexes. Every secondary index you add on the collection makes each upsert slower. Create only the indexes that your queries need, and see step 8 for the unique ones.

    The Advanced settings tab of the target offers three properties after Add property: Enable Recursion Protection, Query Studio maxTimeMS and Schema discovery sample size. Enable Recursion Protection is for two-way setups on the same collection. The MongoDB target settings list the connection properties.

    Expected result

    Measured on one laptop with Core Hub, PostgreSQL and MongoDB in containers, a snapshot of the 20,000 rows of orders took 315 ms with Standard (two writers) and 214 ms with Faster (four parallel reads and four writers). The table is small, so measure on your own data before you change anything: read Tune replication throughput.

    Fix the errors you are most likely to meet

    CLI + UI

    Each error below was reproduced on purpose.

    1. A unique index on the collection. Create a unique index on a field that the documents carry, then insert a source row with a value that already exists:

      db.orders.createIndex({ note: 1 }, { unique: true, name: "note_unique" })
      INSERT INTO public.orders VALUES (100002, 'dup', 1.00, 'new', now(), current_date, false, '{x}', 'note 5');

      The entity goes to Error with Target write failed:

      Bulk write operation error on MongoDB server mongodb:27017. Write errors: [BulkWriteError{index=0, code=11000, message='E11000 duplicate key error collection: shop_mirror.orders index: note_unique dup key: { note: "note 5" }', details={}}].

      The _id cannot collide, because a replace overwrites the document with the same _id. A conflict with another document’s unique index can, and an upsert cannot solve it. The setting On duplicate key (Upsert, Skip, Fail) does not change this: with Skip the entity stopped in the same way. Drop the index, or fix the data, and the entity writes again. Core Hub retries the transaction every few seconds, and in the test the row arrived without a restart;

    2. A user that cannot write. Give the user the role read instead of readWrite, and insert a row on the source. The entity goes to Error, and the Core Hub log shows Command execution failed on MongoDB server with error 13 (Unauthorized): 'not authorized on shop_mirror to execute command { update: "orders", ordered: true, writeConcern: { w: "majority" }, .... Grant readWrite, or a role with find, insert, update and remove, as in step 1;

    3. A user in another database. A user created in admin fails in the target form with Exception authenticating MongoCredential{mechanism=SCRAM-SHA-1, userName='gs_admin_scope', source='shop_mirror', ...}. Add the property Auth Database with the value admin, and Test and continue passes.

    Entity page of public.orders in Error with the banner Target write failed and the E11000 duplicate key error message for the index note_unique
    Figure 5. The unique index conflict. The page shows Target write failed with the full MongoDB message, and the replication status shows the same text.
    Expected result

    After each fix, the entity or the connection works again without a restart of Core Hub.

    Data types

    What the target wrote for the PostgreSQL types of this guide, read back with mongosh:

    Source type BSON type Note

    smallint, integer

    int

    bigint

    long

    Long('9000000000')

    real, double precision

    double

    numeric(p,s)

    decimal

    Decimal128, kept exactly, for example Decimal128('12345.678901')

    varchar(n), text

    string

    boolean

    bool

    date

    date

    Midnight UTC of that day, ISODate('2026-11-10T00:00:00.000Z')

    time

    date

    On 1970-01-01, ISODate('1970-01-01T13:45:10.000Z'). The date part is not meaningful

    timestamp

    date

    Read as UTC, with milliseconds

    timestamptz

    string

    ISO 8601 text of the UTC instant, '2026-11-10T11:45:10Z', not a date

    bytea

    binData

    json, jsonb

    string

    The JSON text, not a subdocument

    uuid

    string

    integer[], text[]

    array

    NULL

    null

    The field is present with the value null

    The MongoDB target reference and the Target type list of the Field mapping dialog show the types you can pick for each column.

    Bulk and throughput settings

    Setting Default When to change it

    Writers during the copy (start dialog) or Snapshot writing concurrency (entity)

    2

    Raise it for a large snapshot when the source and Core Hub are not the limit

    Parallel reads (start dialog, Custom)

    1, or 4 with Faster

    Raise it when the read of the source is the limit. Choose a column with evenly spread values

    Max items per iteration on fetch (entity)

    10000

    The size of the window of rows that the source reads and sends. Change it only while you measure

    Max CDC message size (KB) (entity)

    4096

    The size limit of the items that the source packs into one message during CDC. Change it only if a transaction does not fit

    On duplicate key (entity)

    Upsert

    Leave it. It does not help with a unique index conflict

    Enable Recursion Protection (target, Advanced settings)

    Off

    Turn it on only when two pipelines write the same collection in two directions

    Read Tune replication throughput for the settings that apply to every target.

    Verify the result

    You have set up the MongoDB target if:

    • The user has readWrite, or find, insert, update and remove on the target database, and the connection test passes;

    • The field mapping has no error, and every entity has a key;

    • The count of documents in each collection equals the count of rows of the source after the snapshot;

    • A CDC insert, update and delete reach the collection, and an update replaces the whole document;

    • You know the _id of your documents, and you know that changing the document key leaves the old documents in place;

    • You know that a TRUNCATE on the source is skipped and that a unique index on the collection can stop an entity.

    Troubleshooting

    Problem Possible cause What to do

    Failed to save with Exception authenticating MongoCredential{mechanism=SCRAM-SHA-256, userName='<user>', source='<database>', ...}

    Wrong password, or the user was created in another database than the one in source

    Correct the password. If the user lives in admin, add the property Auth Database with the value admin

    The entity is in Error with error 13 (Unauthorized): 'not authorized on <database> to execute command { update: "<collection>" ...

    The role of the user has no update, insert or remove, for example read

    Grant readWrite, or a role with find, insert, update and remove. The entity retries on its own

    The entity is in Error with E11000 duplicate key error collection: <database>.<collection> index: <name> dup key: ...

    A unique secondary index on the collection rejects a document whose value exists in another document

    Drop the index or fix the value. On duplicate key does not apply to this case

    No key on the source and No key on the target

    The source table has no primary key

    Tick the key columns in the Field mapping dialog, or add a primary key to the source

    The Port field shows 0 when you edit a target whose first test failed

    The form of a connection that failed its first test does not keep the default port

    Enter the port again, 27017 by default, and select Test and continue

    A field you added in MongoDB disappears

    Every insert and update replaces the whole document

    Keep your own fields in another collection, or in another database

    A TRUNCATE on the source does not empty the collection

    MongoDB has no truncate in this connector. The Core Hub log says This operation is not supported by MongoDB. Skipped.

    Delete the documents with deleteMany({}), then run a Full copy

    After you changed the document key, the collection has the old and the new documents

    Gluesync does not remove the documents of the old key

    Run a Full copy, then delete the old documents by their _id pattern, as in step 6

    A jsonb column is a string, and a timestamptz column is a string

    The connector writes both as text

    Parse the string in your application

    Contact MOLO17 Support if you need assistance with a MongoDB target.