Payload CMS Development

Payload CMS development, where the content model is the deliverable.

You’ve already chosen Payload. The risk left isn’t whether it can be built. Payload will let you model almost anything, and that’s the failure mode: a schema nobody argued about turns into editorial and technical debt inside eighteen months. This page is about the modelling, access-control and hosting decisions that decide which of those you end up with.

If you’re still comparing content platforms, or scoping the application around the CMS, start at our Next.js web development service instead. Everything below assumes Payload is settled.

What a Payload build leaves behind.

Six artifacts, all of them files in your repository. A Payload project gets judged on what the next engineer finds when they open the config, not on how the admin panel looked at launch.

A Payload 3 install inside your Next.js app

The (payload) route group, withPayload wired into next.config, and an admin panel rendering as React Server Components. One repo, one build, one deploy.

A content model with the arguments written down

Every collection, global and block type, and the reason it exists rather than the alternative. The decision log lives beside payload.config.ts.

Access control that does the authorisation

Read functions returning Where constraints so list views stay one indexed query, field access kept synchronous, and a test per role that fails the build on a regression.

A localisation call made before the first collection

Locale list, per-field fallback, and an explicit decision on localised slugs. Choose it up front and it costs an afternoon. Retrofit it and you’re migrating through every nested block.

A media pipeline that survives a restart

S3, R2 or Vercel Blob through the cloud storage adapter, with imageSizes chosen against layouts that exist, and no local staticDir a container can forget.

Migrations, drafts and preview wired for CI

Generated Postgres migrations reviewed in a pull request, draft mode on a real preview route, revalidation from an afterChange hook, and version retention capped.

Engineering position

Six decisions a Payload schema locks in.

Where collections end and globals begin

A global is a single document: no list view, no per-document access control, and on the Postgres adapter its own table. Right for the things there will only ever be one of, like navigation or site-wide SEO defaults. Teams reach past that line because the sidebar looks tidier, then find the Homepage global can’t be duplicated for a campaign variant, can’t carry the draft workflow the rest of the site uses, and can’t be versioned per market once localisation lands. Converting it afterwards is a data migration.

The opposite mistake is a collection holding one row forever, which makes editors hunt a list view for the only document in it and hands them a delete button for the site configuration. The rule we use: if the business could plausibly want two, it’s a collection. If two would be a bug, it’s a global.

Blocks are not free, and block soup makes editors slower

The blocks field is what sells Payload to a marketing team and what wrecks the schema eighteen months later. Every block type is another entry in the editor’s picker and, on Postgres, another table joined on read. Forty block types is not a page builder, it’s an undocumented design system with three near-identical hero variants, because adding the second was faster than reconciling it with the first.

The test is whether the editor is choosing content or choosing layout. Content belongs in blocks. Layout belongs in the front end, as a variant select on one block rather than three blocks. Where a page is genuinely fixed, flat named fields win outright: the editor gets a form instead of a canvas, and the front end gets a type rather than a union it has to defend against.

Localisation is a storage decision, not a checkbox

Adding localization to the config and localized: true to a field looks like flipping a switch. What it does is change where the data lives. On Postgres, localised fields move out of the parent table into locale tables, and localised arrays and blocks cascade that structure down every nested level. Retrofit it after a year of content and you’re running a migration that lifts existing column values into the default locale for every affected field, including the ones buried inside blocks.

Fallback is the second call, and it’s not a default to accept unread. It renders the English title wherever the German one is missing, which is right for a catalogue and wrong for a legal page. Then decide whether slugs localise at all. Localised slugs buy you per-locale URLs and a routing layer to match; unlocalised slugs buy you one URL per document.

Access control is the authorisation layer, and field-level access is where list views die

A Payload access function can return a boolean or a Where query, and that difference matters more than anything else on this page. A read function returning a constraint, tenant equals the current user’s tenant, gets pushed down into the database query, so the list view stays a single indexed lookup whether the collection holds two hundred documents or two hundred thousand. One that fetches something and returns true or false is evaluated per document, and a page of twenty-five rows becomes twenty-five extra round trips.

Field-level access is worse: it can only return a boolean, and it runs per field, per document. Ten restricted fields across twenty-five rows is 250 evaluations on every page of the list view, and any database call inside them is an N+1 you built yourself. Keep field access synchronous and derived from req.user alone.

The related trap is the Local API. payload.find runs with overrideAccess true by default, which is correct for seed scripts and catastrophic in a server component that forgot to pass the current user and overrideAccess false. That’s documented behaviour rather than a bug, and it’s the most common way a Payload app serves one tenant another tenant’s rows.

Payload 3 runs inside your Next.js app. Know what that buys and what it costs

This is the version-defining change and the real reason to pick Payload over a hosted CMS. Payload 3 installs into a route group inside your App Router project rather than alongside it. The admin panel is React Server Components rather than a separate single-page app pointed at an API. getPayload gives you the Local API in-process, so a server component queries the database directly: no HTTP hop, no API token, no network latency between the CMS and the page rendering its content.

The tradeoff is that sentence read backwards. CMS and front end share a deployment, a build and a cold start. For a marketing site that’s close to ideal. It turns into a liability the moment the two want different scaling profiles: an editorial team hammering a write-heavy admin while the public site is almost entirely cached, or Sharp resizing a 40-megapixel upload on the same function meant to render pages. That’s when we split the admin onto its own instance against the same database.

Self-hosting, database and media are three separate bills

Payload is MIT-licensed, so the cost moved rather than disappeared. The Postgres adapter builds a real relational schema through Drizzle, so schema changes need real migrations: generated with the CLI, committed, reviewed in a pull request, run in CI before the deploy that depends on them. Mongo removes that step and hands you the flexibility bill later, when four years of documents have three shapes and nothing ever stopped them.

