SUITS & SANDALS

Expertise

  • Website Strategy, UX & Development
  • Sales, Marketing & Investor Communications
  • Clarifying Complex Stories
  • Embedded Creative & Digital Services
  • Product UX/UI & Design Systems

Who We Help

  • Technical B2B & Expert-Led Companies
  • Healthtech & Life Sciences
  • Growth & Repositioning Brands
  • Platforms & Digital Products
Ask
Answers about our work, services, and insights
  • HR TechVault Workforce Screening
  • InsurtechArturo
  • FintechInterchecks
  • Works
  • Insights
  • Immersive Lab
  • About Us
  • Website Strategy, UX & Development
  • Sales, Marketing & Investor Communications
  • Clarifying Complex Stories
  • Embedded Creative & Digital Services
  • Product UX/UI & Design Systems
  • Technical B2B & Expert-Led Companies
  • Healthtech & Life Sciences
  • Growth & Repositioning Brands
  • Platforms & Digital Products
Get in touch

Brooklyn, NY / Philadelphia, PA

Get in touch

Lab

The new Suits & Sandals CMS

We moved our site off Webflow and onto Payload CMS, installed right inside our Next.js app. The bigger change was the Content Hub: the facts, stories, quotes and files live as records, and pages point at them instead of keeping their own copies.

Editor
Miles Roxas
Published
21 September 2026
Built with
Payload CMS, Next.js, TypeScript+5
Read
22 MIN

Write it once, use it everywhere

Introduction

Webflow gave us a tidy portfolio CMS, but every case study wore the same template and the page was the only copy of the story. We rebuilt on Payload CMS, where the whole content model is TypeScript in our own repository, and split the site in two. A Content Hub holds the facts, the stories, the short summaries, the quotes and the files. The website presents them. Pages, the assistant on our site and the agents we work with all read the same records, so a fact fixed once is fixed everywhere it appears.

Legacy

A portfolio CMS on Webflow

Our old site ran on Webflow, with the kind of CMS any small portfolio site has: collections for services, portfolios, categories, blog posts and episodes.

Portfolios carried the most fields: client and project overviews, a client review, a hero video and image, portfolio samples, related clients and a few switches for the homepage. Every one of them filled a slot in the same case study template.

The old Webflow CMS sidebar listing five collections, Services, Portfolios, Categories, Blogs and WA Episodes, with their item counts.

Our old Webflow CMS: five collections.

The custom fields on the Webflow Portfolios collection: services, a marquee switch, client and project overviews, a client review, the project website, hero video and image, samples, related clients, a home excerpt and homepage switches.

The Webflow Portfolios collection: one field for every slot in the case study template.

The problem

One template for every case study

Webflow held every case study to one template. The deeper problem was that the page was the only copy of the story.

Webflow has no matrix fields or dynamic content areas. Each collection is limited to one structured template, so it is not possible to compose case study pages with a unique layout per entry.

We had already solved that limit in another project: Beacon, a hybrid Webflow app that unlocks composable content areas. Even with the core limitation removed, the new site had requirements Webflow would not meet, around true headless content management, digital asset management, developer experience, scalability and price.

Beacon, a hybrid Webflow app, configuring a CMS slot: a panel lists a component's props, such as heading text, rich text, button text and link, with checkboxes for the ones that become editable.

Beacon, our hybrid Webflow app, choosing which props of a component become editable.

Payload CMS

Enter Payload

Rebuild on Payload CMS and split the site into two layers: a Content Hub that holds the work, and a website that presents it.

For most clients looking beyond Webflow we would usually recommend Sanity. For our own site we chose Payload CMS. We have used it since version 2 and supported the project for a long time, and it is extremely developer friendly.

The Payload CMS homepage, headlined The backend to build the modern web, beside a code editor open on payload.config.ts and the Payload admin editing a post.

Payload CMS.

Collections

A collection is just a TypeScript object

Collections are the content. Blocks are the layouts. One config file ties them together. It is all code: it lives in git, it goes through code review, and we own every piece of it.

