How Connections Work
Your application never connects to raw database servers. Every connection goes through the TenantsDB proxy, which reads the database name from your connection string and routes to the right backend.
How routing works
Your Application
Same host · Same project · Per-tenant password · Different database name
TenantsDB Proxy
TLS encrypted · Enforces credential scope · Routes by database name
controlplane_workspace Your backend. Users, billing, config.
myapp_workspace Schema design. DDL tracked as blueprint.
myapp__wayne Customer A. Isolated data, CRUD only, per-tenant password.
myapp__globex Customer B. Isolated data, CRUD only, per-tenant password.
One project, one proxy. Scope controls who can connect; the database name controls what gets routed.
Your app connects to pg.tenantsdb.com, mysql.tenantsdb.com, mongo.tenantsdb.com, or redis.tenantsdb.com. Same pattern for every database type.

Three Connection Types
Every connection to TenantsDB falls into one of three categories. The database name determines which type.
Control
Your App's Backend
Users, billing, config. Everything that isn't per-tenant. Full DDL and DML access. No blueprints, no versioning.
controlplane_workspace
Workspace
Schema Design
Where you build and iterate on your schema. DDL changes are tracked as versioned blueprints and deployed to tenants.
myapp_workspace
Tenant
Customer Database
Isolated production database for one customer. Schema deployed from blueprint. CRUD only. DDL is blocked.
myapp__wayne
TypeDDLDMLBlueprintWho connects
Control ✓ Full access None Your application
Workspace ✓ Tracked Versioned You, during development
Tenant ✗ Blocked Deployed Your application, per customer
Your application typically holds two connection pools: one to the control database (shared across all requests) and one per tenant (resolved from authentication). See Quick Start for the full wiring pattern.

Credential Scopes
TenantsDB issues three credential scopes. The proxy enforces which scope can reach which connection type. Same model across all four databases.

Every credential carries two attributes: a scope (which databases it can connect to) and a role (what it can do once connected). Scope is enforced at the proxy handshake. Role is enforced by the native database itself via a backing role user.

Project scope
Admin reach, not tenants
The proxy_password printed at project creation, plus any sk_ key with scope_type=project. Reaches workspaces and the control plane API. Cannot reach tenant databases through the wire proxy.
Admin tools, dashboard, CLI
Workspace scope
Blueprint admin
Created by tdb apikeys create --scope-type workspace --scope-values myapp. Reaches the workspace AND every tenant of that blueprint. For cross-tenant maintenance jobs (migrations, sweeps, analytics ingest).
Backend jobs
Tenant scope
One tenant only
Auto-generated on every tdb tenants create and returned in the response. Reaches one tenant database. Use this for the per-customer connection in your application.
App backend, per request
Scope Control workspace Tenant workspace Tenant DB (same blueprint) Tenant DB (other blueprint)
Project
Workspace (myapp)
Tenant (wayne) ✓ (wayne only)
Project credentials reaching a tenant database fail the handshake with credential is project-scoped; direct-tenant connections require a tenant-scoped or workspace-scoped key. Each native client surfaces this differently (Postgres shows the message inline, MySQL collapses it to ERROR 1045, Redis to AUTH failed: ERR access denied). The proxy logs always show the full reason.
Every credential also has a role: admin, write, or read. The role is enforced by the native database via a backing role user, so a read key cannot INSERT even if its scope allows the connection. Scope + role stack: defense in depth.

Query Paths
There are two ways to run a query against a tenant. Both land on the same database. Both enforce the same scope and the same role. Pick the one that matches your client.
Wire-protocol proxy
Native database clients
psql, mysqlsh, mongosh, redis-cli, and any ORM (pg, mysql2, mongoose, ioredis, GORM, SQLAlchemy, Prisma).
pg.tenantsdb.com:5432
Authenticates with proxy_password. Native SQL/Mongo/Redis only.
HTTP API
tdb CLI, curl, MCP clients
tdb query, direct curl against the API, OAuth/MCP integrations like Claude.
api.tenantsdb.com
Authenticates with api_key. Accepts OQL and native queries.
Wire-protocol proxyHTTP API
Host pg.tenantsdb.com / mysql. / mongo. / redis. api.tenantsdb.com
Authenticates with proxy_password (short, tdb_ prefix) api_key (long, tdb_sk_ prefix) in Authorization: Bearer
Scope enforcement Handshake. Project credentials are rejected for direct-tenant connections. Request validation. Same scope rules; same credential, same outcome.
Query languages Native SQL / Mongo / Redis only Native or OQL
Per-tenant query Connect with myapp__wayne as the database name POST /tenants/{id}/query
Cross-tenant fan-out Not supported (one DB per connection) POST /admin/query with all_tenants: true. Requires role=admin.
Role enforcement Database role (admin/write/read) at the SQL/Mongo/Redis layer Same database role at the SQL/Mongo/Redis layer. Defense in depth.
DDL on tenants Blocked. Deploy via blueprints. Blocked. Deploy via blueprints.
Best for Application backends with ORMs, dashboards using BI tools, anything that opens a long-lived database connection CLI workflows, AI/MCP clients, scripts, any caller that does not want to manage a wire-protocol connection pool
A key's scope and role apply to both paths. A read-role key attempting an INSERT is rejected the same way whether the request comes through the proxy or the HTTP API. A project-scoped key attempting to reach a tenant database is rejected the same way too. The enforcement lives on the server, not the client.

