Recommended resources
The freely available resources this lesson will direct you to.
- Postgres multi-tenant architecture for AI apps (database per user/workspace/org)www.youtube.com
- Implementing Multi-Tenant Architecture the RIGHT Waywww.youtube.com
- Building Multi-Tenant Apps With Better-Auth and ZenStackzenstack.dev
- AI Agent Tools: MCP, OpenAPI, and Tool Management That Actually ...www.chanl.ai
Introduction
In our last lesson, we successfully built the relational backbone connecting tenants to the AI agents they are permitted to use. By implementing a many-to-many mapping, we established a clear mechanism for defining agent availability on a per-tenant basis. This was a critical step, but an agent's identity is only part of the story; its true power lies in the tools and prompts it can wield.
This lesson drills down to that next layer of granularity. We will explore how to create tenant-to-tool and tenant-to-prompt mappings, effectively controlling the specific capabilities available to each tenant's agents. This is the mechanism that allows you to offer different tiers of service—for example, a "Pro" plan might grant access to advanced data analysis tools, while a "Free" plan offers only basic text-generation capabilities. You will apply the same data modeling pattern from the previous lesson, reinforcing a core architectural principle for building scalable, secure multi-tenant systems.
The Principle of Scoped Resources
In a multi-tenant application, sharing a single database and application instance among all tenants is a cost-effective and scalable approach. However, it introduces a significant challenge: ensuring that tenants can only access their own data and resources.
As an experienced developer, you're familiar with the principle of least privilege. In our agentic infrastructure, this means a tenant's agent should only have access to the exact set of tools and prompts it is authorized to use. Failing to enforce this can lead to several problems:
- Cost Overruns: A free-tier user might inadvertently trigger a computationally expensive, premium-only tool.
- Data Leakage: A tool configured with one tenant's API keys or data sources could be mistakenly accessed by another.
- Inconsistent User Experience: Agents might behave unpredictably if they have access to an incorrect or incomplete set of prompts and tools.
The solution is to rigorously segregate resources using a tenantId. The following video provides a clear explanation of multi-tenancy concepts and the importance of using a tenantId to isolate data within a shared database.
Implementing Multi-Tenant Architecture the RIGHT Way
This video from Jan Marshal explains the fundamental difference between single-tenant and multi-tenant architectures and introduces the core concept of data segregation using a tenant ID.
Watch the segment from shared resources. Focus on the explanation of how a tenantId is used in a WHERE clause to ensure a query only returns data belonging to the correct tenant. This is the foundational concept we will be building upon.
Modeling Tenant-to-Tool and Tenant-to-Prompt Mappings
Just as we did for agents, we will use an explicit many-to-many relationship to map tools and prompts to tenants. The logic is identical: a tool can be assigned to many tenants, and a tenant can have many tools. The same applies to prompts. This consistency in design is a hallmark of a well-architected system.
Drawing on the Prisma schema design from our previous lesson, we will introduce two new join tables: tenant_tools and tenant_prompts.
Here is how we can extend our schema.prisma file:
// Existing Tenant and Agent models...
// --- Tool Mappings ---
// Represents a tool in our system (from Module 3)
model Tool {
id String @id @default(cuid())
name String @unique
description String?
// Other tool metadata...
tenants TenantTool[] // Relation to the join table
@@map("tools")
}
// Join table for the Tenant-Tool many-to-many relationship
model TenantTool {
tenant Tenant @relation(fields: [tenantId], references: [id], onDelete: Cascade)
tenantId String @map("tenant_id")
tool Tool @relation(fields: [toolId], references: [id], onDelete: Cascade)
toolId String @map("tool_id")
// You could add tenant-specific overrides here, e.g.:
// isEnabled Boolean @default(true)
// configOverrides Json?
createdAt DateTime @default(now()) @map("created_at")
@@id([tenantId, toolId])
@@map("tenant_tools")
}
// --- Prompt Mappings ---
// Represents a prompt template in our system (from Module 3)
model Prompt {
id String @id @default(cuid())
name String @unique
template String
version String @default("1.0.0")
// Other prompt metadata...
tenants TenantPrompt[] // Relation to the join table
@@map("prompts")
}
// Join table for the Tenant-Prompt many-to-many relationship
model TenantPrompt {
tenant Tenant @relation(fields: [tenantId], references: [id], onDelete: Cascade)
tenantId String @map("tenant_id")
prompt Prompt @relation(fields: [promptId], references: [id], onDelete: Cascade)
promptId String @map("prompt_id")
createdAt DateTime @default(now()) @map("created_at")
@@id([tenantId, promptId])
@@map("tenant_prompts")
}
Notice the onDelete: Cascade. This is a useful addition that ensures if a Tenant or a Tool is deleted, the corresponding mapping records in the join table are automatically cleaned up, maintaining data integrity.
A More Scalable Approach: Toolsets
As your system grows, managing individual tool assignments for hundreds of tenants can become cumbersome. A more scalable pattern is to group related tools into "toolsets" and assign these sets to tenants. For instance, you could have a "Customer Support Toolset" or a "Data Analytics Toolset".
The article "AI Agent Tools" by Chanl.ai provides an excellent overview of this concept.
AI Agent Tools: MCP, OpenAPI, and Tool Management That Actually ...
This article discusses production-grade tool management for AI agents, including the concept of toolsets.
Read the section titled MCP Toolsets. Pay attention to the Toolset interface and how it groups toolIds. This approach simplifies administration by abstracting away individual tool management, a pattern you could easily implement using our established many-to-many model (i.e., tenants to toolsets).
Enforcing Scoping at the Data Access Layer
Having the database mappings is only half the battle. The crucial part is to enforce them consistently across the entire application. Relying on developers to manually add where: { tenantId: '...' } to every single database query is prone to error and creates a significant security risk. A forgotten clause could lead to cross-tenant data access.
A much more robust and secure pattern is to enforce this isolation automatically at the data access layer. The idea is to create a "tenant-aware" session or client that automatically injects the tenant context into every query.
The video "Implementing Multi-Tenant Architecture the RIGHT Way," which you reviewed earlier, demonstrates a powerful way to achieve this using the Decorator Pattern.
Implementing Multi-Tenant Architecture the RIGHT Way
This video provides a practical, code-level demonstration of how to build a tenant-aware session decorator that automatically enforces tenant isolation for all database operations.
Watch the section from the decorator solution. This is the most important part of this lesson. The presenter builds a TenantedDocumentSession class that wraps the native database session. This wrapper automatically: Adds a whereEquals('tenantId', ...) clause to every query. Sets the tenantId on every entity before store. Validates the tenantId on load and delete, throwing an error on a mismatch. This "security by default" approach is exactly what is needed for a production-grade multi-tenant application.
Another excellent example of this pattern is found in the "Building Multi-Tenant Apps With Better-Auth and ZenStack" article. It uses a library to create an "enhanced" Prisma client that achieves the same goal through policy enforcement.
Building Multi-Tenant Apps With Better-Auth and ZenStack
This blog post demonstrates a policy-based approach to enforcing multi-tenancy rules using an enhanced Prisma client.
First, read the section on creating the enhanced client. Notice how a userContext containing the organizationId (our tenantId) is created from the session and used to enhance the standard Prisma client. Next, review the policy rules in the subsection Tenant segregation. The rule @@deny('all', auth().organizationId != organizationId) is the key. It instructs the enhanced client to automatically deny any access where the active tenant's ID does not match the record's organizationId. This declarative approach achieves the same outcome as the decorator pattern shown in the video.
Both resources illustrate the same core principle: move tenancy enforcement from the individual developer's responsibility to a centralized, automated part of your data access infrastructure.
Conclusion
In this lesson, you have extended our multi-tenant architecture to encompass tools and prompts, giving you fine-grained control over the capabilities of each tenant's agents. By reusing the many-to-many relationship pattern, you've reinforced a clean and scalable architectural approach. More importantly, you've explored sophisticated, production-ready patterns for automatically enforcing tenant isolation at the data access layer.
Key Takeaways:
- Reusable Mapping Pattern: The many-to-many join table is a versatile pattern for mapping any resource (agents, tools, prompts) to tenants.
- Granular Resource Control: Scoping tools and prompts is essential for implementing tiered pricing, ensuring security, and controlling operational costs in a SaaS environment.
- Automated Tenancy Enforcement: Moving tenant isolation logic into a centralized decorator or an enhanced database client is far more secure and reliable than manual filtering in every query. This "security by default" posture is critical for production systems.
- Scalable Management: Grouping resources into "sets" (like toolsets) can simplify administration as your application and tenant base grow.
We have now designed the database models and access patterns to determine which agents, tools, and prompts a tenant can use. The next step is to put this into practice. In our next lesson, we will implement the logic to resolve the tenant context from an incoming API request and use it to dynamically filter the available resources for the agent pipeline.