What got us hooked on Payload: there is no schema builder, no clicking around adding fields one at a time. A collection is a TypeScript object. Give it a slug and some fields, and it is done.

typescript
1// src/collections/Testimonials.ts, trimmed
2export const Testimonials: CollectionConfig<'testimonials'> = {
3 slug: 'testimonials',
4 admin: { group: 'Content Hub', useAsTitle: 'internalTitle' },
5 access: {
6 create: authenticated,
7 read: publicApprovedTestimonial,
8 update: authenticated,
9 delete: authenticated,
10 },
11 fields: [
12 { name: 'organization', type: 'relationship', relationTo: 'organizations', required: true },
13 { name: 'speakerName', type: 'text', required: true },
14 { name: 'quote', type: 'richText', required: true },
15 { name: 'portrait', type: 'upload', relationTo: 'media' },
16 // ...a few more
17 ],
18 versions: {
19 drafts: { autosave: { interval: AUTOSAVE_INTERVAL_MS }, schedulePublish: true },
20 },
21}

From that one object Payload builds everything else: the database table, the admin screen the team edits in, a REST API, GraphQL and fully generated TypeScript types. Nobody writes a line of the types file.

So if a field is renamed and a component is not updated, the build fails before anything ships. All of it comes out of the box.

Everything Payload builds from one collection

buildsgeneratestype checksOne collection configTable, admin, REST,GraphQLTypesOur components
buildsgeneratestype checksOne collection configTable, admin, REST,GraphQLTypesOur components
Types means payload-types.ts, generated from the config. Nobody writes it by hand, so a renamed field fails the build before anything ships.
Description and steps

A flow from one collection config, a single TypeScript object. Payload builds the database table, the admin screen and the REST and GraphQL APIs from it, and generates the types file from the same object. Those generated types type check our components. The takeaway is that one object produces the database, the admin, the APIs and the types, so a renamed field breaks the build instead of the live site.

  1. One collection config (start or end)
  2. Table, admin, REST, GraphQL
  3. Types
  4. Our components (start or end)

Connections

  • One collection config to Table, admin, REST, GraphQL: builds
  • One collection config to Types: generates
  • Types to Our components: type checks

Access control

Access control returns a query

Access control is also just a function. If you are on the team you see everything. If you are the public you only get what is published.

The return value is not a yes or a no, it is a query. Payload bakes it into the database call, so a draft cannot leak.

typescript
1// src/access/authenticatedOr.ts
2export const authenticatedOr =
3 (where: Where): Access =>
4 (args) =>
5 authenticated(args) || where
6
7// src/access/authenticatedOrPublished.ts
8export const authenticatedOrPublished = authenticatedOr({ _status: { equals: 'published' } })

The setup

Two layers: a Content Hub and a website

The collections fall into two layers. The first is the Content Hub, the source of truth: clients, projects, case studies, lab projects and testimonials. It has no opinion about what a web page looks like. It holds the facts.

The second is the website: everything with a URL. A work page does not own its story. It points at a case study and decides how to present it.

A work page points into the Content Hub

presentsquotesaboutsaid byfortaggedContent Hub: the source of truthWork pageCase Study ContentTestimonialsProjectClientShared tags
presentsquotesaboutsaid byfortaggedContent Hub: the source of truthWork pageCase Study ContentTestimonialsProjectClientShared tags
Lab pages present Lab Projects the same way, and Asset Libraries file each project’s media. The page itself owns none of the words.
Description and steps

A work page presents one Case Study Content record inside the Content Hub. The case study is about a project and quotes testimonials. The project is for a client, and the testimonials are said by that client. The project is also tagged from a shared vocabulary of capabilities, industries and platforms. The takeaway is that the page holds no story of its own: its one arrow ends in the hub, and everything it shows about the client, the quotes and the tags comes from records it points at.

  1. Work page (start or end)
  2. Case Study Content (Content Hub: the source of truth)
  3. Testimonials (Content Hub: the source of truth)
  4. Project (Content Hub: the source of truth)
  5. Client (Content Hub: the source of truth)
  6. Shared tags (Content Hub: the source of truth)

