Payload CMS is celebrated for its beautiful, intuitive admin interface. It’s clean, fast, and makes content management a breeze. But the true superpower of Payload—the feature that sets it apart in the crowded headless CMS market—lies beneath the surface: its code-first configuration.
While many CMS platforms force you into a click-and-configure workflow within a GUI, Payload empowers you to define your entire data schema, access control, and business logic directly in TypeScript. This approach transforms your CMS from a rigid box into a version-controlled, fully extensible part of your application's codebase.
At studio.do, we let you harness this power instantly. We handle the deployment, hosting, and custom branding, so you can focus on what matters: crafting the perfect content engine with code.
Let's dive into why this code-first philosophy is a game-changer and how you can use it to build robust, scalable backends.
Before we get into the code, let's understand the "why." Defining your CMS in code offers several massive advantages:
Every Payload project is anchored by a central configuration file, typically payload.config.ts. This is where the magic happens. Here, you define your collections, global settings, user types, and more.
Let's start by building a simple Posts collection.
A Collection is a schema for a group of documents, like blog posts, products, or pages. Here’s a basic example:
// payload.config.ts
import { buildConfig } from 'payload/config';
import { Posts } from './collections/Posts'; // We'll define this next
export default buildConfig({
// ... other config
collections: [
Posts,
// Other collections like Users, Media, etc.
],
});
// collections/Posts.ts
import { CollectionConfig } from 'payload/types';
export const Posts: CollectionConfig = {
slug: 'posts',
admin: {
useAsTitle: 'title',
description: 'A collection for blog posts.',
},
fields: [
{
name: 'title',
type: 'text',
required: true,
},
{
name: 'content',
type: 'richText',
required: true,
},
{
name: 'status',
type: 'select',
options: [
{ label: 'Draft', value: 'draft' },
{ label: 'Published', value: 'published' },
],
defaultValue: 'draft',
admin: {
position: 'sidebar',
}
}
],
};
In this example, we've defined a Posts collection with a title, content, and status. Notice how easy it is to set fields as required, provide options for a select dropdown, and even configure the layout of the admin panel (position: 'sidebar').
This is where code-first truly shines. Instead of simple role-based checkboxes, Payload uses function-based access control. You write functions that return true (access granted) or a query constraint to filter documents.
Let's secure our Posts collection:
// collections/Posts.ts
import { CollectionConfig } from 'payload/types';
import { User } from 'payload/generated-types';
const canModifyPost = ({ req: { user } }: { req: { user: User } }) => {
// Admins can do anything
if (user?.roles?.includes('admin')) {
return true;
}
// Authors can modify their own posts
// This returns a query constraint
return {
author: {
equals: user.id,
},
};
};
export const Posts: CollectionConfig = {
slug: 'posts',
// ... other config from above
access: {
// Anyone can read published posts
read: ({ req: { user } }) => {
// Logged-in users can see drafts, everyone else sees published
if (user) return true;
return { status: { equals: 'published' } };
},
// Only logged-in users can create posts
create: ({ req: { user } }) => !!user,
// Admins can update any post, authors can only update their own
update: canModifyPost,
// Admins can delete any post, authors can only delete their own
delete: canModifyPost,
},
fields: [
// ... fields from above
{
name: 'author',
type: 'relationship',
relationTo: 'users',
required: true,
// Hide from the API, set automatically
access: {
create: () => false,
update: () => false,
},
admin: {
readOnly: true,
position: 'sidebar',
}
},
],
// ...
};
This is incredibly powerful. We've just implemented fine-grained, row-level security:
This level of granular control is simple to write and reason about in code but often impossible to achieve in a GUI-based system.
You've seen the power and flexibility that Payload's code-first configuration provides. Now, imagine offering this to every one of your clients, fully branded with their logo and colors, without ever touching a server, configuring a database, or setting up a deployment pipeline.
That's the studio.do promise.
You write the payload.config.ts that defines the perfect, bespoke CMS for your client's needs. Then, you make a single API call to studio.do.
import { studio } from '@do-sdk/js';
// Deploy a new white-labeled Payload CMS instance
const newSite = await studio.deploy({
projectName: 'acme-corp-blog',
brand: {
name: 'ACME Corp',
logoUrl: 'https://cdn.acme.com/logo.svg',
colors: {
primary: '#1E40AF',
secondary: '#F3F4F6'
}
},
database: {
provider: 'mongodb',
connectionString: process.env.MONGO_DB_URL
}
});
console.log('CMS deployed:', newSite.adminUrl);
We instantly provision, deploy, and host your Payload instance as a service, complete with your client's branding. You focus on creating value with code; we handle the rest.
Payload's code-first configuration is more than just a feature—it's a paradigm shift that gives developers the control, flexibility, and power they need to build truly modern content solutions. By embracing code as the source of truth, you can create systems that are version-controlled, highly customizable, and infinitely scalable.
Ready to leverage the full power of Payload without the overhead of infrastructure management? Explore studio.do and deploy your first custom-branded CMS in minutes.