# Presence now works in both directions

**Version:** v2.19.0

**Published:** August 3, 2026

Until this release, an app could see other people in a document but never appeared to them, so a Studio user editing alongside your app saw nobody. Your app can now announce its own users, read presence for a single document rather than the whole dataset, and interoperate with the Studio's navbar and field indicators. Presence also survives network interruptions, and participants who disappear no longer linger.

Both `@sanity/sdk-core` and `@sanity/sdk-react` are published at `2.19.0`. There are no breaking changes: existing `usePresence` callers keep working, and they don't start announcing on upgrade.

## Report presence from your app

`useReportPresence` is the new write side. Reporting is opt-in per call site, so nothing announces unless you ask for it.

**index.ts**

```
import {useReportPresence} from '@sanity/sdk-react'

// Document level: "I am in this document"
useReportPresence({documentId, documentType})

// Field level: "I am in the title field"
useReportPresence({documentId, documentType, path: ['title']})
```

`path` accepts keyed and numeric segments, so you can address array items and Portable Text spans. Pass `selection` to report a Portable Text caret. You can report carets today, but there's no hook yet for turning remote selections into rendered ones. That arrives with a Portable Text plugin built on this release.

### Read presence for a single document

`usePresenceForDocument` returns only the participants in one document:

**index.ts**

```
const {presence} = usePresenceForDocument({documentId, documentType})
const inTitle = usePresenceForDocument({documentId, documentType, path: ['title']})
```

Each participant is `{user, sessionId, documentId, path, lastActiveAt, selection?}`.

By default, a draft, its published document, and any release versions count as the same document, which is what a document list wants. Pass `excludeVersions: true` to compare ids exactly, which is what a field indicator wants.

### Studio interoperability

Your app and the Studio share the same presence room and wire format, so both directions work without configuration. Your app's users appear in the Studio's navbar and field indicators, and Studio users appear in your app.

Field-level interoperability depends on the perspective, because the Studio's field indicators compare the exact document id its form is on. Pass the plain document id and let the perspective decide, either from `ResourceProvider` or on the handle:

**index.ts**

```
// Reports the draft, which is what the Studio edits by default
useReportPresence({documentId, documentType})

// Reports a release version
useReportPresence({documentId, documentType, perspective: {releaseName: 'autumn'}})
```

### Things to know

- Presence is scoped to a project and dataset, not to your organization. It answers who else is in this Document, not who is online at your company.
- Participants are counted by session. One person in two tabs appears twice, with two `sessionId` values and the same `user.id`. Group by `user.id` for one avatar per person.
- `lastActiveAt` comes from the sender's clock, so use it for display only. Expiry is handled for you, and a skewed clock will report the wrong time.

### New exports

- `@sanity/sdk-react`: `useReportPresence`, `UseReportPresenceOptions`, `usePresenceForDocument`, and `UsePresenceForDocumentOptions`.

## Notable bugfixes and improvements

- Presence survives a dropped connection. Reconnects use exponential backoff, and the inbound subscription is rebuilt with the socket instead of leaving an empty participant list behind.
- Participants who vanish are cleaned up. Sessions expire after 90 seconds without a heartbeat, and the local user disconnects explicitly on `beforeunload` and `pagehide`, which covers iOS Safari and the back/forward cache.