Connections

  • Work page to Case Study Content: presents
  • Case Study Content to Testimonials: quotes
  • Case Study Content to Project: about
  • Testimonials to Client: said by
  • Project to Client: for
  • Project to Shared tags: tagged

The same case study can feed the website today and a newsletter tomorrow, and the assistant on the site reads from it too. Write it once, use it everywhere. That is where headless actually pays off.

Editorial flow

Publishing rebuilds one page

Pages and every Content Hub record have drafts turned on. Work autosaves while you type, live preview shows it on the real site, and it can be published now or scheduled for later.

The site is essentially static, so it is fast. The moment someone publishes, a hook fires and tells Next.js to rebuild just that one page. No full redeploy, no waiting around.

From draft to live, one page at a time

EditorPayloadNext.jsVisitorEdit, the draft autosavesLive preview of the real pagePublish now or scheduleafterChange: revalidate the pathRebuild that pageRequest the pageStatic page, published copy
EditorPayloadNext.jsVisitorEdit, the draft autosavesLive preview of the real pagePublish now or scheduleafterChange: revalidate the pathRebuild that pageRequest the pageStatic page, published copy
No full redeploy: the rest of the site stays as it was, and a visitor never reads a draft.
Description and messages

A sequence between an editor, Payload, the Next.js site and a visitor. The editor edits and the draft autosaves, and Payload answers with a live preview of the real page. The editor publishes, now or on a schedule. An afterChange hook in Payload asks Next.js to revalidate that page’s path, and Next.js rebuilds just that one page. When a visitor requests the page, Next.js serves the static page with the published copy only. The takeaway is that publishing is one hook and one rebuilt page, not a redeploy, and a draft never reaches a visitor.

  1. Editor to Payload: Edit, the draft autosaves
  2. Payload to Editor: Live preview of the real page (reply)
  3. Editor to Payload: Publish now or schedule
  4. Payload to Next.js: afterChange: revalidate the path
  5. Next.js, to itself: Rebuild that page
  6. Visitor to Next.js: Request the page
  7. Next.js to Visitor: Static page, published copy (reply)

Blocks

Blocks fix the one-template problem

Blocks are the fix for the Webflow problem, where every case study was stuck in the same template, and they are built into Payload. A block is a mini collection: a slug and some fields.

typescript
1// src/blocks/faq/config.ts, trimmed
2export const Faq: Block = {
3 slug: 'faq',
4 interfaceName: 'FaqBlock',
5 fields: [
6 { name: 'eyebrow', type: 'text' },
7 { name: 'heading', type: 'text', required: true },
8 {
9 name: 'items',
10 type: 'array',
11 minRows: 1,
12 fields: [
13 { name: 'question', type: 'text', required: true },
14 { name: 'answer', type: 'richText', required: true },
15 ],
16 },
17 ],
18}

A page has one field that holds a list of blocks and names the ones allowed there. Editors stack them in whatever order they want, so every page can be different without anyone building a template.

typescript
1// src/collections/Pages/index.ts
2{
3 name: 'layout',
4 type: 'blocks',
5 labels: { singular: 'Section', plural: 'Sections' },
6 blocks: pageLayoutBlocks,
7}

Rendering

One map from block type to component

On the frontend it is boring, in a good way. Every block is saved with its type, and a map points each type at a React component.

tsx
1// src/blocks/RenderBlocks.tsx
2const blockComponents = {
3 ...sectionChildComponents, // faq, carousel, media, text...
4 cta: CallToActionBlock,
5 featuredWork: FeaturedWorkBlock,
6 testimonialsMarquee: TestimonialsMarqueeBlock,
7 // ...
8}

How a saved block becomes a component

saved aspage loadslooks upreturnsEditor stacks blocksin the adminlayout array, eachitem tagged with ablockTypeRenderBlocksblockComponents mapReact component withtyped props
Every block is two files: a config for what the editor fills in, and a component for what it looks like.
Description and steps

