Website logo
CMS,  web development,  nextjs

đŸšȘ Mastering Roles and Multi-Tenancy in Payload CMS

Date Published

Access Control and multi-tenancy in payload CMS

🔐 The Problem with Old CMS Access Control (aka "The Click Hell Era")

Let’s be real for a second:
If you’ve ever tried to set up roles or multi-tenancy in legacy CMSs, you probably ended up with either:

  • A spaghetti bowl of “Editor”, “SuperEditor”, “MegaEditor”, and “WeForgotWhatThisDoesEditor” roles
  • Or worse — hacking around permission settings with checkboxes buried under four tabs and an “Advanced Settings” dropdown nobody dared to open
Legacy cms: this is fine meme


✹ Why Payload’s Access Control Is Different (And Better)

Payload CMS doesn't treat access control as an afterthought. It's function-based, not checkbox-based. This means instead of ticking random boxes in a UI, you just write functions — clean, readable functions that make sense to developers.

You’re not stuck with “read-only” vs “admin” — you can define anything from:

  • Role-based access (classic)
  • Field-level permissions
  • Per-document conditions
  • Organization-based multi-tenancy
  • Even: "Users on a Pro plan can create posts, but Free users can only like them" đŸ€Ż


đŸ§‘â€đŸ’» Defining User Roles Like a Pro

Here’s a minimal setup for defining roles inside your payload.config.ts. You can start with simple roles like admin, editor, and viewer, and build from there.

1// In your payload.config.ts
2export default buildConfig({
3 collections: [
4 {
5 slug: 'users',
6 auth: true,
7 fields: [
8 {
9 name: 'role',
10 type: 'select',
11 options: [
12 { label: 'Admin', value: 'admin' },
13 { label: 'Editor', value: 'editor' },
14 { label: 'Viewer', value: 'viewer' },
15 ],
16 defaultValue: 'viewer',
17 required: true,
18 },
19 // Other user fields
20 ],
21 },
22 // Other collections
23 ],
24});
25

Now that you’ve got roles, you’re in full control of who can do what across your CMS.

freedom meme


🧠 Collection-Level and Field-Level Access Control

Payload lets you be extremely specific about access. Here’s what that can look like for a blog post collection:

1// Collection with granular access control
2{
3 slug: 'posts',
4 access: {
5 create: ({ req }) => req.user?.role === 'admin' || req.user?.role === 'editor',
6 read: ({ req }) => true, // Anyone can read posts
7 update: ({ req, doc }) => {
8 // Admins can update any post
9 // Editors can only update their own posts
10 return req.user?.role === 'admin' ||
11 (req.user?.role === 'editor' && req.user.id === doc.author);
12 },
13 delete: ({ req }) => req.user?.role === 'admin', // Only admins can delete
14 },
15 fields: [
16 {
17 name: 'title',
18 type: 'text',
19 access: {
20 // All authenticated users can read the title
21 // Only admins and editors can update it
22 update: ({ req }) => req.user?.role === 'admin' || req.user?.role === 'editor',
23 },
24 },
25 // Other fields
26 ],
27}
28
You can control who can edit what, all the way down to individual fields — because hey, maybe editors can update the title, but not the secret SEO strategy field 😉
Access control meme


🏱 Multi-Tenancy: One Payload App, Many Organizations

Now this is where things get really exciting.

Multi-tenancy with Payload means you can have multiple organizations using the same CMS instance, each with isolated content and users.

Here’s how it works:

  1. Organizations have members
  2. Users belong to organizations
  3. Content (like projects or posts) belongs to an organization
  4. Access control functions check org membership before showing content

Example: Showing projects only from the user’s org:

1// Organizations collection
2{
3 slug: 'organizations',
4 fields: [
5 {
6 name: 'name',
7 type: 'text',
8 },
9 {
10 name: 'members',
11 type: 'relationship',
12 relationTo: 'users',
13 hasMany: true,
14 },
15 ],
16}
17
18// Users with organization relationship
19{
20 slug: 'users',
21 auth: true,
22 fields: [
23 {
24 name: 'organizations',
25 type: 'relationship',
26 relationTo: 'organizations',
27 hasMany: true,
28 },
29 // Other fields
30 ],
31}
32
33// Content with organization context
34{
35 slug: 'projects',
36 access: {
37 read: async ({ req }) => {
38 if (!req.user) return false;
39
40 // If admin, return all projects
41 if (req.user.role === 'admin') return true;
42
43 // Otherwise, only return projects from the user's organizations
44 return {
45 organization: {
46 in: req.user.organizations,
47 },
48 };
49 },
50 // Similar patterns for create, update, delete
51 },
52 fields: [
53 {
54 name: 'organization',
55 type: 'relationship',
56 relationTo: 'organizations',
57 required: true,
58 },
59 // Project fields
60 ],
61}
62

That’s it. You now have true multi-tenancy. No tenant IDs in the URL, no hard-to-maintain API filters. Just functions.

Payload multi tenancy meme


đŸ‘„ Organization Management — Like You Actually Meant It

Payload lets you build proper organization features, like:

  • Invite flows for members
  • Assigning different roles within the org
  • Approval workflows
  • Custom content access based on tiers/subscriptions

And because it’s all code, everything is version-controlled and testable. No more guessing what a role does in production đŸ«Ł


🚀 Conclusion: Function-Based Access Control FTW

After dealing with rigid, click-heavy CMSs, Payload feels like a breath of fresh air.

  • Roles? ✅ Easy and extendable
  • Multi-tenancy? ✅ Built right in
  • Access control down to the field level? ✅ You got it
  • Peace of mind as a developer? ✅ Priceless

Whether you're building an internal dashboard, a multi-client platform, or just a highly-permissioned content site — Payload makes you feel like you’re building software the way it should’ve always worked.

Like what you read? Here are some related posts: