QUESTPIE
GuidesCollaborative docs

Who may type

A collaborative session has no rules of its own. It reads the access rules already on the collection, once for the document and once for every marked field.

View markdown

Your document opened read-only and you did not ask it to. Or a field you marked throws the moment you touch it. Both answers are in the collection file, not in the CRDT config.

The document grant

The server resolves two grants when you call connect().

GrantComes from
viewthe collection's read rule, evaluated against the loaded row
editthe collection's update rule, against the same row

Both rules run with the stored row as data. The update rule also gets an empty input, because there is no patch yet. So it has to decide on the row alone. A rule that returns a row filter is matched against that one row. A rule that throws is read as a refusal.

Fail the read rule and the open is refused outright. Pass read, fail update, and a mode: "edit" open is refused too. Add fallback: "view" to take the read-only session instead.

Anonymous is never an option

Opening a session needs a user session, an OAuth principal or an agent credential. read: true opens the REST route to anonymous callers. It does not open a collaborative session to them.

The field grants

Each marked field is graded separately, and the document grant caps it.

What you declaredWhat the browser gets
nothingedit, when the document has edit
fields: { content: { read: false } }no grant, and every call on it throws
fields: { content: { update: false } }view on that field
src/questpie/server/collections/articles.ts
.access({
	read: ({ session }) => !!session?.user,
	// `data` is the stored row, so this needs an `ownerId` field on it.
	update: ({ session, data }) => data.ownerId === session?.user?.id,
	fields: {
		// Everyone signed in watches. Only the owner types.
		content: { update: ({ user, doc }) => doc?.ownerId === user?.id },
	},
})

Field rules take read, create and update. Only two of them reach a collaborative field. create is about a write the collaborative client never makes. Field-level access covers the context each rule receives.

Deny read on every marked field and the open is refused too. A session with nothing in it is not a session.

What a refusal looks like in the browser

Grants arrive with the session and land on fieldGrants in the ready state. The ports throw rather than quietly doing nothing.

CallThrows
write into a field you hold view onCrdtMutationError FIELD_VIEW_ONLY
write into a field you were not givenCrdtMutationError FIELD_HIDDEN
read a field you were not givenCrdtReadError FIELD_HIDDEN

So render from fieldGrants rather than from try and catch. The guide does that in one line, setting readOnly on the textarea.

A refused open says nothing about why

Every failure on the open route answers the same way, HTTP 404 with CRDT_UNAVAILABLE. A failed rule, a short secret and an anonymous caller all look identical in the browser. Work out which one it was on the server.

Defaults

Write no .access() at all and each rule falls back to your app's defaultAccess. Past that it requires a session. So every signed-in user gets view and edit on every marked field, and nobody else gets in. That is the right default for an internal tool and the wrong one for a public site.

The working result

You can say, per person and per field, who watches and who types. It is the same object that already guards GET /api/articles. There is no second policy to keep in sync, and no way for the two to disagree.

On this page