Skip to content
Get started

Opinion

Drafts had to be a permission, not a flag

Testing saved content on a development server should require permission to read drafts, without turning Publish into a development tool.

Delivery roles in the Shapio admin: each site has a delivery role and a drafts role for its development server.

Save, refresh, nothing

I was editing my own site's content in the Shapio admin with the site's development server running. I saved a change, refreshed the page, and nothing changed. I asked whether I had to publish too.

The answer was yes. The site reads published content only, by design. Drafts never leak to the public API, so saving a draft had done exactly what it was supposed to do.

Publish meant production

My objection was simple: Publish is a production action. On my site, publishing fires a Cloudflare Pages rebuild of the live site. Using that button to test content on a development server would also ship it.

That left a gap between saving content and seeing it in the development site. Shapio already had per-entry preview tokens for the admin's Preview pane. Those covered one entry for one hour.

A per-entry preview did not give the development server access to saved drafts across the site. I wanted to render those drafts while keeping Publish as the action that changes production. That distinction belongs in the product, even when the person editing the content also runs the site.

The tempting flag

The wrong fix would have been a site setting: SHAPIO_DRAFTS=true. Set it, ask the server for drafts, and have the server hand them over. It would have answered my immediate complaint.

It would also have made any site with that flag able to read any draft. The rule that public delivery never leaks drafts would then depend on a configuration value on someone else's machine. That is an unacceptable place to enforce access.

The site can decide which content it wants to request. The server has to decide which content the caller is allowed to receive. Combining those decisions into one flag makes the access rule too easy to mistake for a development convenience.

Two safeguards

The fix shipped on October 5, 2026, in release 0.5.1. Delivery roles gained a new permission, "Read drafts". That permission covers every model the role can read.

The delivery API accepts ?publicationState=draft, and GraphQL accepts publicationState: DRAFT. A token without the grant gets DRAFTS_FORBIDDEN. Anonymous callers and end-user accounts can never read drafts.

That gives us two required safeguards: the grant on the server and the request from the site. The token must be allowed to read drafts, and the site must ask for them. A production token has no grant, so the flag alone shows nothing.

The starters use SHAPIO_DRAFTS=true to turn on draft reads with a separate development token created by the seed. They also show a "Drafts" badge on every page. The development token is never put in a production environment.

The flag still exists, but it now expresses what the site is asking to render. It cannot grant itself access. Configuration selects the mode, while the server enforces the permission.

Draft responses also carry Cache-Control: private, no-store. The client has a drafts: true option, and under Next.js those reads are never cached or tagged. Being allowed to read a draft never makes it cacheable.

Relations exposed the mistake

The first draft of the plan had a per-model grant. Review caught the problem with it: related entries were only checked for plain read permission, so a token allowed drafts of posts could have pulled draft authors through a relation.

The grant became token-wide, covering every model the role reads. A caller's ability to read drafts therefore applies across those models, including the related entries.

Plan review caught several problems like it before any code was written, and the change shipped in one release. The relation catch is the one I keep coming back to. It shows why the permission needs to cover the content a request can reach, rather than just the entry where the request starts.

Who enforces it

If the server cannot refuse it, it is not a permission, it is a hope.

That is my test for where a thing belongs: who enforces it. A site can ask for drafts with a flag. The server must be able to say no.