Skip to content

Build per-tenant search

AI Search supports per-tenant search isolation. You can either create a separate instance for each tenant or use a shared instance with metadata filtering.

Instance per tenant

Create isolated AI Search instances for each tenant at runtime using the namespace binding. Each tenant gets their own instance with separate storage and search index.

JSONC
{
"$schema": "./node_modules/wrangler/config-schema.json",
"ai_search_namespaces": [
{
"binding": "TENANTS",
"namespace": "default"
}
]
}
JavaScript
export default {
async fetch(request, env) {
const url = new URL(request.url);
// Identify the tenant from the request header
const tenantId = request.headers.get("x-tenant-id");
if (!tenantId) {
return new Response("Missing x-tenant-id header", { status: 400 });
}
// Create a new instance for the tenant
if (url.pathname === "/onboard" && request.method === "POST") {
const instance = await env.TENANTS.create({
id: `tenant-${tenantId}`,
});
return Response.json({ success: true, instance: await instance.info() });
}
// Upload a document to the tenant's instance
if (url.pathname === "/upload" && request.method === "POST") {
const formData = await request.formData();
const file = formData.get("file");
// Upload the file to the tenant's built-in storage
const item = await env.TENANTS.get(`tenant-${tenantId}`).items.upload(
file.name,
await file.arrayBuffer(),
);
return Response.json({ success: true, item });
}
// Search the tenant's instance
if (url.pathname === "/search") {
const query = url.searchParams.get("q") || "";
// Each tenant's search is isolated to their own instance
const results = await env.TENANTS.get(`tenant-${tenantId}`).search({
messages: [{ role: "user", content: query }],
});
return Response.json(results);
}
// Delete the tenant's instance and all its data
if (url.pathname === "/offboard" && request.method === "DELETE") {
await env.TENANTS.delete(`tenant-${tenantId}`);
return Response.json({ success: true });
}
return new Response("Not found", { status: 404 });
},
};

Shared instance with metadata filtering

Use a single AI Search instance and organize content by tenant using folder paths. This approach works with both R2 buckets and built-in storage. Apply metadata filters at query time to ensure each tenant only retrieves their own documents.

JSONC
{
"$schema": "./node_modules/wrangler/config-schema.json",
"ai_search": [
{
"binding": "SHARED_INSTANCE",
"instance_name": "shared-instance"
}
]
}

Organize your content by tenant using unique folder paths:

  • Directorycustomer-a
    • Directorylogs/
    • Directorycontracts/
  • Directorycustomer-b
    • Directorycontracts/

When searching, filter by the tenant's folder to restrict results:

TypeScript
// Filter results to only return documents from this tenant's folder
const results = await env.SHARED_INSTANCE.search({
messages: [{ role: "user", content: "When did I sign my agreement?" }],
ai_search_options: {
retrieval: {
filters: {
folder: { $gte: "customer-a/", $lt: "customer-a0" },
},
},
},
});

This example uses a "starts with" filter to match all files under customer-a/ including subfolders.