Why It's Different
Cross-database search across isolated tenant databases is not something you can build easily. Here it ships out of the box.

Most search solutions assume a single data source - one database, one schema, one tenant. Scaling that to multi-tenant means writing an indexing pipeline per database type, partitioning the index per tenant, keeping everything in sync as schemas evolve, and making sure no tenant's query ever touches another's data. That is a significant engineering project on its own.

TenantsDB Search solves all of that as a single primitive. Because TenantsDB sits between your application and every database, it sees every write across every tenant and every database type. The indexer uses that position to build a unified, tenant-partitioned search index automatically.

Without TenantsDB
Deploy and operate a separate search service
Write an indexing pipeline per database type
Partition the index per tenant manually
Maintain index mappings on every schema change
Query each database type separately
Merge and re-rank results in your own code
Pay for and operate extra infrastructure
With TenantsDB Search
Nothing to deploy
Nothing to configure
Tenant isolation is structural, not a filter
Schema changes picked up automatically
All database types in one API call
Results ranked and merged for you
Included in the platform, all tiers
Search is available on all tiers. No plan upgrade required.

Use Cases
Search unlocks capabilities that are disproportionately hard to build otherwise.
Support and Admin Lookup
Your support team types a customer name, order ID, or email into your admin panel. Search returns the matching record instantly regardless of which database or table it lives in. No need to know whether it is in PostgreSQL orders, MongoDB tickets, or a Redis session.
In-App Search for Your Customers
Give each tenant a search bar inside your product. Call POST /tenants/{id}/search from your backend with the authenticated tenant's ID. Results come back with source_db and collection so you can render them differently by type. Each tenant only ever sees their own data.
Cross-Tenant Monitoring
Use POST /tenants/_all/search to scan across your entire customer base. Find all tenants with overdue invoices, accounts in a specific state, or records matching a compliance keyword. Results include tenant_id so you can act per customer.
Cross-Database Record Linking
A single customer might have a profile in PostgreSQL, activity logs in MongoDB, and a session in Redis. Search surfaces all of them in one query, so you can build unified customer timelines or audit views without manually joining across database types in code.

Real-Time Indexing
Every write is indexed automatically. Your application does nothing differently.

TenantsDB runs an internal indexer that subscribes to the message bus. Every time a write completes through the proxy, the indexer receives the event and indexes the affected rows or documents into a tenant-partitioned namespace. By the time your application could issue a search, the data is already there.

Your App
INSERT / UPDATE
Proxy
Executes + acks
Message Bus
Write event
Indexer
Writes to namespace
Search Index
Ready to query
Indexing is async. Your writes are never delayed by the indexer.
The proxy acknowledges the write to your application before indexing begins. Search indexing never adds latency to your writes.

Supported Databases
All four database types are indexed. What gets indexed depends on the data model of each engine.
DatabaseIndexedWhat Gets Indexed
PostgreSQL All rows from all tables. Every column value indexed as a searchable field. id and created_at extracted automatically when present.
MySQL Same as PostgreSQL. All rows and columns indexed per write event.
MongoDB Full documents indexed field by field. Nested fields are flattened. GridFS collections are excluded automatically.
Redis Hash fields (HSET) indexed. Plain strings, lists, and keys with TTL under 60 seconds are excluded.

Tenant Isolation
Isolation is structural. It is enforced at write time, not at query time.

When a write is indexed, it is written into a namespace derived from your project ID and the tenant ID. The namespace is immutable and tied to the data at the moment of indexing. When a search query arrives for tenant acme, the engine resolves the namespace for acme and only that namespace is queried. There is no filter that runs at query time that could be misconfigured, bypassed, or accidentally omitted.

Index partitioning - write time
Write
acme / users
Write
globex / orders
Write
stark / invoices
Indexer
Routes by tenant
ns:acme
users / row 12
orders / row 88
ns:globex
orders / row 201
ns:stark
invoices / row 9
Every write is stamped with its tenant namespace before it enters the index. Namespaces never overlap.
Query scoping - search time
POST /tenants/acme/search
Query
"invoice overdue"
ns:acme only
other namespaces not touched
Results
acme data only
POST /tenants/_all/search
Query
"invoice overdue"
ns:acme
ns:globex
ns:stark
Results
each tagged with tenant_id
Single-tenant queries resolve one namespace. The _all endpoint queries all namespaces in parallel, tagging each result with its tenant. Namespaces are never mixed.
Tenant Status and Search Visibility
Tenant StatusPOST /tenants/{id}/searchPOST /tenants/_all/search
ready✓ Yes✓ Yes
suspended✓ Yes✓ Yes
deleted (soft)✗ No✗ No
provisioning / migratingPartialPartial
Never use _all to serve per-tenant search results in your product. Always use POST /tenants/{id}/search with the authenticated tenant's ID. The _all endpoint is for your own internal admin and analytics use.

Scoring
Results are ranked by relevance. Higher score means a stronger match.

Each result includes a score field. Scores are floating-point relevance values computed using TF-IDF weighting: how often the term appears in the document versus how rare the term is across all documents in the namespace. Results are returned sorted by score descending.

What Increases ScoreWhat Decreases Score
Query term appears multiple times in the documentTerm is very common across all documents in the namespace
Term appears in a short field (high density)Document has many fields where the term does not appear
Exact match on the full query stringOnly a partial token match

Scores are not normalized to a fixed range. A score around 3.0 is typical for a strong single-field match. Use scores to rank results, not as an absolute threshold.


Filtering
Narrow results by database type or collection. Filtering happens at the index level, before scoring.

