# Who may type (/docs/guides/collaborative-docs/access)

---
title: Who may type
description: 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.
kind: guide
package: questpie
---

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()`.

| Grant  | Comes from                                                     |
| ------ | -------------------------------------------------------------- |
| `view` | the collection's `read` rule, evaluated against the loaded row |
| `edit` | the 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.

<Callout type="warn" title="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.
</Callout>

## The field grants

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

| What you declared                        | What the browser gets                 |
| ---------------------------------------- | ------------------------------------- |
| nothing                                  | `edit`, 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                  |

```ts title="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](/docs/schema/access-control/fields) 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.

| Call                                  | Throws                                |
| ------------------------------------- | ------------------------------------- |
| write into a field you hold `view` on | `CrdtMutationError` `FIELD_VIEW_ONLY` |
| write into a field you were not given | `CrdtMutationError` `FIELD_HIDDEN`    |
| read a field you were not given       | `CrdtReadError` `FIELD_HIDDEN`        |

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

<Callout type="info" title="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.
</Callout>

## 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.
