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 rejectsmongodb+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 standardmongodb://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 wholeh1:27017,h2string is treated as one hostname, fails DNS resolution, and the system is rejected withBAD_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-connectionreportsreplication.rolefromhello—primaryorreplica— 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:replSetGetStatusneedsclusterMonitor, 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
$IDENTIFIERplaceholder, 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 $elemMatchEvery 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:
- BurnLedger substitutes the subject value into the template, replacing
$IDENTIFIERwith the actual value as a string. - It runs
db.collection.find(query)on the specified collection. - It computes a deterministic hash of the matching documents (or their existence, depending on hash scope).
- 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:
- Go to Database Access.
- Click Add New Database User.
- Set authentication method to Password.
- 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-widelistDatabasesaction and is refused withWRITE_ACCESS_DETECTED. - Click Restrict to Specific Clusters/Databases and select only the database BurnLedger needs.
For a custom role restricted to one database and collection:
- Go to Database Access > Custom Roles.
- Create a new role with these actions:
findonmyapp.userslistCollectionsonmyapp- 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". |