---
title: "Payload Redirects Plugin: Fix 301s in Multi-Tenant Sites"
slug: "payload-redirects-plugin-multi-tenant-multi-locale-301s"
published: "2026-08-18"
updated: "2026-08-19"
validated: "2026-08-19"
categories:
  - "Payload"
tags:
  - "Payload redirects plugin"
  - "Payload CMS redirects"
  - "multi-tenant redirects"
  - "multi-locale redirects"
  - "compound index site locale from"
  - "Next.js middleware redirects"
  - "redirects plugin configuration"
  - "301 redirects migration"
  - "relationship field redirect reference"
  - "plugin ordering multi-tenant"
llm-intent: "reference"
audience-level: "intermediate"
framework-versions:
  - "payload cms"
  - "@payloadcms/plugin-redirects"
  - "next.js"
  - "typescript"
  - "pnpm"
status: "stable"
llm-purpose: "Payload redirects plugin guide: migrate 301s safely in multi-tenant, multi-locale sites by replacing unique 'from' with a site+locale+from compound index…"
llm-prereqs:
  - "Access to Payload CMS"
  - "Access to @payloadcms/plugin-redirects"
  - "Access to Next.js"
  - "Access to TypeScript"
  - "Access to pnpm"
llm-outputs:
  - "Completed outcome: Payload redirects plugin guide: migrate 301s safely in multi-tenant, multi-locale sites by replacing unique 'from' with a site+locale+from compound index…"
---

**Summary Triples**
- (Payload Redirects Plugin: Fix 301s in Multi-Tenant Sites, focuses-on, Payload redirects plugin guide: migrate 301s safely in multi-tenant, multi-locale sites by replacing unique 'from' with a site+locale+from compound index…)
- (Payload Redirects Plugin: Fix 301s in Multi-Tenant Sites, category, general)

### {GOAL}
Payload redirects plugin guide: migrate 301s safely in multi-tenant, multi-locale sites by replacing unique 'from' with a site+locale+from compound index…

### {PREREQS}
- Access to Payload CMS
- Access to @payloadcms/plugin-redirects
- Access to Next.js
- Access to TypeScript
- Access to pnpm

### {STEPS}
1. Install the redirects plugin
2. Limit redirect target collections
3. Override default fields
4. Add a compound unique index
5. Set redirect types and defaults
6. Register plugin ordering
7. Wire multi-tenant scoping
8. Implement runtime redirect execution

<!-- llm:goal="Payload redirects plugin guide: migrate 301s safely in multi-tenant, multi-locale sites by replacing unique 'from' with a site+locale+from compound index…" -->
<!-- llm:prereq="Access to Payload CMS" -->
<!-- llm:prereq="Access to @payloadcms/plugin-redirects" -->
<!-- llm:prereq="Access to Next.js" -->
<!-- llm:prereq="Access to TypeScript" -->
<!-- llm:prereq="Access to pnpm" -->
<!-- llm:output="Completed outcome: Payload redirects plugin guide: migrate 301s safely in multi-tenant, multi-locale sites by replacing unique 'from' with a site+locale+from compound index…" -->

# Payload Redirects Plugin: Fix 301s in Multi-Tenant Sites
> Payload redirects plugin guide: migrate 301s safely in multi-tenant, multi-locale sites by replacing unique 'from' with a site+locale+from compound index…
Matija Žiberna · 2026-08-18

The `@payloadcms/plugin-redirects` package injects a full redirects collection into Payload, letting editors manage HTTP redirect rules from the Admin Panel instead of hardcoding them into `next.config.js` or an Nginx rewrite file. I added it during a WordPress-to-Payload migration where the legacy site carried 31 explicit 301 rules that had to survive the move without breaking search rankings. Tested with Payload 3.88.0 and the plugin at the matching version, in a multi-tenant project with bilingual content across `en-CA` and `fr-CA`.

