MongoDB
Document-oriented database for flexible schemas, nested data, and large binary payloads via GridFS.

Through TenantsDB, each tenant gets their own MongoDB database with the same indexes and validators, deployed from a blueprint. The proxy handles routing, TLS, query logging, and settings enforcement. Your app connects with any standard MongoDB driver. No SDK needed.

The proxy speaks OP_MSG, supports cursors, GridFS, and binary protocol throughout. Control workspaces give your application a managed backend database. Tenant workspaces track index and validator changes as blueprints for deployment.


Credentials
TenantsDB issues three credential scopes. The proxy enforces which one can reach which database.

Every connection uses your project_id as the username (for example tdb_2abf90d3) and a proxy_password that depends on what you are connecting to. Pick the scope by matching the reach you need.

Project scope

The proxy_password printed when you created the project (or any sk_ key with scope_type=project). Reaches workspaces and the control plane API. Cannot reach individual tenant databases through the wire proxy. Use for admin tooling, the dashboard, and CLI calls.

Workspace scope

Generated by tdb apikeys create --scope-type workspace --scope-values myapp. Reaches the named workspace database AND every tenant database under that blueprint. Use for backend jobs that touch all customers of a blueprint, such as migrations, sweeps, and analytics ingest.

Tenant scope

Auto-generated on every tdb tenants create and returned in the response connection_string. Reaches one tenant database only. Use this for the per-customer database connection in your application.

Need a read-only or write-only key for the same tenant? Create one with tdb apikeys create --role read --scope-type tenant --scope-values wayne.

Every credential also carries a role (admin, write, or read). The proxy logs in to the backend as a native MongoDB user that enforces permissions at the database layer. A read key cannot execute insertOne even if scope allows the connection. Scope and role stack: defense in depth.

Connect to Control Workspace
Your application's backend database. Users, billing, config. Full schema access, no blueprints.
Shell
mongosh "mongodb://tdb_2abf90d3:tdb_d2bf66ed7898c448@mongo.tenantsdb.com:27017/controlplane_workspace?authMechanism=PLAIN&tls=true"

Control mode workspaces accept all schema operations immediately. createCollection, createIndex, validator changes (none are tracked as blueprints, none require deployment). Use this for your application's own collections that are not per-tenant.

The example above uses the project proxy_password. You can also create a workspace-scoped key for controlplane via tdb apikeys create --scope-type workspace --scope-values controlplane and use that instead.


Connect to Tenant Workspace
Where you design and iterate on your tenant schema. Index and collection changes are tracked as versioned blueprints.
Shell
mongosh "mongodb://tdb_2abf90d3:tdb_d2bf66ed7898c448@mongo.tenantsdb.com:27017/myapp_workspace?authMechanism=PLAIN&tls=true"

Every createCollection, createIndex, collMod, or drop you run here is captured as a blueprint version. Deploy it to all tenants with tdb deployments create --blueprint myapp --all.

The project proxy_password works for any workspace in the project. If you want to give one engineer or one CI pipeline access to one blueprint only, create a workspace-scoped key and hand that out instead. Workspace-scoped keys also reach the tenants of that blueprint, which is useful for cross-tenant maintenance jobs.


Connect to Tenant Databases
Isolated production databases for your customers. CRUD only. Schema operations are blocked.

Each tenant has its own proxy_password. The value is returned in the connection_string field of the tdb tenants create response. Save it alongside the tenant record in your application, keyed by tenant_id.

wayne
Shell
mongosh "mongodb://tdb_2abf90d3:tdb_4f2c9d1ab7e8350c@mongo.tenantsdb.com:27017/myapp__wayne?authMechanism=PLAIN&tls=true"
globex
Shell
mongosh "mongodb://tdb_2abf90d3:tdb_e7b1f5c821a04d68@mongo.tenantsdb.com:27017/myapp__globex?authMechanism=PLAIN&tls=true"
Same project, same host, different database, different password. Each tenant carries its own proxy_password so credentials never travel across customers. Lose wayne's password and only wayne is affected.

Schema operations against a tenant database are rejected with MongoServerError: DDL not allowed on tenant databases, use workspace mode. Schema lives in the workspace and is deployed via blueprints.

If you connect to a tenant database using the project proxy_password or an sk_ key, the proxy rejects the first routed command with: MongoServerError: access denied: credential is project-scoped; direct-tenant connections require a tenant-scoped or workspace-scoped key. This is intentional. The project credential is for control plane and workspace access only.

Note: handshake commands (ping, hello, ismaster, buildinfo) are answered by the proxy locally and succeed even with a project credential, because they do not select a database. The scope check fires the moment your driver issues a real routed command (find, insertOne, etc.) with a $db field pointing at the tenant.


Collections & Indexes
Connect to your tenant workspace and define collections, indexes, and validators. Every change is tracked as a blueprint version.
JavaScript (mongosh)
use myapp_workspace