A left to right chain. An editor stacks blocks in the admin, and they are saved as a layout array where every item is tagged with its blockType. When the page loads, RenderBlocks reads that array and looks each blockType up in the blockComponents map, which returns the React component for it with typed props. The takeaway is that rendering is one lookup per block, with nothing page specific in between.

  1. Editor stacks blocks in the admin (start or end)
  2. layout array, each item tagged with a blockType
  3. RenderBlocks
  4. blockComponents map
  5. React component with typed props (start or end)

Connections

  • Editor stacks blocks in the admin to layout array, each item tagged with a blockType: saved as
  • layout array, each item tagged with a blockType to RenderBlocks: page loads
  • RenderBlocks to blockComponents map: looks up
  • blockComponents map to React component with typed props: returns

So every block is two files: a config that says what the editor fills in, and a component that says what it looks like. Because the types are generated, the component’s props come straight from the config.

Sections

Sections own the rhythm

On top of that we added a Section block: a block that holds other blocks. The Section owns the background, the theme and the spacing, and the blocks inside only worry about their own content. Editors build a page out of Sections, and the vertical rhythm stays consistent whatever goes inside.

Payload config

The whole system in one file

All of it is wired up in one config file. Read it top to bottom and you know the whole system: the database is Postgres, email goes through Resend and media lives on Cloudflare R2. Then come every collection, the globals for one-off content like the header and footer, the plugins and the background jobs. Swapping the database is one line. Moving the files somewhere else is one line.

typescript
1// src/payload.config.ts, with the noise cut out
2export default buildConfig({
3 db: vercelPostgresAdapter({ pool: { connectionString: process.env.POSTGRES_URL } }),
4 email: resendAdapter({ /* ... */ }),
5 editor: defaultLexical,
6 collections: [
7 // Website
8 Pages, Posts, WorkPages, LabPages, ExpertisePages, AudiencePages, ContactPages,
9 // Content Hub
10 Organizations, Projects, CaseStudies, LabProjects, Testimonials,
11 // Assets
12 Media, AssetLibraries,
13 // Taxonomy
14 Capabilities, Industries, Platforms, Categories,
15 // Inbox, newsletter, system
16 Inquiries, AskQuestions, Newsletters, Audiences, Subscribers, Users,
17 ],
18 globals: [Home, InsightsIndex, LabIndex, WorksIndex, Header, Footer],
19 plugins: [...plugins, s3Storage({ collections: { media: true } /* Cloudflare R2 */ })],
20 jobs: { tasks: [newsletterSendTask, askQuestionRetentionTask] },
21})

One app

No separate service, no network hop

What really sold us: Payload is not a separate service we pay for and call over the network. It installs into our Next.js app. Same repository, same deploy, and the admin panel is just a route at /admin.

Payload runs inside the site

function calleditsSQLfiles, emailOne Next.js app, one deployPostgresR2 and ResendFrontend pages/adminPayload and its LocalAPI
function calleditsSQLfiles, emailOne Next.js app, one deployPostgresR2 and ResendFrontend pages/adminPayload and its LocalAPI
A page asking for its content makes a function call inside the same app. Payload’s only network hops are to Postgres, the R2 file store and Resend for email.
Description and steps

One Next.js app, deployed as one unit. Inside it, the frontend pages make a function call to Payload and its Local API, and the admin at /admin edits through the same Payload instance. Payload reaches outside the app only for Postgres over SQL, for files on Cloudflare R2 and for email through Resend. The takeaway is that the site reads its content with a function call, not an HTTP request to a separate CMS.

  1. Frontend pages (One Next.js app, one deploy)
  2. /admin (One Next.js app, one deploy)
  3. Payload and its Local API (One Next.js app, one deploy)
  4. Postgres (start or end)
  5. R2 and Resend (start or end)

Connections

  • Frontend pages to Payload and its Local API: function call
  • /admin to Payload and its Local API: edits
  • Payload and its Local API to Postgres: SQL
  • Payload and its Local API to R2 and Resend: files, email