The site being migrated had accumulated years of vanity URLs and merged taxonomy paths, tracked in WordPress through a legacy SEO plugin's redirect table. Moving that table's logic into Payload meant more than copying rows across. The project's existing redirects collection had been hand-built with plain `source` and `destination` text fields, which meant a destination page's slug could change and silently break every redirect pointing at it, with no relationship to catch the drift. The official plugin replaces that with a real relationship field, so a redirect can point at a living document instead of a hardcoded string. It also defaults to a single unique constraint on the source path, which conflicts directly with a multi-tenant, multi-locale project where the same relative path is legitimately reused across sites and languages. This guide covers the plugin's structure, the exact configuration used to fix that constraint, and how it fits alongside the multi-tenant plugin in the same config.

If you're setting up the multi-tenant foundation first, my <a href="https://www.buildwithmatija.com/blog/production-ready-multi-tenant-nextjs-payload">production-ready multi-tenant setup guide</a> covers the tenant isolation this plugin needs to sit on top of.

## What the Redirects Plugin Actually Adds

`@payloadcms/plugin-redirects` is a first-party Payload package published in the same monorepo and version line as core, alongside `plugin-nested-docs`, `plugin-seo`, `plugin-form-builder`, and `plugin-multi-tenant`. Installing `3.88.0` alongside Payload `3.88.0` keeps it on the compatibility line it was built against.

Registering the plugin appends an entire new collection to the config, defaulting to the slug `redirects`. That collection ships with:

- A `from` field (text, indexed, unique by default) holding the incoming path to match.
- A `to` group field with a radio toggle between `reference` and `custom`. Choosing `reference` exposes a relationship picker scoped to whichever collections you list; choosing `custom` exposes a plain URL text field.
- A `type` select field for the HTTP status code, populated from `redirectTypes` if you pass that option: `301`, `302`, `303`, `307`, or `308`.
- Built-in admin translations for English, French, and Spanish.

The plugin does not execute redirects at the HTTP layer on its own. It's a data store. Actually running redirects at request time is still the frontend's job, typically through Next.js middleware or an edge proxy that queries this collection.

## Installing and Configuring the Plugin

```bash
pnpm add @payloadcms/plugin-redirects
```

```typescript
// File: src/payload/config/plugins/redirects.ts
export const redirects: Plugin = redirectsPlugin({
  collections: [...REDIRECT_TARGET_COLLECTIONS],
  redirectTypes: ["301", "302", "307", "308"],
  redirectTypeFieldOverride: {
    defaultValue: "308",
  },
  overrides: {
    admin: {
      group: "Site configuration",
      defaultColumns: ["from", "to.type", "locale", "site"],
    },
    access: {
      create: createContentCreateAccess("redirects"),
      delete: createContentDeleteAccess("redirects"),
      read: createContentReadAccess("redirects"),
      update: createContentUpdateAccess("redirects"),
    },
    indexes: [{ fields: ["site", "locale", "from"], unique: true }],
    fields: ({ defaultFields }) => {
      const fields = defaultFields.map((field) => {
        if ("name" in field && field.name === "from") {
          return {
            ...field,
            unique: false,
          };
        }
        return field;
      });

      return [
        {
          name: "locale",
          type: "select",
          required: true,
          options: [...SUPPORTED_LOCALES],
        },
        ...fields,
      ];
    },
  },
});
```

`collections` limits which document types are selectable as redirect targets, in this project spanning ten collections including `pages`, `blogs`, `recipes`, and `products`. `redirectTypes` enables the status code dropdown, defaulted here to `308` since that preserves the original request method during migration-driven redirects rather than forcing every redirect to a GET. `overrides.access` wires the project's own RBAC functions into the collection instead of leaving it on Payload's default access rules. `overrides.indexes` and the `fields` override are what fix the uniqueness problem covered next.

Register the plugin before the multi-tenant plugin in the plugin array:

```typescript
// File: src/payload/config/plugins/index.ts
export const plugins: Plugin[] = [
  ecommerce,
  nestedDocs,
  seo,
  redirects,
  formBuilder,
  multiTenant,
  importExport,
  mcp,
];
```

Order matters because `redirects` generates a brand-new collection at config time, and `multiTenant` needs to discover that collection to inject the `site` relationship into it. Registering `multiTenant` first means it runs before the `redirects` collection exists.

