Skip to main content

ARC 2.0.0-beta.35 · after arc#4065

Restrict who can read a page

Declare a collection default, tighten individual records with access, and check the serve-time gate before you publish.

Web Device decides who may read a page at serve time, before it replays prerendered HTML. public must be written at some policy layer. An unmarked record with no collection, path, or hatch default is a 404, including for site admins.

Connect currently admits four roles: guest, member, admin, and owner. none is not a role; it is a hard deny.

Start with a public collection and a tighter record

The usual setup: the collection default is public, so unmarked records stay readable. Then mark the few restricted records.

yaml
# blocklet.yaml
collections:
  articles:
    access: public
yaml
# content/articles/members-only/content.md
---
title: Members-only note
access: member
---

This site's articles collection already has that default. collections.<type>.access is the default for details of that type. It does not set the /articles/ list page. Zero-config collection list routes are public unless you lock that route. A type-level member default therefore does not close the list; it closes unmarked details.

Leave preview unset. A signed-out reader gets the first screen of the body, then the gate. The rest of the tree never enters the HTML. Sitemap, llms.txt, and MCP omit a gated record unless you set preview: true. Unmarked records that inherit public from the collection stay on those faces (after arc#4086).

collections.<type>.readRole is a storage read grant, not this HTML gate.

A signed-out caller is guest. A signed-in DID Connect session may include member, admin, or owner. This page does not document how a site grants those roles.

Choose the record fields

You writeUnsigned readerMatching role, or admin/owner
No access, collection access: publicFull pageFull page
No access, and no collection / path / hatch default404404
access: publicFull pageFull page
access: member (or admin / owner), preview omitted, record is listableFirst-screen teaser + gateFull page
access: member and preview: trueTeaser + gate. Declared excerpt is used only when there is no teaser HTMLFull page
access: member and preview: false, record is listableGate only, no teaserFull page
access: member and preview: false, record is not listable404Full page
access: none404404

A record named role only tightens the scope above it. access: public on a record cannot open a locked path or collection. To open one slug under a lock, add a more specific accessPaths rule. See Content access reference.

A record is listable when the list route is public (zero-config /{locale}/{type}/ routes are), or when blocklet.yaml sets the hatch accessDefault: public. Collection access is not what makes the list public. On this site the article list is public, so a gated article without preview: false shows a teaser.

Prerender may write the full body into .web-cache. The gate runs on serve, before that file is replayed. Do not host .web-cache as static files.

What a denied reader sees

The detail stays in site chrome. Title, author, and date stay in post-hero. The cover is cleared. The body slot is a first-screen teaser (unless preview: false) and the access-gate stop.

Anonymous reader on an admin-only article: hero stays, first-screen teaser, then a Sign in gate

Anonymous GET of a listed admin article on ARC 2.0.0-beta.35, 2026-08-15, http://arcblock.localhost:4922/. Live capture, not a mock. That fixture URL is no longer on this site.

Anonymous GET of a listed access: admin article on 2026-08-15, ARC 2.0.0-beta.35, http://arcblock.localhost:4922/:

bash
curl -sI http://arcblock.localhost:4922/en/articles/access-fixture-team/
text
HTTP/1.1 200 OK
cache-control: private, no-store
x-robots-tag: noindex

The HTML includes access-teaser, then:

html
<aside class="access-gate" data-kind="promo-shell" data-required-role="admin">
  <p class="access-gate-message">The rest of this piece is for admins.</p>
  <a class="access-gate-signin"
     href="/.well-known/service/login?return_to=%2Fen%2Farticles%2Faccess-fixture-team%2F&amp;cancel_to=%2Fen%2Farticles%2Faccess-fixture-team%2F">Sign in</a>
</aside>

The protected body from that capture is absent. The same URL with a matching test caller returned the body and no gate. Sign-in is DID Connect.

The teaser is an SSR cut of the AUP tree (~480 visual units, at most two top-level blocks, last block always withheld). It is not a CSS fade over the full body.

Check a live site

Use a runtime that includes arc#4065 (arc --version on the capture above: 2.0.0-beta.35). Point the service at your blocklet and probe records you declared there. This site does not ship access-gate test articles.

bash
# Hard deny — a record with access: none
curl -sI http://arcblock.localhost:4900/en/articles/<your-none-record>/
# HTTP/1.1 404 Not Found
# cache-control: private, no-store
# content-length: 0

# Explicit public
curl -sI http://arcblock.localhost:4900/en/articles/<your-public-record>/
# HTTP/1.1 200 OK
# cache-control: no-cache

Local services started with AFS_TEST_ALLOW_CALLER_HEADER=1 accept a test caller. Both headers are required:

bash
curl -s -H 'x-caller-did: z1admin' -H 'x-caller-role: admin' \
  http://arcblock.localhost:4900/en/articles/<your-admin-record>/ \
  | rg '<a string that exists only in the protected body>'

A member caller opens a access: member record and still sees the gate on an access: admin record:

bash
curl -s -H 'x-caller-did: z1member' -H 'x-caller-role: member' \
  http://arcblock.localhost:4900/en/articles/<your-member-record>/ \
  | rg '<member-only body marker>'

curl -s -H 'x-caller-did: z1member' -H 'x-caller-role: member' \
  http://arcblock.localhost:4900/en/articles/<your-admin-record>/ \
  | rg 'access-gate|<admin-only body marker>'

x-caller-role alone still returns the gate. Do not send these headers on a public deployment.

Failures

What you seeUsual cause
404, empty body, Cache-Control: private, no-storeNo scoped default, or access: none
Gate, but no first-screen teaserYou set preview: false
Teaser includes the last section of the articleUnexpected. File an upstream issue; the cut must withhold the last top-level block
Record access: public still gated or 404A stricter accessPaths or collection default is the ceiling. If the list route is public you get a teaser/gate, not a 404
Signed-in member still sees an admin gateAdmit is AND across every named constraint. member does not satisfy admin
Sitemap / llms.txt / MCP lists a gated articleThat face only emits a gated record when preview: true (shell fields, no body). Omit preview to keep it off discovery

Limits

  • v1 admits guest / member / admin / owner from DID Connect. Other role names can be written and are shown in the gate copy; Connect does not currently issue them.
  • Verifiable-credential roles are not an admit path. Track arc#4056.
  • Item-local media and collection RSS use the same admit rule. A teaser never keeps a media block (the file would 404).
  • Shared public cache is only allowed for a full anonymous-open page.
  • index.access.json next to prerendered HTML is not a public URL. A GET of that path is 404.

Resolution order, accessPaths syntax, HTTP headers, and discovery rules: Content access reference. Gate markup: access-gate.