So when a page needs its content there is no fetch, no API key and no network hop. It is a function call straight to the database, typed the whole way through: the page knows every block its layout could possibly hold.

typescript
1// src/app/(frontend)/[slug]/page.tsx
2const payload = await getPayload({ config: configPromise })
3
4const result = await payload.find({
5 collection: 'pages',
6 draft,
7 limit: 1,
8 overrideAccess: draft,
9 where: { slug: { equals: slug } },
10})

The Content Hub

The reason for the move

Collections, blocks and a config file are plumbing. The reason for the whole move was the Content Hub: one place for the work to live, apart from any page.

A website is a menu. The Content Hub is the kitchen. In Webflow the menu was the kitchen: the only place the food existed was on the plate already served. To serve the same dish somewhere else, in a proposal, a pitch deck, an email or a slide, we copied it off the plate.

And copies drift. The case study on the site says one thing, the deck says a slightly better thing, the proposal has a number nobody can source anymore, and somewhere in a Slack thread there is a client quote nobody is sure we are allowed to use. That is not exactly a Webflow problem. It is what happens when the only copy of the truth is also the marketing layout.

Where the story lives, before and now

copiedreplaced byread byBefore: the page owns the storyNow: the hub owns the storyCase study page, theonly copyDeck, proposal, email,each driftingCase Study Content,the only copyWork page, Ask, agentdrafts
copiedreplaced byread byBefore: the page owns the storyNow: the hub owns the storyCase study page, theonly copyDeck, proposal, email,each driftingCase Study Content,the only copyWork page, Ask, agentdrafts
Before, every use of the story was a copy that aged on its own. Now every use reads the one record, so a fact fixed once is fixed everywhere it is cited.
Description and steps

A chain in two stages. Before: the case study page was the only copy of the story, and the deck, the proposal and the email were each copied from it and drifted on their own. Those scattered copies are replaced by one Case Study Content record in the hub, now the only copy, which is read by the work page, by Ask and by agents drafting decks and proposals. The takeaway is that nothing leaves the record anymore: every use points back to it.

  1. Case study page, the only copy (Before: the page owns the story)
  2. Deck, proposal, email, each drifting (Before: the page owns the story)
  3. Case Study Content, the only copy (Now: the hub owns the story)
  4. Work page, Ask, agent drafts (Now: the hub owns the story)

Connections

  • Case study page, the only copy to Deck, proposal, email, each drifting: copied
  • Deck, proposal, email, each drifting to Case Study Content, the only copy: replaced by
  • Case Study Content, the only copy to Work page, Ask, agent drafts: read by

The difference comes down to one kind of arrow. When the page owns the story, copies leave home and start aging the moment they are pasted. When the hub owns the story, nothing leaves: every use points back to the record, and fixing a fact in one place fixes it everywhere it is cited.

The easiest way to show it is to follow one real engagement all the way through, so everything below is Vault Workforce Screening.

The records

Each record type has one job

Each record type in the hub has its own job. Clients are who: name, logo, site, industries.

Projects are what happened: the plain, factual record of an engagement. Who it was for, when it ran, whether it was a project or a retainer, what was in scope, what we delivered and what constrained us. No adjectives. There is a project for every engagement, even one that will never become a case study, because the file has to live somewhere.

The Projects list in the Content Hub: client engagements with their organization, status and publish state, one row blurred.

The Projects list: one factual record per engagement.

Case Study Content is the story. It is the writing, and it is separate from the project on purpose. It does not need a web page to exist, so a story can be usable long before anyone designs a page for it.

Testimonials are quotes, with an approval state attached so we always know which ones we are cleared to use. Asset Libraries are the files, grouped by client and project instead of dumped in one big media bin.

Taxonomy

Tag a project once

Projects carry the tags, and the tags are a shared vocabulary rather than free text, so Brand Strategy means the same thing on every record. Vault is tagged with its capabilities and its industry, and that is the whole input.