## Fixing the Uniqueness Constraint for Multi-Tenant, Multi-Locale Sites

The plugin's default `from` field carries `unique: true` at the field level. That's a reasonable default for a single-site, single-locale project, where the same source path should only ever map to one destination. It breaks immediately on a multi-tenant, bilingual project, where `/about` is a legitimate redirect source on Site A, Site B, and again separately in the `fr-CA` locale. A single-field unique constraint has no way to know those are three different contexts.

The fix does two things: it strips the field-level uniqueness from `from`, and it replaces it with a compound index across `site`, `locale`, and `from`.

```typescript
indexes: [{ fields: ["site", "locale", "from"], unique: true }],
fields: ({ defaultFields }) => {
  const fields = defaultFields.map((field) => {
    if ("name" in field && field.name === "from") {
      return {
        ...field,
        unique: false,
      };
    }
    return field;
  });

  return [
    {
      name: "locale",
      type: "select",
      required: true,
      options: [...SUPPORTED_LOCALES],
    },
    ...fields,
  ];
},
```

`defaultFields` gives you the plugin's own generated field array to map over, rather than replacing the whole set from scratch. Finding the `from` field by name and spreading it with `unique: false` removes the conflicting constraint. The compound index then enforces the uniqueness the project actually needs: one redirect rule per source path, per site, per locale, rather than one redirect rule per source path globally. The `locale` field itself isn't part of the plugin's default schema; it gets pushed into the returned array explicitly, since a bilingual redirects table needs to know which language a given rule applies to.

## Wiring Redirects Into the Multi-Tenant Plugin

With the collection generated and the constraint fixed, the multi-tenant plugin needs to know it should scope this collection to tenants:

```typescript
// File: src/payload/config/plugins/multi-tenant.ts
collections: {
  [Blogs.slug]: {},
  [Pages.slug]: {},
  redirects: {},
  [Stories.slug]: {},
  // ...remaining collections
},
```

Because `redirects` is a plugin-generated collection with no static class or exported slug constant to reference, it gets registered by its literal string slug rather than an imported reference. Adding it to this map is what causes the multi-tenant plugin to auto-inject the `site` relationship field, which is the field the compound index above depends on.

## Comparing Redirect Approaches

| Approach | When to use | Trade-off |
|---|---|---|
| `@payloadcms/plugin-redirects` | Editorially managed redirects that need to stay in sync with living CMS content | Requires a frontend layer (middleware or edge proxy) to actually query and execute the redirect; adds nothing at the HTTP level by itself |
| Hand-rolled redirects collection with plain text fields | Very small, static redirect sets that rarely change | No relational binding to real documents, so a destination slug change breaks the redirect silently, and any relational or status-code logic has to be built and maintained by hand |
| Hardcoded `next.config.js` redirects array | A handful of permanent, code-level rules | Every new rule needs a code change and a deployment, which puts redirect management outside marketing or content editors' reach |
| Edge or reverse proxy rewrites (Nginx, Cloudflare) | High-volume, performance-critical rewrite rules | Fully disconnected from CMS content, unmanageable by non-engineering staff, and invisible to editors who need to add a rule |

The migration case for this project ruled out the first two alternatives directly: the legacy hand-rolled collection had already shown its weakness by breaking silently on slug changes, and 31 rules were too many to justify hardcoding into the Next.js config given ongoing editorial ownership after launch.

## FAQ

**Does the redirects plugin actually perform the HTTP redirect?**
No. The plugin only provides the data collection and the admin UI for managing rules. Executing a redirect at request time is the frontend's responsibility, typically through Next.js middleware or an edge proxy that queries this collection and issues the response.

**Why does the default `from` field break on a multi-tenant project?**
The plugin sets `unique: true` directly on the `from` field, which enforces global uniqueness across the entire collection. A multi-tenant or multi-locale project legitimately reuses the same source path across different sites or languages, which the field-level constraint has no way to distinguish. Replacing it with a compound index on `site`, `locale`, and `from` fixes this.

