MongoDB Integration Guide

This guide covers how to connect a MongoDB database to BurnLedger, configure query templates, and set up a read-only database user.


Connection Configuration

BurnLedger connects to MongoDB using a standard connection URI.

Format:

mongodb://<user>:<password>@<host>:<port>/<database>?tls=true&authSource=admin

A single host per URI. The multi-host seed-list form (…@<host1>:<port>,<host2>:<port>/…?replicaSet=<name>) is not accepted today — see the known limitation below.

mongodb+srv:// is not supported. The SRV form requires SRV and TXT DNS lookups performed by the driver itself. BurnLedger's connectors run inside a Nitro Enclave that has no DNS resolver of its own — name resolution happens on the host that proxies the connection — so those lookups cannot succeed and the connector rejects mongodb+srv:// URIs up front rather than failing with an opaque DNS error. MongoDB Atlas users: in Connect → Drivers, select an older driver version to get the equivalent standard mongodb:// string with the hosts listed explicitly, and use that.

Percent-encode special characters in the password. The URI is parsed with a strict URI parser, so @ : / ? # & in the username or password must be escaped (@%40, #%23) or the URI is rejected outright ("invalid MongoDB URI") or the wrong host is validated. If you control the credential, the simplest option is a password limited to letters, digits, - and _.

REPLACE-WITH-YOUR-PASSWORD is a placeholder, not a suggestion: choose your own password when you create the user below, and substitute it wherever the placeholder appears in this guide.