The project fills in the client, industry and capabilities on the live page, and its tags power the related work at the bottom of a case study, which asks for pages that share a capability with this one. Nobody picks those by hand unless they want to. Tag a project once and it starts showing up in the right places on its own.

The Classification tab on the Vault Workforce Screening Brand and Website project: capabilities Brand Strategy, Messaging, Brand Identity, Web Design, UX Strategy and Marketing Collateral, and the industry HR Tech.

Vault's project, tagged from the shared vocabulary.

typescript
1// src/blocks/shared/related-pages.ts
2where: {
3 and: [{ id: { not_equals: currentId } }, { [capabilityPath]: { in: capabilityIds } }],
4}

The story model

The story model is the whole trick

Every case study is written in six sections: context, challenge, strategy, approach, outcomes and learnings. That is not a template invented for the website. It is how work gets explained out loud, and it is stated in exactly one place in the code. Lab projects, this one included, use the same six.

typescript
1// src/collections/story/sections.ts
2export const STORY_SECTIONS = [
3 'context',
4 'challenge',
5 'strategy',
6 'approach',
7 'outcome-summary',
8 'learnings',
9] as const

Inside each section you can write one continuous overview, or break it into Story Beats: small, self-contained ideas. Each beat has a key that never changes, an internal label so it is findable in a dropdown, an optional public heading and the copy itself.

Vault’s Approach has one overview paragraph up top and the beats under it: Introduction, Connecting Solutions, Brand Foundation Elements, Web Design, Social Assets and more.

Vault's case study in the admin: the Approach overview, then the first Story Beat with its key approach-introduction, the label Introduction and the heading A Visual Language Rooted in the Real World.

Vault's Approach: an overview, then Story Beats with stable keys.

It sounds fussy. It is not, and here is the payoff: copy written this way is channel-neutral. It does not know it is on a website. The same beat can be a paragraph next to a photo on the work page, a bullet in a deck, or the passage the assistant quotes when someone asks how we approach a redesign. Same words, no retyping, no drift.

Presentation

The page owns the layout, not the words

The work page is where presentation happens, and it starts by naming the story it is telling. One field holds the canonical Case Study Content record the page renders, and Vault’s work page points at Vault’s case study and nothing else. The page owns layout, order, art direction, media and SEO. It does not own the words.

Vault's work page in the admin, Content Source tab: the Case Study field set to Redefining a health brand for a new market, with the live preview beside it.

Vault's work page names the one case study it presents.

The page is composed out of blocks, and the rows say what they are. Stacked, Challenge: Repositioning Vault. Split narrow, Challenge: Communications. Pair offset, Challenge: Impact and Value. Section, Approach. Every row is a layout choice on the left and a piece of the canonical story on the right. Whoever builds the page decides how it looks, not what it says.

The Composition tab of Vault's work page: rows named by layout and story part, such as Stacked, Challenge: Repositioning Vault, Split narrow, Challenge: Communications, and Section, Approach.

Composition rows: a layout choice paired with a piece of the canonical story.

Wiring

A block points at a beat

Open one of those rows and the wiring is plain. The source is the challenge section, the story content is a Story Beat, and the beat is Repositioning Vault. The block renders that beat, whatever the copy says after the next edit in the hub. Underneath, the block picks its own media, because the image is a page decision.

One composition row opened: the source set to challenge, the story content set to Story beat, the beat Repositioning Vault, and the row's own media below.

A row pointed at the Repositioning Vault beat, with its own media below.

Choose custom instead and the copy is written right there on the page, for the times a page genuinely needs its own line. The resolution rule is written once and tested: whatever the block’s author typed wins, and the moment that field is empty the canonical copy falls through into its place. No stale duplicate sits underneath.

The hub protects the pages too. Rename or delete a beat that a page presents, and publishing the record is refused, with a message naming the page to update first.

typescript
1// src/collections/story/validate.ts
2throw new APIError(
3 `${missing.section} Story Beat ${missing.key} is used by a ${singular}. ` +
4 'Update that page before renaming or removing the beat.',
5 400,
6)