Both databases and collections filters are applied inside the search engine at query time, reducing scope before scoring runs. Filtered searches are faster and return tighter results.

Filter by Database Type
Shell
# Search only PostgreSQL and MongoDB, skip MySQL and Redis
$ tdb search --tenant acme --query "Alice" --databases PostgreSQL,MongoDB
Filter by Collection
Shell
# Search only the invoices and orders tables
$ tdb search --tenant acme --query "overdue" --collections invoices,orders

# Combine both filters
$ tdb search --tenant acme --query "overdue" \
    --databases PostgreSQL \
    --collections invoices,orders
Both filters are optional and independent. You can use either, both, or neither.

API Reference
Two endpoints. One for a single tenant, one for all active tenants at once.
POST /tenants/{tenantID}/search
Keyword search across all databases for a single tenant. Queries only the namespace belonging to this tenant.
Request Body
FieldTypeDescription
querystringrequiredKeyword search string. Matched against all indexed field values.
databasesstring[]optionalLimit to specific database types: PostgreSQL, MySQL, MongoDB, Redis.
collectionsstring[]optionalLimit to specific tables or collections (e.g., ["users", "orders"]).
limitintoptionalMaximum results to return. Default: 20.
Example Request
Shell
curl -X POST https://api.tenantsdb.com/tenants/acme/search \
  -H "Authorization: Bearer $API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "query": "NativeCheck1",
    "databases": ["PostgreSQL"],
    "limit": 10
  }'
Response
HTTP 200
{
  "success": true,
  "total": 1,
  "took_ms": 106,
  "results": [
    {
      "source_db": "PostgreSQL",
      "collection": "users",
      "score": 3.138,
      "content": {
        "id": 49,
        "name": "NativeCheck1",
        "email": "[email protected]",
        "created_at": "2026-03-04T23:52:00Z"
      }
    }
  ]
}
Response Fields
POST /tenants/_all/search
Keyword search across all active tenants simultaneously. Queries all tenant namespaces in parallel. Soft-deleted tenants are excluded. Each result includes a tenant_id field.

The request body is identical to the single-tenant endpoint. The response adds tenant_id on each result.

Example Request
Shell
curl -X POST https://api.tenantsdb.com/tenants/_all/search \
  -H "Authorization: Bearer $API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "query": "overdue invoice",
    "collections": ["invoices"],
    "limit": 50
  }'
Response
HTTP 200
{
  "success": true,
  "total": 3,
  "took_ms": 142,
  "results": [
    {
      "tenant_id": "acme",
      "source_db": "PostgreSQL",
      "collection": "invoices",
      "score": 4.21,
      "content": {
        "id": 201,
        "status": "overdue",
        "amount": 4800.00,
        "created_at": "2026-02-01T09:00:00Z"
      }
    },
    {
      "tenant_id": "globex",
      "source_db": "PostgreSQL",
      "collection": "invoices",
      "score": 3.87,
      "content": {
        "id": 87,
        "status": "overdue",
        "amount": 1200.00,
        "created_at": "2026-01-28T11:30:00Z"
      }
    }
  ]
}
Use collections to narrow scope when using _all. Without a filter it queries every indexed table across every active tenant.

CLI Reference
FieldTypeDescription
totalintTotal number of matching results.
took_msintSearch execution time in milliseconds.
results[].source_dbstringDatabase type this result came from: PostgreSQL, MySQL, MongoDB, or Redis.
results[].collectionstringTable or collection name.
results[].scorefloatRelevance score. Higher is a stronger match. Sorted descending.
results[].contentobjectThe full indexed document. id (or _id for MongoDB) extracted into the ID column. All results include an indexed timestamp in the CREATED column.
FlagDefaultDescription
--tenantrequired-Tenant ID to search, or _all for all active tenants.
--queryrequired-Keyword search string.
--databasesoptionalallComma-separated database types (e.g., PostgreSQL,MongoDB).
--collectionsoptionalallComma-separated table or collection names (e.g., users,orders).
--limitoptional20Maximum number of results to return.
--jsonoptionalfalseOutput full raw JSON instead of the formatted table.
Examples
Shell
# Search a single tenant
$ tdb search --tenant demo --query "OQLTest1"

DATABASE    COLLECTION  ID  SCORE  CREATED       CONTENT
PostgreSQL  users       50  3.093  Mar 05 00:15  email: [email protected] | name: OQLTest1
1 results in 112ms

# Insert with native SQL, then search
$ tdb query --tenant demo --blueprint App1 \
    --query "INSERT INTO users (name, email) VALUES ('NativeCheck1', '[email protected]')"
$ tdb search --tenant demo --query "NativeCheck1"

DATABASE    COLLECTION  ID  SCORE  CREATED       CONTENT
PostgreSQL  users       49  3.138  Mar 04 23:52  name: NativeCheck1 | email: [email protected]
1 results in 106ms

# Filter by database type
$ tdb search --tenant acme --query "Alice" --databases PostgreSQL

# Filter by collection
$ tdb search --tenant acme --query "overdue" --collections invoices,orders

# Search all tenants
$ tdb search --tenant _all --query "overdue invoice" --collections invoices --limit 50

# Raw JSON output
$ tdb search --tenant acme --query "Alice" --json
Table Output

By default results print as a table. System fields (id, _id, created_at, updated_at) are extracted into dedicated columns. All remaining fields appear in the CONTENT column as key: value pairs, truncated at 100 characters.

JSON Output

Use --json for the full raw API response with all content fields, exact scores, and no truncation. Useful for scripting or piping into other tools.


Next Steps