Dyrected
Deployment & OperationsPlugins and ExtensionsMulti-Tenant Architecture

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.

When you operate a single Dyrected deployment that powers multiple distinct client websites, regional portals, or sub-brands, you often need separate collections or globals per site alongside shared content.

Dyrected enables this with the siteId property on collections and globals, paired with the X-Site-Id HTTP header and server-side anti-spoofing verification.


Mental Model

In header-scoped multi-tenancy:

  • Site-Specific Collections & Globals: Tagged with siteId: "site-a". Only accessible when requests provide X-Site-Id: site-a.
  • Shared Collections & Globals: Omit siteId. Accessible across all sites.
  • Client Requests: Frontend applications or API consumers send the X-Site-Id HTTP header to declare their target site.
  • Security & Anti-Spoofing: Authenticated users have an allowedSites list. If a user attempts to send an X-Site-Id they are not permitted to access, Dyrected immediately rejects the request with 403 Forbidden.

Complete Configuration Example

Here is a complete, copy-pasteable dyrected.config.ts defining shared content, site-specific collections, site-specific globals, and an authentication collection with anti-spoofing rules:

dyrected.config.ts
import {
  defineConfig,
  defineEmailField,
  defineJsonField,
  defineTextField,
  defineRichTextField,
} from "@dyrected/core";

export default defineConfig({
  collections: [
    // 1. Auth collection with allowedSites anti-spoofing validation
    {
      slug: "users",
      auth: true,
      fields: [
        defineEmailField({
          name: "email",
          label: "Email Address",
          required: true,
        }),
        defineJsonField({
          name: "allowedSites", // e.g. ["brand-us", "brand-eu"]
          label: "Allowed Sites",
          required: true,
          defaultValue: ["brand-us"],
        }),
      ],
    },

    // 2. Shared collection accessible by all sites
    {
      slug: "shared-faqs",
      fields: [
        defineTextField({
          name: "question",
          label: "FAQ Question",
          required: true,
        }),
        defineRichTextField({
          name: "answer",
          label: "FAQ Answer",
          required: true,
        }),
      ],
    },

    // 3. Articles collection exclusive to Brand US
    {
      slug: "articles",
      siteId: "brand-us",
      fields: [
        defineTextField({
          name: "title",
          label: "Article Title",
          required: true,
        }),
        defineTextField({
          name: "usHeadline",
          label: "US Regional Headline",
        }),
      ],
    },

    // 4. Articles collection exclusive to Brand EU
    {
      slug: "articles",
      siteId: "brand-eu",
      fields: [
        defineTextField({
          name: "title",
          label: "Article Title",
          required: true,
        }),
        defineTextField({
          name: "euVatDisclaimer",
          label: "EU VAT Disclaimer",
        }),
      ],
    },
  ],

  globals: [
    // Site-specific navigation global for Brand US
    {
      slug: "site-navigation",
      siteId: "brand-us",
      fields: [
        defineTextField({
          name: "logoUrl",
          label: "Brand Logo URL",
        }),
        defineTextField({
          name: "tollFreePhone",
          label: "US Toll-Free Support Phone",
        }),
      ],
    },

    // Site-specific navigation global for Brand EU
    {
      slug: "site-navigation",
      siteId: "brand-eu",
      fields: [
        defineTextField({
          name: "logoUrl",
          label: "Brand Logo URL",
        }),
        defineEmailField({
          name: "gdprContactEmail",
          label: "GDPR Privacy Contact Email",
        }),
      ],
    },
  ],
});

Anti-Spoofing & Security Verification

Dyrected enforces strict site authorization checks across all dynamic REST endpoints and /api/schemas:

1. Authorized Site Access

When an editor with allowedSites: ["brand-us"] sends X-Site-Id: brand-us:

  • /api/schemas returns only shared-faqs, articles (for brand-us), and the site-navigation global for brand-us.
  • /api/collections/articles queries Brand US articles.

2. Header Spoofing Rejection (403 Forbidden)

If the same user attempts to send X-Site-Id: brand-eu:

  • Dyrected's resolveAuthorizedSiteId middleware compares the header against the authenticated user's allowedSites.
  • The request is immediately rejected with 403 Forbidden:
{
  "error": true,
  "message": "Forbidden: User is not authorized to access site \"brand-eu\"."
}

3. Collection Isolation (404 Not Found)

If an unauthenticated request (or a consumer without access) tries to query /api/collections/articles with X-Site-Id: brand-eu, but is only permitted for brand-us:

  • Dyrected returns 404 Not Found rather than leaking information about collections belonging to other sites.

Client & SDK Integration

Using @dyrected/sdk

When configuring the @dyrected/sdk client in your frontend application (e.g. Next.js, Remix, Astro, or Vue), specify the headers option:

lib/cms.ts
import { createClient } from "@dyrected/sdk";

// Create client configured for Brand US
export const cms = createClient({
  apiUrl: process.env.NEXT_PUBLIC_CMS_URL || "https://cms.example.com",
  headers: {
    "X-Site-Id": process.env.NEXT_PUBLIC_SITE_ID || "brand-us",
  },
});

async function loadHomePageContent() {
  // Queries articles belonging to Brand US
  const articles = await cms.collection("articles").find({ limit: 5 });

  // Reads the site-navigation global for Brand US
  const navigation = await cms.global("site-navigation").get();

  console.log("Articles:", articles.docs);
  console.log("Navigation:", navigation);
}

loadHomePageContent();

Using Standard Fetch

fetch-example.ts
async function fetchBrandContent() {
  const response = await fetch("https://cms.example.com/api/collections/articles", {
    headers: {
      "Authorization": `Bearer ${process.env.CMS_TOKEN}`,
      "X-Site-Id": "brand-us",
      "Content-Type": "application/json",
    },
  });

  const data = await response.json();
  return data.docs;
}

AI Content Assistant & Multi-Site Scoping

When using the AI Content Assistant with header-scoped multi-tenancy:

  1. Context Initialization: The AI agent initializes using the resolved siteId for the current editor.
  2. Schema Visibility: The AI can only inspect collection schemas and globals assigned to the active siteId (plus shared ones).
  3. RAG Vector Scoping: Semantic search (searchContent) only searches text chunks tagged with the active projectId / siteId.
  4. Action Approval: Proposals (proposeCreateDocument, proposeUpdateGlobal) record the target projectId. At execution time, Dyrected verifies that the approving user has permission for that site before applying any database changes.

On this page

Dyrected| Cloud

Get your backend ready in minutes

Use a managed database, storage, APIs, and admin dashboard without setting up the infrastructure yourself.

Set Up My Backend