Connection Format
Database names follow a consistent pattern. The proxy uses the name to identify the workspace or tenant.
TypePatternExample
Control workspace {name}_workspace controlplane_workspace
Tenant workspace {blueprint}_workspace myapp_workspace
Tenant database {blueprint}__{tenant} myapp__wayne
Double underscore __ is a reserved separator. Workspace and tenant names cannot contain __.

Endpoints
One host per database type. All connections require TLS.
DatabaseHostPortTLS
PostgreSQLpg.tenantsdb.com5432?sslmode=require
MySQLmysql.tenantsdb.com3306TLS required
MongoDBmongo.tenantsdb.com27017&tls=true
Redisredis.tenantsdb.com6379rediss:// scheme
PostgreSQL postgresql://{project_id}:{proxy_password}@pg.tenantsdb.com:5432/{database}?sslmode=require
MySQL mysql://{project_id}:{proxy_password}@mysql.tenantsdb.com:3306/{database}
MongoDB mongodb://{project_id}:{proxy_password}@mongo.tenantsdb.com:27017/{database}?authMechanism=PLAIN&directConnection=true&tls=true
Redis rediss://{tenant_id_or_blueprint}:{proxy_password}@redis.tenantsdb.com:6379/0
The {proxy_password} value depends on which connection type you're hitting. Project proxy_password for workspaces. Tenant-scoped or workspace-scoped proxy_password for tenant databases. See Credential Scopes above.
MySQL TLS is configured per driver, not via URL parameter. See the MySQL page for per-language examples. Redis uses the username field as the routing identifier: blueprint name for workspace, tenant ID for tenant.

Connection Limits & Rejection Behavior
The proxy enforces per-IP connection limits to protect shared infrastructure. Limits are protocol-agnostic: the same caps apply to PostgreSQL, MySQL, MongoDB, and Redis wire connections.
Per-IP Caps
LimitValueScopePurpose
Concurrent connections 200 per IP Per proxy pod Maximum active wire connections from a single source IP at any time. Per source IP, not per project, so multi-server deployments scale naturally with their fleet.
Connection rate 10 / second Per IP New connections per second from a single source IP. Smooths burst opens during reconnect storms.
Burst allowance 30 Per IP Token-bucket capacity. Short bursts above the steady rate are tolerated.
Auth-Ban Tracker

Repeated failed AUTH attempts from one IP trigger a temporary ban at the wire-protocol layer. This catches credential-stuffing and brute-force attempts.

ParameterValueDescription
Failure threshold 10 failures in 3 min 10 failed AUTH attempts from one IP within a 3-minute window triggers a ban.
Ban duration 3 minutes The ban lasts 3 minutes from when it triggers. Successful AUTHs during the ban do not clear it.
Counter reset On successful AUTH A successful AUTH (when not banned) clears the failure counter, so a transient typo followed by the right password does not accumulate toward future bans.
Rejection Messages

When a connection is rejected at the IP layer, the proxy sends a protocol-level error frame before closing. Native database clients surface this as a normal exception, not a silent connection drop.

ReasonMessage
Auth-ban your IP is temporarily rate-limited after repeated failed auth attempts, retry in 47s
Connection rate your IP is opening connections too quickly, please slow down
Concurrent cap your IP has too many concurrent connections, reduce concurrency or contact support

The auth-ban message includes the remaining ban duration (e.g. retry in 47s) so client libraries can implement automatic retry. The TTL decrements on each subsequent attempt. Each protocol returns this in its native error format:

ProtocolError frameState / code
PostgreSQL ErrorResponse SQLSTATE 53300, severity FATAL
MySQL Error Packet Code 1040, SQLSTATE 08004
MongoDB OP_MSG (errmsg field) Code 16500, codeName ConnectionRejected
Redis -ERR reply Prefix connection rejected:
Cross-System Ban Behavior

TenantsDB has two independent ban systems: the wire-protocol auth-ban above, and the HTTP API rate-limit ban documented in Rate Limits. They coordinate in one direction only.

Wire → HTTP
Bans propagate
A wire-level auth-ban also blocks HTTP API requests from the same IP. Repeated bad AUTH attempts are a strong signal of an attacker, so we lock them out of both surfaces.
HTTP → Wire
Bans stay local
An HTTP rate-limit violation does NOT cut wire (database) connections. A customer bursting the management API still gets uninterrupted database access. Rate-limit violations are a weak signal (often legitimate bursts), not strong attacker evidence.
Customer applications hitting any of these limits see a clear protocol-level error frame instead of a silent TCP close. The error includes the reason and, for auth-bans, the remaining time before retry. Build retry logic to parse retry in Xs if you want automatic backoff.