db.createCollection("accounts", {
  validator: {
    $jsonSchema: {
      bsonType: "object",
      required: ["name", "email"],
      properties: {
        name:    { bsonType: "string" },
        email:   { bsonType: "string" },
        balance: { bsonType: "decimal" }
      }
    }
  }
});

db.accounts.createIndex({ email: 1 }, { unique: true });
db.accounts.createIndex({ created_at: -1 });

db.accounts.insertOne({
  name: "Alice",
  email: "alice@test.com",
  balance: NumberDecimal("1000"),
  created_at: new Date()
});
Schema-shaping operations (createCollection, createIndex, collMod, drop) are tracked as blueprint changes. Plain documents (insertOne, updateOne, deleteOne) run in the workspace only and are not deployed to tenants.

Drivers
Pick your language. Each section shows the official MongoDB driver plus notes for the ODMs that build on it. Unlike SQL databases, every MongoDB driver shares a single connection URL format (mongodb://user:pass@host:27017/db?authMechanism=PLAIN&tls=true), so once you have the connection working, all drivers speak the same protocol.

All tenant connection examples below use the per-tenant proxy_password tdb_4f2c9d1ab7e8350c (which would belong to tenant wayne). In your application, look this value up by tenant_id at connect time.

Node.js

The official mongodb driver is the standard. Mongoose (the most common ODM) sits on top of it and works without configuration changes.

Install
npm install mongodb
mongodb driver
JavaScript
const { MongoClient } = require('mongodb');

const client = new MongoClient(
  'mongodb://tdb_2abf90d3:tdb_4f2c9d1ab7e8350c@mongo.tenantsdb.com:27017/myapp__wayne?authMechanism=PLAIN&tls=true'
);

await client.connect();
const db = client.db('myapp__wayne');
const accounts = await db.collection('accounts').find({}).toArray();
await client.close();
Mongoose
JavaScript
const mongoose = require('mongoose');

await mongoose.connect(
  'mongodb://tdb_2abf90d3:tdb_4f2c9d1ab7e8350c@mongo.tenantsdb.com:27017/myapp__wayne?authMechanism=PLAIN&tls=true'
);

const Account = mongoose.model('Account', new mongoose.Schema({
  name:    { type: String, required: true },
  email:   { type: String, required: true, unique: true },
  balance: mongoose.Decimal128
}));

const accounts = await Account.find({});
Install Mongoose separately if needed: npm install mongoose. It bundles its own mongodb driver.
Python

pymongo is the standard sync driver. motor is the async variant, built on top of pymongo for use with FastAPI and other async frameworks. Both use the same connection URL.

Install
pip install pymongo    # sync
pip install motor      # async (depends on pymongo)
pymongo
Python
from pymongo import MongoClient

client = MongoClient(
    "mongodb://tdb_2abf90d3:tdb_4f2c9d1ab7e8350c@mongo.tenantsdb.com:27017/myapp__wayne?authMechanism=PLAIN&tls=true"
)
db = client["myapp__wayne"]
for doc in db.accounts.find({}):
    print(doc)
client.close()
motor (async)
Python
import asyncio
from motor.motor_asyncio import AsyncIOMotorClient

async def main():
    client = AsyncIOMotorClient(
        "mongodb://tdb_2abf90d3:tdb_4f2c9d1ab7e8350c@mongo.tenantsdb.com:27017/myapp__wayne?authMechanism=PLAIN&tls=true"
    )
    db = client["myapp__wayne"]
    async for doc in db.accounts.find({}):
        print(doc)
    client.close()

asyncio.run(main())
Beanie (FastAPI's preferred ODM) and ODMantic both wrap motor. Pass the same connection URL to their init_beanie or AIOEngine setup.
Go

mongo-go-driver is the official driver. There is no widely-used ODM layer in Go; most apps use the driver directly with struct tags.

Install
go get go.mongodb.org/mongo-driver/mongo
Go
package main

import (
    "context"
    "fmt"

    "go.mongodb.org/mongo-driver/bson"
    "go.mongodb.org/mongo-driver/mongo"
    "go.mongodb.org/mongo-driver/mongo/options"
)

type Account struct {
    Name    string `bson:"name"`
    Email   string `bson:"email"`
    Balance float64 `bson:"balance"`
}

func main() {
    ctx := context.Background()
    uri := "mongodb://tdb_2abf90d3:tdb_4f2c9d1ab7e8350c@mongo.tenantsdb.com:27017/myapp__wayne?authMechanism=PLAIN&tls=true"
    client, err := mongo.Connect(ctx, options.Client().ApplyURI(uri))
    if err != nil { panic(err) }
    defer client.Disconnect(ctx)

    coll := client.Database("myapp__wayne").Collection("accounts")
    cursor, _ := coll.Find(ctx, bson.M{})
    var results []Account
    cursor.All(ctx, &results)
    fmt.Println(results)
}
Java

The official MongoDB Java driver works directly. Add directConnection=true to the URL to skip replica set discovery, since the proxy presents itself as a single-node instance.

Maven
<dependency>
  <groupId>org.mongodb</groupId>
  <artifactId>mongodb-driver-sync</artifactId>
  <version>5.1.0</version>
</dependency>
Java
import com.mongodb.client.*;
import org.bson.Document;

String uri = "mongodb://tdb_2abf90d3:tdb_4f2c9d1ab7e8350c@mongo.tenantsdb.com:27017/"
    + "myapp__wayne?authMechanism=PLAIN&tls=true&directConnection=true";

try (MongoClient client = MongoClients.create(uri)) {
    MongoDatabase db = client.getDatabase("myapp__wayne");
    MongoCollection<Document> accounts = db.getCollection("accounts");

    for (Document doc : accounts.find()) {
        System.out.println(doc.toJson());
    }
}
Spring Data MongoDB and Morphia both work on top of the official driver. Configure the URL in spring.data.mongodb.uri (Spring) or your Morphia Datastore setup.

Proxy Behavior
MongoDB-specific details about how the proxy handles your queries.
Wire Protocol

The proxy speaks OP_MSG end-to-end. find, getMore, aggregation pipelines, bulk writes, and GridFS chunks all pass through. The proxy advertises MongoDB 6.0.0 with maxWireVersion=17, which covers every feature modern drivers (3.6 and newer) rely on.

Authentication

The proxy uses SASL PLAIN authentication over TLS. The connection is encrypted before credentials are exchanged. Pass authMechanism=PLAIN and tls=true in every URL.

Credential Scope

The proxy enforces credential scope when your driver issues its first routed command (a command that includes a $db field). Project credentials can connect to workspaces. Workspace-scoped credentials can connect to their workspace AND every tenant of that blueprint. Tenant-scoped credentials can only connect to their specific tenant database.

Using a project credential against a tenant database surfaces as:

Error
MongoServerError: access denied: credential is project-scoped; direct-tenant connections require a tenant-scoped or workspace-scoped key

Using a workspace-scoped credential against a different blueprint's tenant surfaces as:

Error
MongoServerError: access denied: credential scoped to workspaces [myapp], attempted workspace "other_app"

Handshake commands (ping, hello, ismaster, buildinfo) succeed regardless of scope because they do not select a database. The scope check fires on the first routed command.

Role Enforcement

Each API key has a role (admin, write, or read) which the proxy maps to a native MongoDB user on the backend connection. A read-role key attempting an insertOne is rejected by MongoDB itself, not just by the proxy.

Through the wire proxy the failure looks like:

Error
MongoServerError: not authorized on myapp__wayne to execute command { insert: "accounts", ... }

Through the control plane POST /tenants/{id}/query endpoint the same operation returns HTTP 403 with code: permission_denied and the MongoDB "not authorized" message in the error body. Same underlying MongoDB role, two different transports.

This is defense in depth. Even if the proxy were bypassed, the database role would still reject the write. Scope (set on the API key) controls which databases you can connect to. Role (mapped to a MongoDB user) controls what you can do once connected.

Cross-Protocol Guard

If you connect to a workspace or tenant whose blueprint targets a different database type (PostgreSQL, MySQL, Redis), the proxy rejects the connection with a clear pointer to the right proxy:

Error
blueprint "pg_test" is PostgreSQL, not MongoDB. Connect via the postgresql proxy instead.
Connection Limits & Auth-Ban

The proxy enforces per-IP connection limits and a wire-level auth-ban tracker. These apply to MongoDB the same way they apply to PostgreSQL, MySQL, and Redis. Full values and rejection message formats live in Connection Limits & Rejection Behavior.

For MongoDB specifically, IP-level rejections arrive as an OP_MSG with an errmsg field, code 16500, codeName ConnectionRejected. Standard drivers (mongodb, pymongo, motor, mongo-go-driver, mongodb-driver-sync) surface this as a MongoServerError. Example:

Error
MongoServerError: connection rejected: your IP is temporarily rate-limited after repeated failed auth attempts, retry in 47s

The TTL portion (retry in 47s) decrements on each retry while the ban is active, so client retry logic can parse it for accurate backoff. This IP-level rejection is distinct from the scope rejection described above: IP bans block the connection at accept time, before any MongoDB handshake or routing happens.

Row Limit Behavior

When the workspace setting max_rows_per_query is reached, the proxy truncates the result set and adds a top-level field to the response:

Response
{
  "cursor": { "id": 0, "firstBatch": [ ... 10 docs ... ] },
  "warning": "results truncated to 10 rows by tenant max_rows_per_query setting",
  "ok": 1
}

The cursor is closed (cursor.id = 0), so the driver will not issue further getMore calls. Application code should check for the warning field on the top level of the response when partial results matter.

GridFS

GridFS file chunks (fs.chunks collection) are exempt from max_rows_per_query truncation. Truncating chunk reads would corrupt large file downloads. The exemption applies only to the chunks collection: fs.files metadata queries follow the regular row limit.

Settings Enforcement

The proxy enforces max_rows_per_query, query_timeout_ms, and max_connections at the proxy level. These are configured per workspace and apply to all tenants using that blueprint.