Published and public are not the same word
Most headless CMSes have two states for a record: draft, and visible to the entire internet. That is the wrong shape for a members’ area, per-customer documentation or a launch that opens by region. Segments add the missing axis — an entry can be live and still be readable by only the people it is for.
For teams whose content is gated, tiered, or opens to different people at different times
Three rules, and nothing else to learn
An segment is a named set of tags. An entry carries two lists of them — allow and deny — and a reader arrives carrying tags of their own. Everything the feature does follows from comparing those three things.
A reader on the deny list is refused, whatever else is true. An empty allow list means the entry is unrestricted. Otherwise the reader has to match the allow list. That is the whole predicate, and it is one SQL fragment rather than a service the API calls — it lands inside the query the reader was already making.
The two absences are read in opposite directions, and both are deliberate. No access row means unrestricted, because that is what every entry ever written already is, and reading absence as a locked door would black out an entire library the day you switched the feature on. No reader context means anonymous, because treating an unknown caller as unconstrained would turn every gap in coverage into an open door.
| Entry | Anonymous | Member | Staff |
|---|---|---|---|
| No lists set | Yes | Yes | Yes |
| Allow: members | No | Yes | No |
| Allow: members, staff | No | Yes | Yes |
| Allow: members · Deny: staff | No | Yes | No |
Where a reader’s tags come from is your decision
Apograph does not authenticate your readers, and deliberately does not try to. You give it one function, and it receives the request: read a JWT claim, trust a header your CDN set, look the caller up in your billing system, or any combination of those. What comes back is a set of tags.
That is the whole per-install configuration. The alternative — a subscriber table inside the CMS — would put Apograph in the business of owning your customer records, which is a worse place for them to live than wherever they already are.
Leaving the resolver out is a working configuration, not a broken one. Every reader is then anonymous: unrestricted content serves exactly as before, restricted content does not serve at all. It fails in the safe direction, which is the only direction an entitlement check may fail in.
The parts that are easy to get wrong, done for you
- Relations are checked on every hop
- A reader allowed to see an article is not thereby allowed to see everything it links to. The same predicate is asked about the target of each relation — on REST expansion, on GraphQL’s nested resolvers, and through an agent’s tools. There is no protocol that quietly skips it.
- Hidden records do not leak through counts
- A restricted target is missing from the items and absent from the total. "5 links, 2 visible" would tell a reader that three restricted records exist here, which is exactly the fact the restriction was meant to withhold.
- Access is versioned with the words
- Segments travel in the entry’s own save, on the same transaction, so a save cannot land with its restriction missing. The revision that save appends records the access it applied — which means restoring an old version puts back who could read it, not just what it said.
- Translations cannot drift apart
- "Who may read this" is a fact about the record, not about its German wording, so it is written to the whole locale group at once. Left per row it was a hole invisible from any screen: the English article restricted, the German one published to everybody.
- Editors can see the restriction
- Reading an entry’s segments is granted to contributors and viewers, because an editor who cannot see that a record is gated will eventually publish one believing it is public. Changing them is admin-only.
- Turning it on changes nothing
- With no segment created, the predicate is never emitted and a public read costs exactly what it did before. There is no migration to plan and no day-one behaviour change to communicate.
A tiered publication
You run a research site: summaries are open, full reports are for subscribers, and two reports are under embargo for a client until Thursday. That is three segments, an allow list on each report, and a deny list on the two embargoed ones. Your front end sends the reader’s subscription tier as a header, and every route — the list, the detail page, the related-reports rail — narrows itself without your querying code knowing this feature exists.
Questions about gating content
How many segments can I have?
Three on Community, unlimited on a plan — the Segments cap is one of the four a plan lifts. Most installations need one or two in any case: a segment is a named set of reader tags rather than a record per customer, so one usually covers a whole tier of them.
Does this give me a login for my readers?
No. Apograph authenticates the people working in the CMS, never your segment. You resolve a reader to a set of tags however you already identify them — that is the seam, and it is one function.
Can an editor restrict content without an admin?
No. Reading the restriction is granted to contributors and viewers; setting it needs segments:manage, which is admin-only. Renaming a segment’s tags changes who every entry naming it is visible to, so it is a configuration decision rather than an editorial one.
Does it slow down public reads?
Only where there is something to check. With no segment created nothing is emitted at all. Where there is, the check is a primary-key probe plus two indexed array intersections, inside the query you were already running rather than a second round trip.
What happens on a restore?
The version’s segments come back with its words. A restore that would genuinely change who can read the entry is refused unless the person doing it may manage segments — which is right rather than a special case, whatever button started it.
Next
Every feature is included, free
The core is MIT licensed and every feature is in it — none of them is paid-only. Community runs free in production; a plan buys room and governance, not a different product.