Known limitation — multi-host URIs. Registration validates the URI host with a single-host URI parser. A comma-separated seed list (mongodb://u:p@h1:27017,h2:27017/db) is not split, so the whole h1:27017,h2 string is treated as one hostname, fails DNS resolution, and the system is rejected with BAD_REQUEST ("connection config targets a blocked host"). Until that is fixed, register a single host per system — point it at a replica-set member (ideally a secondary, which is what a read-only credential should be reading anyway).

That advice stands, and BurnLedger now tells you which endpoint answered: POST /v1/systems/test-connection reports replication.role from helloprimary or replica — because a secondary that was re-seeded or restored from an older backup can report zero where the primary would not, and that absence would be certified. Lag is reported as unknown and always will be: replSetGetStatus needs clusterMonitor, which this connector refuses as write-capable. See endpoint role.

Configuration fields:

Field Required Description
uri Yes MongoDB connection URI. mongodb+srv:// is rejected (see above).
database Yes The database to query. Not inferred from the path component of the URI — you must set it explicitly or construction fails with "MongoDB config missing database".
collection Yes The collection to query.
read_only No Operator assertion that the credential is read-only. Consulted only when MongoDB auth is disabled and privileges cannot be introspected; see Read-Only User Setup.

TypeScript

const system = await bl.registerSystem({
  name: "users-collection",
  connectorType: "mongodb",
  connectionConfig: {
    uri: "mongodb://burnledger_ro:REPLACE-WITH-YOUR-PASSWORD@cluster0-shard-00-00.example.mongodb.net:27017/myapp?tls=true&authSource=admin",
    database: "myapp",
    collection: "users",
  },
  subjectQuery: '{"email": "$IDENTIFIER"}',
});

Python

system = bl.register_system(
    name="users-collection",
    connector_type="mongodb",
    connection_config={
        "uri": "mongodb://burnledger_ro:REPLACE-WITH-YOUR-PASSWORD@cluster0-shard-00-00.example.mongodb.net:27017/myapp?tls=true&authSource=admin",
        "database": "myapp",
        "collection": "users",
    },
    subject_query='{"email": "$IDENTIFIER"}',
)

Connection requirements:

Requirement Details
TLS ?tls=true is required. A URI without it connects in cleartext and is refused before the connector is built. Watch two driver quirks: tlsInsecure=true on its own does not turn TLS on (the URI stays plaintext), and combined with tls=true it disables certificate verification, which is also refused. tlsAllowInvalidCertificates is not a driver option at all and is silently ignored. Require TLS server-side too (net.tls.mode: requireTLS; Atlas always requires it). See connector transport security.
Network access Add BurnLedger's egress address to your MongoDB Atlas IP access list or firewall rules.
Permissions Read-only. BurnLedger refuses to connect if the user has write privileges. See Read-Only User Setup.
Timeout Connections must be established within 10 seconds. Queries must complete within 30 seconds.

Query Template Format

For MongoDB, the query template is a JSON document that serves as a find() filter. Use $IDENTIFIER as the placeholder for the data subject identifier. BurnLedger substitutes the subject value at attestation time.

Rules:

  • Must be valid JSON.
  • Must contain at least one $IDENTIFIER placeholder, and it must be the entire string value — {"email": "$IDENTIFIER"} substitutes, {"email": "user-$IDENTIFIER"} does not. A template whose placeholder never substitutes is rejected at attestation time ("contains no $IDENTIFIER placeholder; would scan entire collection"), so it can pass registration and fail on first use.
  • Operators are a default-deny allowlist. Only these $-prefixed keys are permitted anywhere in the filter: $and $or $nor $not $in $nin $eq $ne $gt $gte $lt $lte $exists $type $regex $options $size $all $elemMatch Every other $ key is rejected — that covers update operators ($set, $unset, $push), server-side JavaScript ($where, $expr, $function, $accumulator), aggregation writes ($out, $merge), $jsonSchema, and also perfectly innocent read operators such as $text, $mod, $near, and $bitsAllSet. If you need one of those, rewrite the filter using the allowed set.

Examples:

// Simple equality match on email
{"email": "$IDENTIFIER"}

// Match on a nested field
{"profile.email": "$IDENTIFIER"}

// Match on an external ID field
{"externalId": "$IDENTIFIER"}

// Compound query: match by email across multiple possible fields
{"$or": [{"email": "$IDENTIFIER"}, {"alternateEmail": "$IDENTIFIER"}]}

How it works during attestation:

  1. BurnLedger substitutes the subject value into the template, replacing $IDENTIFIER with the actual value as a string.
  2. It runs db.collection.find(query) on the specified collection.
  3. It computes a deterministic hash of the matching documents (or their existence, depending on hash scope).
  4. The result is signed and appended to the transparency log.

Read-Only User Setup

BurnLedger requires a user with read-only access to the specific database and collection. It detects write permissions during connection validation and rejects connections that have them — without ever writing. It runs connectionStatus with showPrivileges: true and inspects the authenticated user's granted actions.

The check is default-deny, not a blocklist of write actions. The only actions accepted are the action set of MongoDB's built-in read role:

changeStream  collStats  dbHash  dbStats  find
killCursors   listCollections  listIndexes  listSearchIndexes  planCacheRead

Any other granted action — including non-mutating ones like listDatabases, serverStatus, or anything from clusterMonitor — is treated as write-capable and construction is refused with WRITE_ACCESS_DETECTED. Grant the BurnLedger user the built-in read role on one database, or a custom role narrower than that. In particular, readAnyDatabase is refused: it adds the cluster-wide listDatabases action, which is outside the accepted set.

This works automatically whenever authentication is enabled. If the MongoDB deployment has auth disabled (no authenticated user, so privileges cannot be introspected), set "read_only": true in the connector config to assert read-only; otherwise construction is refused (fails closed).

MongoDB Atlas

In the Atlas UI:

  1. Go to Database Access.
  2. Click Add New Database User.
  3. Set authentication method to Password.
  4. Under Database User Privileges, choose Specific Privilege / a custom role scoped to your database — not the built-in Only read any database (readAnyDatabase), which carries the cluster-wide listDatabases action and is refused with WRITE_ACCESS_DETECTED.
  5. Click Restrict to Specific Clusters/Databases and select only the database BurnLedger needs.

For a custom role restricted to one database and collection:

  1. Go to Database Access > Custom Roles.
  2. Create a new role with these actions:
  3. find on myapp.users
  4. listCollections on myapp
  5. Assign this role to the BurnLedger user.

Self-Hosted MongoDB

Connect to your MongoDB instance with an admin user:

// Switch to the admin database
use admin

// Create a read-only role scoped to a specific database and collection
db.createRole({
  role: "burnledgerReadOnly",
  privileges: [
    {
      resource: { db: "myapp", collection: "users" },
      actions: ["find"]
    },
    {
      resource: { db: "myapp", collection: "" },
      actions: ["listCollections"]
    }
  ],
  roles: []
});

// Create the user with this role
db.createUser({
  user: "burnledger_ro",
  pwd: "REPLACE-WITH-YOUR-PASSWORD",
  roles: [
    { role: "burnledgerReadOnly", db: "admin" }
  ]
});

Verify the permissions

Connect as the read-only user:

// Connect
mongosh "mongodb://burnledger_ro:REPLACE-WITH-YOUR-PASSWORD@cluster0-shard-00-00.example.mongodb.net:27017/myapp?tls=true&authSource=admin"

// This should succeed
db.users.find({"email": "test@example.com"}).limit(1)

// These should all fail with "not authorized"
db.users.insertOne({"email": "test@test.com"})
db.users.updateOne({"email": "test@example.com"}, {$set: {"name": "changed"}})
db.users.deleteOne({"email": "test@example.com"})
db.users.drop()

Hash Scope Options

full

Hashes the complete BSON content of every matching document. Field ordering is normalized (sorted lexicographically) before hashing to ensure deterministic results regardless of insertion order.

const system = await bl.registerSystem({
  // ...
});

Use when: You need to prove the exact data that existed, or prove that data was truly deleted and not merely modified.

existence

Records only how many documents matched. Document content — including _id — is never read or hashed.

const system = await bl.registerSystem({
  // ...
  hashScope: "existence",
});

Use when: You only need to prove that documents existed and were later removed, without proving their content.


Multiple Collections

If a data subject's information spans multiple collections, register each collection as a separate system and include all system IDs when creating an attestation.

TypeScript

const usersSystem = await bl.registerSystem({
  name: "users-collection",
  connectorType: "mongodb",
  connectionConfig: {
    uri: process.env.MONGODB_URI,
    database: "myapp",
    collection: "users",
  },
  subjectQuery: '{"email": "$IDENTIFIER"}',
});

const ordersSystem = await bl.registerSystem({
  name: "orders-collection",
  connectorType: "mongodb",
  connectionConfig: {
    uri: process.env.MONGODB_URI,
    database: "myapp",
    collection: "orders",
  },
  subjectQuery: '{"customerEmail": "$IDENTIFIER"}',
});

const attestation = await bl.attest("user@example.com", {
  systemIds: [usersSystem.id, ordersSystem.id],
  proofMode: "merkle",
});

Python

users_system = bl.register_system(
    name="users-collection",
    connector_type="mongodb",
    connection_config={
        "uri": os.environ["MONGODB_URI"],
        "database": "myapp",
        "collection": "users",
    },
    subject_query='{"email": "$IDENTIFIER"}',
)

orders_system = bl.register_system(
    name="orders-collection",
    connector_type="mongodb",
    connection_config={
        "uri": os.environ["MONGODB_URI"],
        "database": "myapp",
        "collection": "orders",
    },
    subject_query='{"customerEmail": "$IDENTIFIER"}',
)

attestation = bl.attest(
    "user@example.com",
    system_ids=[users_system.id, orders_system.id],
    proof_mode="merkle",
)

End-to-End Example

TypeScript

import { BurnLedger } from "burnledger";
import fs from "node:fs/promises";

const bl = new BurnLedger({
  apiKey: process.env.BURNLEDGER_API_KEY,
});

// Register the MongoDB collection
const system = await bl.registerSystem({
  name: "myapp-users",
  connectorType: "mongodb",
  connectionConfig: {
    uri: "mongodb://burnledger_ro:REPLACE-WITH-YOUR-PASSWORD@cluster0-shard-00-00.example.mongodb.net:27017/myapp?tls=true&authSource=admin",
    database: "myapp",
    collection: "users",
  },
  subjectQuery: '{"email": "$IDENTIFIER"}',
});

// Create pre-deletion attestation
const attestation = await bl.attest("jane.doe@example.com", {
  systemIds: [system.id],
  proofMode: "merkle",
});

for (const s of attestation.systems) {
  console.log(`${s.systemName}: ${s.recordCount} documents found`);
}

// Delete the data in your application
// await db.collection("users").deleteMany({ email: "jane.doe@example.com" });

// Verify deletion
// The certificate is ISSUED BY verify() once the data is gone -- there is
// no separate "create certificate" step.
const result = await bl.verify(attestation.id, "jane.doe@example.com", { timeout: 60 });

for (const s of result.systems) {
  console.log(`${s.systemName}: now ${s.recordCount} documents`);
}

if (result.certificate) {
  await bl.savePdf(result.certificate.id, `mongo-deletion-cert-${result.certificate.id}.pdf`);
  console.log(`Certificate issued: ${result.certificate.id}`);
  console.log(`Transparency status: ${result.certificate.transparencyStatus}`);
} else {
  console.log(`Not certified: status ${result.status}`);
}

Python

import os
from burnledger import BurnLedger

bl = BurnLedger(api_key=os.environ["BURNLEDGER_API_KEY"])

# Register the MongoDB collection
system = bl.register_system(
    name="myapp-users",
    connector_type="mongodb",
    connection_config={
        "uri": "mongodb://burnledger_ro:REPLACE-WITH-YOUR-PASSWORD@cluster0-shard-00-00.example.mongodb.net:27017/myapp?tls=true&authSource=admin",
        "database": "myapp",
        "collection": "users",
    },
    subject_query='{"email": "$IDENTIFIER"}',
)

# Create pre-deletion attestation
attestation = bl.attest(
    "jane.doe@example.com",
    system_ids=[system.id],
    proof_mode="merkle",
)

for s in attestation.systems:
    print(f"{s.system_name}: {s.record_count} documents found")

# Delete the data in your application
# db.users.delete_many({"email": "jane.doe@example.com"})

# Verify deletion
# The certificate is ISSUED BY verify() once the data is gone -- there is
# no separate "create certificate" step.
result = bl.verify(attestation.id, "jane.doe@example.com", timeout=60)

for s in result.systems:
    print(f"{s.system_name}: now {s.record_count} documents")

if result.certificate:
    bl.save_pdf(result.certificate.id, f"mongo-deletion-cert-{result.certificate.id}.pdf")
    print(f"Certificate issued: {result.certificate.id}")
    print(f"Transparency status: {result.certificate.transparency_status.value}")
else:
    print(f"Not certified: status {result.status.value}")

Troubleshooting

Error Cause Fix
CONNECTION_FAILED with "authentication failed" Incorrect username/password or wrong auth database Verify credentials. Ensure authSource is correct (Atlas uses admin).
CONNECTION_FAILED with "connection timed out" Network unreachable or IP not allowlisted Add BurnLedger IPs to your Atlas IP access list or firewall.
WRITE_ACCESS_DETECTED The user holds any action outside the built-in read action set — write roles (readWrite, dbOwner) but also readAnyDatabase or clusterMonitor Reassign the user to read on a single database, or a narrower custom role. See Read-Only User Setup.
INVALID_QUERY_TEMPLATE (HTTP 422) JSON is malformed, uses an operator outside the allowlist, or $IDENTIFIER never substitutes Ensure the template is valid JSON using only the allowed operators, with $IDENTIFIER as a whole string value.
CONNECTION_FAILED "MongoDB config missing database" The database field was omitted from the connection config Add "database": "<db>". It is not read from the URI path.
CONNECTION_FAILED "mongodb+srv:// URIs are not supported" An Atlas SRV connection string was used Use the standard mongodb:// string with the host listed explicitly.
CONNECTION_FAILED on attestation: "connector mongodb: transport security plaintext is below the required minimum verified". A health check reports only connection failed — read transport_security on the system The URI has no tls=true (note that tlsInsecure=true alone does not enable TLS) Add ?tls=true to the URI.
CONNECTION_FAILED on attestation: "connector mongodb: transport security encrypted is below the required minimum verified". A health check reports only connection failed — read transport_security on the system tlsInsecure=true / sslInsecure=true alongside tls=true — TLS with no certificate verification Remove it and use a server certificate that chains to a trusted root.
Zero documents when you expect matches The collection name is wrong. A non-existent collection is not an error in MongoDB — it simply matches nothing Verify the collection name; it is case-sensitive. This is exactly the false-negative case to watch for: the attestation will succeed and certify "0 records".
© 2026 ProChatFlow LLC Last updated present → absent → proven