One record, its readers and its guardrail

Work pageVault recordAgentEditorPresent the Repositioning Vault beatThe beat as it reads todayRead the Web Design beat over MCPCopy for a deck or proposalPublish a rename of that beatRefused: a work page presents it
Work pageVaultrecordAgentEditorPresent the Repositioning Vault beatThe beat as it reads todayRead the Web Design beat over MCPCopy for a deck or proposalPublish a rename of that beatRefused: a work page presents it
The work page names the challenge section and the Repositioning Vault beat. Draft edits save freely; publishing a record that would strand a page’s beat is refused.
Description and messages

A sequence between Vault’s work page, Vault’s case study record, an agent and an editor. The work page asks the record for the Repositioning Vault beat and gets the beat as it reads today. An agent reads the Web Design beat over MCP and gets copy for a deck or a proposal. The editor then tries to publish a rename of the Repositioning Vault beat, and the record refuses because a work page presents it. The takeaway is that the page, the agent and every other reader share one copy, and the page is protected from losing the passage it depends on.

  1. Work page to Vault record: Present the Repositioning Vault beat
  2. Vault record to Work page: The beat as it reads today (reply)
  3. Agent to Vault record: Read the Web Design beat over MCP
  4. Vault record to Agent: Copy for a deck or proposal (reply)
  5. Editor to Vault record: Publish a rename of that beat
  6. Vault record to Editor: Refused: a work page presents it (reply)

Summaries

Short versions, written once

The summaries live on the record too, at three lengths: one line for a card, short for a listing, medium for a hero. Vault’s one-liner is "Making workforce screening matter by showing the people doing high-stakes work." That exact sentence is available to a card, a listing, a search result, or a channel nobody has built yet.

Write the short versions once, with the story in front of you, instead of improvising a new one at 11pm inside a deck.

The Overview tab of Vault's case study: title, project, thesis, and the one-line summary Making workforce screening matter by showing the people doing high-stakes work.

Vault's summaries, written once on the record.

Guardrails

Nothing goes out by accident

This is what makes the hub safe for material that is not public. Every record can hold internal notes, provenance and an internal claim log, all invisible outside the team, because access is set on the field, not only on the document.

Client quotes carry an approval state, and only approved-public quotes can ever render. Media carries a usage status, and anonymous visitors cannot query anything that is not public-approved.

typescript
1// src/collections/Media.ts
2read: authenticatedOr({ usageStatus: { equals: 'public-approved' } }),

Numbers get the strictest treatment, because numbers are what get us in trouble. A metric cannot be marked public without a label, a value, and either a source or a qualifier. If one is missing, the CMS refuses the publish.

typescript
1// src/collections/CaseStudies/hooks/validateCaseStudy.ts
2const invalidMetric = merged.metrics?.find(
3 (metric) =>
4 metric.approvedForPublic &&
5 (!metric.label || !metric.value || (!metric.source && !metric.qualifier)),
6)
7if (invalidMetric) {
8 throw new APIError(
9 'Public metrics require a label, value, and either a source or qualifier.',
10 400,
11 )
12}

Worth saying plainly: none of this is a check someone remembered to write in a component. Document rules are conditions Payload adds to the database query itself, and field rules apply on every read, so the unapproved version cannot be fetched, let alone rendered.

Who gets what from a hub record

teampublicapprovedthe restHub record: facts,notes, quotes, files,numbersWho is asking?EverythingAccess rulesShown on the siteStays private
teampublicapprovedthe restHub record: facts,notes, quotes, files,numbersWho is asking?EverythingAccess rulesShown on the siteStays private
Document rules become conditions on the database query, and field rules strip internal fields on every read. None of it lives in a component.
Description and steps

