# Collaborative item editing

Source: https://docs.externa.qiick.io/docs/collaborative-editing

When two editors open the same collection item, Externa shows **who else is on the form**, optional **soft lock** (read-only for others until takeover), and a **stale-save warning** if the row changed on the server. This is a Directus-style collaboration signal — not CRDT field sync.

> **Requires Reverb**
> Presence and live lock hints use **Laravel Echo** on presence channel `collection-item.{collectionId}.{itemId}`. Configure [Reverb & Echo](/docs/reverb-and-echo) (`BROADCAST_CONNECTION=reverb`, `reverb:start`, and Vite `VITE_REVERB_*`). When realtime is off, the item form still sends `expected_updated_at` on save so the API can reject stale overwrites; avatars and lock whispers are degraded.

> **Related**
> Item CRUD: [Collection items](/docs/collection-items). Team chat (separate channel): [Chat](/docs/chat). Global online users: [Reverb & Echo](/docs/reverb-and-echo).

## Behavior

| Feature                | Realtime on                                                | Realtime off (degraded)                                |
| ---------------------- | ---------------------------------------------------------- | ------------------------------------------------------ |
| Other editors’ avatars | Echo `here` / `joining` / `leaving` on `collection-item.*` | Hidden; short banner notes limited mode                |
| Soft lock              | Whisper `item-lock` + heartbeat (~30s TTL)                 | Not enforced client-side; server ACL unchanged         |
| Stale overwrite        | Whisper `item-updated-at` + dialog before submit           | `expected_updated_at` hidden field + server validation |

Editors with collection access join the presence channel when the item form mounts. The first active editor claims a soft lock; others see read-only fields until **Take over editing**. Saving after a remote change prompts **Reload** or **Save anyway**; matching `expected_updated_at` is required unless the user confirms.

## Authorization

Channel auth: `routes/channels.php` — active user, `can-show-collections`, item exists, `CollectionPermissionEnforcer::isItemReadable`. Same read gate as opening the item editor.

## Manual test checklist

- [ ] Two browsers, same item: each sees the other’s avatar in the form header.
- [ ] Second browser: fields read-only until takeover; takeover restores edit.
- [ ] Browser A saves; browser B save shows stale dialog (with Reverb) or validation error (without).
- [ ] User without collection access cannot authorize the presence channel (403 on `/broadcasting/auth`).

## Code map

| Area            | Location                                                                                               |
| --------------- | ------------------------------------------------------------------------------------------------------ |
| Channel         | `routes/channels.php` (`collection-item.{collectionId}.{itemId}`)                                      |
| Client presence | `resources/js/lib/item-presence.ts`, `hooks/use-item-presence.ts`                                      |
| UI              | `components/collections/item-editing-presence.tsx`, `pages/collections/items/form.tsx`                 |
| Stale guard     | `ItemController::update` + `expected_updated_at` on `UpdateCollectionItemRequest`                      |
| Tests           | `tests/Feature/Collections/CollectionItemPresenceChannelTest.php`, `CollectionItemStaleUpdateTest.php` |