Two costs accumulate quietly. Drafts with autosave write a version row per interval per document, and versions tables grow without limit unless somebody sets a max. Uploads run Sharp in the process that renders your pages, so every imageSizes entry costs CPU and memory at upload time, and a large original on a serverless function doesn’t throw a helpful error. It hits the timeout.

When not to use it

Five projects where we’d talk you out of Payload.

We like Payload, which is exactly why we draw the boundary. If you’re weighing it against staying where you are, our WordPress to Next.js migration page argues both directions on the same question.

Nobody on your team writes TypeScript

Payload’s schema is code: a new field is a commit, a migration and a deploy. Directus and Strapi let a non-engineer add one from a UI. If nobody on staff can review a payload.config.ts pull request, Payload leaves you permanently dependent on whoever built it.

Your front end is not Next.js

The whole argument for Payload 3 is that it lives inside the App Router. Serve Astro, SvelteKit, Rails or a native mobile client and you’re back to an HTTP API and a token, at which point a CDN-backed hosted CMS costs less to run and less to think about.

Two editors need the same document at once

Payload has drafts, version history and document locking. It does not have Google-Docs-style multiplayer editing on a single record. If simultaneous editing happens daily rather than at the edges, Sanity’s real-time model is the better fit, and we’ll say so before the contract.

You want a plugin ecosystem more than a content model

A team that wants forms, SEO, redirects and forty more capabilities installed without an engineer is describing WordPress, and that’s a legitimate thing to want. Payload’s official plugins are good, but installing one is still a dependency bump and a deploy.

You’re really asking for a checkout

Payload models a product catalogue well. Payments, tax, fraud and checkout are a different problem, with regulated answers and incumbents who already solved them. Keep Shopify or Stripe for the transaction. A CMS that grows a payments module is one you’ll be patching at midnight.

How it runs

What we commit to, and what moves a Payload quote.

Your repo, your cloud, from commit one

The config, the migrations and the schema decision log live in your GitHub organisation and deploy to your hosting account. Nothing important sits somewhere you would have to ask us for.

A staging URL by the end of week one

The admin panel is deployed and clickable before the model is finished, because arguing about a collection goes faster when editors can open it. A build and a short note every Friday after that.

Decisions in writing

Collection against global, block against field, localised against not, and every access rule with its reasoning. Two years from now, why the schema looks like this sits in the repository rather than in somebody’s memory.

Fixed scope, quoted before kickoff

Change requests are priced and confirmed in writing before anyone starts them. Every content model grows a tail of small discoveries. We quote that tail rather than absorbing it quietly and billing for it later.

We are not a Payload partner agency. The official programme is small and awarded on evidence of shipped Payload work, so we’d rather say that plainly than let a badge do work the code should be doing. Judge us on the reasoning above and on how we document the builds we publish.

Pricing follows the published rate card rather than a per-field calculator: marketing sites start at $7,500, platforms at $28,000, and the minimum for a build is $7,500. Those figures are estimates in US dollars, not quotations; the quote that follows scoping is fixed in writing before kickoff. The biggest variable is how much of the content model is genuinely distinct. Localisation moves it next, then the number of roles the access layer expresses and whether tenancy is one of them. Remodelling existing content on the way in adds to it, and so does deploying the admin separately from the front end. The same argument we make about what an MVP actually costs applies here: the estimate follows the decisions, not the page count.

If the site is already live and the complaint is speed rather than structure, read why your Next.js site is slow before commissioning anything. Otherwise send us the content you have and the shape you want, and get a fixed quote against a written schema.

FAQ

The questions that decide it

The build is not the hard part. Payload will let you model almost anything, and that’s the risk: schema decisions get made in week one and paid for over three years. What you’re hiring for is somebody to argue collections against globals, cap the block inventory, settle localisation before the first field, and write access control that returns query constraints rather than booleans.

Postgres if the content has real relationships and you want the database to enforce them. It builds a relational schema through Drizzle, so schema changes need migrations committed to the repo and run in CI. Mongo if the shapes are irregular and you’d rather not run migrations at all, accepting that four years of documents may end up in three shapes.

That’s the default and the main reason to choose it. Payload 3 installs into a route group in your App Router project, so the admin panel and the public site share a repo, a build and a deployment, and server components query the database in-process through the Local API. You can split the admin onto its own instance when scaling profiles diverge.

Yes, and the export is the easy half. Somebody usually has to remodel the content on the way in, because a page builder’s single rich-text blob per page isn’t a content model, and porting it faithfully just moves the problem. Migration is the only moment anyone will pay to fix the structure, so we scope it as remodelling with a redirect map attached.

We quote against the published rate card: marketing sites start at $7,500, platforms at $28,000, and the minimum for a build is $7,500. Those figures are estimates in US dollars, not quotations; the quote that follows scoping is fixed in writing before kickoff. What moves the number is the size of the content model, whether localisation is in scope, how many roles the access layer expresses, whether existing content gets remodelled, and whether the admin deploys separately from the front end.

Usually access control. A read function returning a Where constraint gets pushed into the database query and stays one indexed lookup. One returning a boolean is evaluated per document, and field-level access returns booleans only and runs per field per document, so ten restricted fields across twenty-five rows is 250 evaluations per page. Any database call inside those is an N+1 you built yourself.

Argue the collections before anyone writes a field.

Send us the content you have and the editing experience you want. You’ll get a proposed content model back, with the collection, block and localisation calls written down. Fixed scope, fixed price, start date within two working days. Email edgebrainstudios@gmail.com or call the number below.