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
| Type | DDL | DML | Blueprint | Who 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 proxy | HTTP 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.
Endpoints
One host per database type. All connections require TLS.
| Database | Host | Port | TLS |
| PostgreSQL | pg.tenantsdb.com | 5432 | ?sslmode=require |
| MySQL | mysql.tenantsdb.com | 3306 | TLS required |
| MongoDB | mongo.tenantsdb.com | 27017 | &tls=true |
| Redis | redis.tenantsdb.com | 6379 | rediss:// 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
| Limit | Value | Scope | Purpose |
| 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.
| Parameter | Value | Description |
| 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.
| Reason | Message |
| 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:
| Protocol | Error frame | State / 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.
Choose Your Database
Each database has its own connection guide with shell commands, ORM examples, and proxy behavior. Pick yours.