Pattern 3: Dynamic Tenant Schemas
Dynamically load custom per-tenant collections and globals at runtime using the onSchemaFetch hook.
In multi-tenant SaaS platforms, agency networks, and white-label applications, individual enterprise customers often need their own custom collections, specialized fields, or unique branding globals.
Because tenant configurations change dynamically as customers sign up or customize their accounts, these schemas cannot be hardcoded in a static TypeScript file. Dyrected solves this with the onSchemaFetch hook.
Commercial Feature: Dynamic schema loading at runtime is part of the Dyrected Platform License. It is designed for businesses building multi-tenant SaaS products, agency platforms, and white-label CMS services.
Mental Model
When a client or editor sends a request with X-Site-Id: tenant-acme:
- Dyrected intercepts the request and identifies the target tenant (
tenant-acme). - Dyrected invokes your
onSchemaFetch("tenant-acme")hook. - Your hook queries your application database (e.g. PostgreSQL, MongoDB, or an external config service) and returns the custom collections and globals for that tenant.
- Dyrected dynamically merges the tenant schema with your base static schema.
- The merged schema is served to
/api/schemas, handles dynamic REST routing for/api/collections/:slug, and powers the AI Content Assistant.
Complete Configuration Example
Here is a complete, copy-pasteable dyrected.config.ts demonstrating how to load dynamic collections and globals based on the requesting siteId:
import {
defineConfig,
defineEmailField,
defineTextField,
defineRichTextField,
defineDateField,
CollectionConfig,
GlobalConfig,
} from "@dyrected/core";
// Mock database helper simulating external schema retrieval
async function fetchTenantCustomSchemasFromDb(siteId: string): Promise<{
collections: CollectionConfig[];
globals: GlobalConfig[];
}> {
// In a real application, query your database (e.g. Prisma, Drizzle, Mongoose)
if (siteId === "tenant-enterprise-acme") {
return {
collections: [
{
slug: "audit-logs",
fields: [
defineTextField({ name: "action", label: "Action Performed", required: true }),
defineTextField({ name: "performedBy", label: "User or Service", required: true }),
defineDateField({ name: "timestamp", label: "Event Timestamp", required: true }),
],
},
],
globals: [
{
slug: "custom-branding",
fields: [
defineTextField({ name: "primaryColor", label: "Brand Primary Color" }),
defineTextField({ name: "companyRegistrationNumber", label: "Company Registration Number" }),
],
},
],
};
}
// Return empty objects for standard tenants without custom schemas
return { collections: [], globals: [] };
}
export default defineConfig({
// Base collections shared by all tenants
collections: [
{
slug: "users",
auth: true,
fields: [
defineEmailField({ name: "email", label: "Email Address", required: true }),
defineTextField({ name: "name", label: "Full Name" }),
],
},
{
slug: "articles",
fields: [
defineTextField({ name: "title", label: "Article Title", required: true }),
defineRichTextField({ name: "content", label: "Article Content" }),
],
},
],
// Base globals shared by all tenants
globals: [
{
slug: "site-settings",
fields: [
defineTextField({ name: "siteName", label: "Site Name", required: true }),
],
},
],
// Dynamic schema hook invoked per tenant request
onSchemaFetch: async (siteId: string) => {
try {
const customSchema = await fetchTenantCustomSchemasFromDb(siteId);
return {
collections: customSchema.collections,
globals: customSchema.globals,
};
} catch (error) {
console.error(`Failed to load dynamic schema for tenant ${siteId}:`, error);
return {};
}
},
});Runtime Behavior & Endpoint Resolution
1. Dynamic Schema Discovery (/api/schemas)
When the Admin panel or SDK calls /api/schemas:
- For
tenant-enterprise-acme: The response includesusers,articles, and the dynamicaudit-logscollection. - For standard tenants: The response contains only
usersandarticles. The enterprise collection does not exist in their schema.
2. Dynamic Collection REST Routes
Dynamic collections behave exactly like static collections:
POST /api/collections/audit-logs(withX-Site-Id: tenant-enterprise-acme) creates a new audit log record.GET /api/collections/audit-logsretrieves the tenant's audit logs.- Sending a request for
audit-logswith a standard tenant'sX-Site-Idreturns404 Not Found.
3. Dynamic Global REST Routes
Dynamic globals can be retrieved and updated dynamically:
GET /api/globals/custom-brandingreturns the tenant's branding settings.PATCH /api/globals/custom-brandingupdates the fields.
Client Integration with Dynamic Schemas
Frontend applications simply provide the target X-Site-Id. The SDK automatically interacts with both base and dynamic collections transparently:
import { createClient } from "@dyrected/sdk";
const client = createClient({
apiUrl: "https://cms.example.com",
headers: {
"X-Site-Id": "tenant-enterprise-acme",
},
});
async function runEnterpriseWorkflow() {
// 1. Create a record in the dynamic tenant collection
const log = await client.collection("audit-logs").create({
action: "user.login",
performedBy: "[email protected]",
timestamp: new Date().toISOString(),
});
// 2. Fetch the dynamic tenant global
const branding = await client.global("custom-branding").get();
console.log("Created dynamic log:", log);
console.log("Tenant branding:", branding);
}
runEnterpriseWorkflow();AI Assistant & Dynamic Schemas
Dyrected's AI Content Assistant dynamically discovers tenant-specific collections:
- When an editor asks a question or requests an action, the AI controller invokes
onSchemaFetch(projectId)and merges the dynamic collections into the AI context. - The AI tools (
listCollections,getCollectionSchema,queryCollection,proposeCreateDocument) expose the dynamic collections to the model. - Editors can ask the AI assistant: "Summarize our recent audit-logs" or "Propose a new branding color", and the AI generates valid proposals matching the tenant's dynamic field definitions.
Pattern 2: Header-Scoped Sites (X-Site-Id)
Manage multiple client websites, regional portals, or sub-brands from a single Dyrected backend using the X-Site-Id header.
Deployment
Ship Dyrected to production with the right boundary, durable infrastructure, stable secrets, and a deployment flow that matches how the product actually boots.