A flow from a hub record holding facts, internal notes, quotes, files and numbers. A decision asks who is asking. A signed in team member gets everything. A public request goes through the access rules: what is published and approved is shown on the site, and the rest, such as drafts, unapproved quotes, internal media and internal fields, stays private. The takeaway is that the guardrail is part of reading the data, so there is nothing for a component to forget.

  1. Hub record: facts, notes, quotes, files, numbers (start or end)
  2. Who is asking? (decision)
  3. Everything (start or end)
  4. Access rules
  5. Shown on the site (start or end)
  6. Stays private (start or end)

Connections

  • Hub record: facts, notes, quotes, files, numbers to Who is asking?
  • Who is asking? to Everything: team
  • Who is asking? to Access rules: public
  • Access rules to Shown on the site: approved
  • Access rules to Stays private: the rest (optional or async)

Assets

Files carry their own paperwork

Files get the same treatment as the words. Every client has a library, organized by project, and every asset carries its own paperwork: the client, the project, what it documents, whether it is cleared for public use and which channels it is approved for.

When someone needs the Vault banner mockup for a proposal, they go straight to the project library and check what it is cleared for, instead of scrolling through a shared drive and asking around.

The Media library by folder: a folder per client or project, plus folders for brand, insights, posts, SVGs and the Streak Field Studio. One client name is blurred.

Media, filed by client and project.

Vault's media folder: research, working shots, a banner mockup, a home page mockup and logo studies, each filed in the library.

Vault's library.

One Vault asset's paperwork: client, project, purpose, usage status set to public-approved, credit, source URL, approval for all channels and the asset date.

Every asset carries its own paperwork.

What it feeds

One record, four readers

Different readers now work from the same records: the website, the assistant on the site, AI crawlers and the agents we work with.

The website. Each work page presents a case study it does not own. The Vault page is a layout, and the words live in the hub.

Ask, the assistant on the site. When a hub record is published it is turned into embeddings alongside the page that presents it, so a visitor asking a plain question gets an answer grounded in our actual writing, with links. Every question is logged, redacted and tagged with what happened.

The Ask questions log: each question asked on the site with its outcome, such as Partial or Answered, its status, the page it was asked on and when.

Every question asked of Ask, with its outcome.

The quietly most useful thing in the CMS is the outcome recorded on every question asked of Ask. A question marked no sources is someone asking for something we have not written yet. It is a content brief generated by real visitors instead of by us guessing.

AI crawlers. The same registry that decides which pages are public also generates llms.txt, so when someone’s assistant goes looking for us it reads structured text instead of scraping a layout.

Agents, through MCP. The CMS exposes itself as a tool server, so we can ask Claude to draft a case study from a project record, audit which stories are missing summaries, or find every engagement tagged for an industry. Authoring is drafts only, visitor data with contact details is read only, and publishing stays a human decision.

Why it was worth it

The story no longer waits for the page

We can finish telling a story without finishing the design of its page. The writing and the website do not have to move at the same speed anymore.

On Vault, the story blocks pull their copy straight from Story Beats. The page arranges the story without keeping its own copy of every passage, so when a passage changes in the hub and is published, the page picks it up. Nobody goes looking for the same paragraph inside a layout to change it again. And when the layout changes, the story stays put.

Not retyping a passage sounds like a small thing to describe. It was the thing we kept running into with the old setup, where every new use of the work meant pulling it apart and putting it back together somewhere else.

The project, the story, the short version, the quote and the files already exist as records, so an agent can read them through MCP and work from what is actually there.

The result is a working site where changing the presentation does not mean rewriting the content, and writing the content does not mean building a page. The next case study starts with the work itself, and the next place we want to use it has something to start from.

Learnings

What we took from it

The website used to be the thing we owned. Now it is the first thing we do with what we own.

About this project

Status
In progress
Built with
Payload CMSNext.jsTypeScriptPostgreSQLpgvectorCloudflare R2ResendModel Context Protocol

More from the lab

Payload CMS Shader Plugin

Ready to start?

Let’s make it make sense.

Request an estimateGet in touch

Want to go deeper?

Ask about our work, process, capabilities, or what working together could look like.

S&S

Ask Suits & Sandals

Online

Privacy PolicyTerms and Conditions