**Can a redirect point to a document instead of a hardcoded URL?**
Yes. The `to` group field exposes a radio choice between `reference`, which opens a relationship picker scoped to whichever collections you list in the plugin config, and `custom`, which takes a plain URL string. Using `reference` means the redirect stays valid even if the target document's slug changes later.

**Which HTTP status codes does the plugin support?**
`301`, `302`, `303`, `307`, and `308`, exposed as a select field when you pass `redirectTypes` into the plugin config. Setting a `defaultValue` on that field controls what new redirect rules default to.

**Where does the plugin need to sit relative to the multi-tenant plugin?**
Before it. `plugin-redirects` generates the collection at config-evaluation time, and the multi-tenant plugin needs that collection to already exist so it can inject the `site` field into it. Registering multi-tenant first means the redirects collection isn't there yet to scope.

## Conclusion

Migrating a legacy site's redirect rules into Payload is straightforward once the collection exists, but the plugin's defaults assume a single-site, single-locale project. A multi-tenant, bilingual site needs the uniqueness constraint rebuilt as a compound index rather than a single field, and the plugin's `defaultFields` override makes that a small, contained change rather than a fork. Combined with correct plugin ordering against the multi-tenant plugin, the result is an editorially managed redirects table that preserved all 31 legacy rules from the WordPress migration without a single hardcoded rewrite rule in the codebase.

Let me know in the comments if you have questions, and subscribe for more practical development guides.

Thanks,
Matija

## LLM Response Snippet
```json
{
  "goal": "Payload redirects plugin guide: migrate 301s safely in multi-tenant, multi-locale sites by replacing unique 'from' with a site+locale+from compound index…",
  "responses": [
    {
      "question": "How do I change the redirects plugin to allow identical paths across tenants and locales?",
      "answer": "In the redirects collection config, remove the unique constraint from the single 'from' field and add a collection-level compound index with { site: 1, locale: 1, from: 1 } and unique: true. Ensure the collection includes site (relationship) and locale fields. Apply the change and re-run migrations so Mongo/DB creates the compound index. Also confirm the multi-tenant plugin is initialized before the redirects plugin so tenant IDs are available."
    },
    {
      "question": "How can I make redirect destinations resilient to slug changes?",
      "answer": "Use a relationship field on the redirects collection that points to page documents (e.g., relationTo: ['pages']). When you resolve a redirect, build the destination URL from the referenced page's current slug and locale. For imports of legacy redirects, if a destination document exists, store a relationship; otherwise preserve a fallback 'toString' field to handle manual slugs."
    },
    {
      "question": "What steps do I need to migrate WordPress redirect rows into Payload safely?",
      "answer": "1) Export the WordPress redirect table into CSV/JSON with columns: from, to, status. 2) For each row, try to match the 'to' URL to an existing Payload document (page) and store its id in the redirects.to relationship; if no match, store 'to' as a fallback string. 3) Populate 'site' and 'locale' for each redirect based on host and language mapping. 4) Insert into the redirects collection using Payload server-side script or API, respecting the compound index uniqueness (site+locale+from). 5) Run tests in staging: hit legacy URLs and verify 301 responses and destination slugs."
    },
    {
      "question": "How do I order the multi-tenant plugin and redirects plugin in Payload config?",
      "answer": "Register the multi-tenant plugin first in your Payload init call so it scopes request/collections early. Then register the redirects plugin after. Example: plugins: [multiTenant(pluginConfig), redirects(pluginRedirectsConfig)]. The tenant plugin must set up request-level tenant data before collection index creation or middleware that depends on tenant scoping."
    },
    {
      "question": "How should I test redirects locally and in production to avoid SEO issues?",
      "answer": "1) Use staging with the same tenant hostnames and locales. 2) Run an HTTP client (curl) and a browser check to confirm 301 status and Location header. 3) Verify canonical destination pages render correctly and the redirect path resolves to the correct tenant+locale. 4) Run a link-crawl on migrated paths to detect broken targets. 5) Use 302 temporarily during migration if you need safe testing, then flip to 301 once verified."
    }
  ]
}
```