In ICanUp, I ran into a strange bug: after an update arrived through Laravel Reverb, the entity itself changed correctly, but canEdit, canDelete, and isOwn disappeared from the interface.
Everything worked before the event. Edit and delete actions were displayed correctly and ownership was known. Then a WebSocket event arrived, the object was updated, and part of its state vanished.
The problem was not Reverb and it was not Laravel authorization.
I had mixed two different data contracts: a complete resource prepared for the current user and a shared event describing an entity change.

The Initial HTTP Resource Contained More Than Model Fields
When the page loaded through a normal HTTP request, the server already knew the current user.
The resource could therefore return both entity data and derived fields:
{ "id": 42, "body": "Updated text", "authorId": 7, "canEdit": true, "canDelete": true, "isOwn": true}canEdit, canDelete, and isOwn are not ordinary database columns on the entity.
They depend on the request context and on the user currently viewing the page.
The Same Entity Can Produce Different Values for Another User
Entity 42 User A:canEdit = truecanDelete = trueisOwn = true User B:canEdit = falsecanDelete = falseisOwn = falseThis distinction matters.
Those fields describe not only the record itself but also the relationship between the current user and that record.
A Reverb Event Solves a Different Problem
When an entity changes, a realtime event needs to notify subscribed clients about the change.
Shared entity data may be enough:
{ "id": 42, "body": "New text", "updatedAt": "2026-09-18T07:15:00+03:00"}This event has no canEdit, canDelete, or isOwn.
That is not automatically a bug.
A shared event may be delivered to multiple users whose permissions are different.
The Bug Was Replacing the Entire Object
The critical step happened on the client.
After receiving an event, code similar to this replaced the existing object:
const index = items.findIndex( item => item.id === event.item.id); items[index] = event.item;The previous object is discarded completely.
Every field missing from event.item disappears with it.
Before: { id: 42, body: "...", canEdit: true, canDelete: true, isOwn: true} Reverb event: { id: 42, body: "changed"}This Did Not Mean Server Permissions Had Changed
This can initially send debugging in the wrong direction.
When an Edit button disappears, it is natural to inspect policies, permissions, or WebSocket channel authorization.
In this case, however, the server-side permission had not disappeared.
The client simply lost the field it had previously used to render the action.
An HTTP Resource and a Reverb Event Have Different Contexts
HTTP resource | Reverb event |
|---|---|
Built for a specific request | Describes a change |
Knows the current user | May be shared by many users |
Can include derived permissions | Should not guess every recipient permission |
Suitable for initial page state | Suitable for propagating changed data |
The same model does not imply the same contract for an HTTP response and a WebSocket event.
Adding canEdit to a Shared Event Can Also Be Wrong
Once the missing fields are discovered, another simple idea is to add them to the event.
But when one event is sent to multiple users, whose canEdit value should it contain?
Same event | +--> User A: canEdit = true | +--> User B: canEdit = falseA single derived permission in a shared event can therefore be conceptually wrong.
If user-specific realtime data is required, it needs to be built in the recipient context or resolved through another mechanism.
In My Case, the Important Part Was Preserving Known User State
If an event changes only shared entity fields, updating those fields is safer than discarding the complete existing object.
items[index] = { ...items[index], ...event.item,};The event fields change while existing user-specific fields remain available:
Existing:idbodycanEditcanDeleteisOwn Event:idbody Result:idbody <- updatedcanEdit <- preservedcanDelete <- preservedA Merge Is Not a Universal Fix Either
Object merging works when the event is genuinely a partial update.
It can be wrong when omission means deliberate removal or when an entity change should cause user-specific permissions to be recalculated.
The real solution is to define the event contract instead of applying object spread everywhere.
I Separate Three Kinds of State
State type | Examples |
|---|---|
Shared entity fields |
|
Current-user projection |
|
Temporary interface state | loading, expanded, selected |
A WebSocket event should not accidentally destroy the second or third category merely because it updates the first.
UI Permissions and Real Authorization Are Different Things
canEdit and canDelete help the client render the correct interface.
They must not be the only protection for the actual operation.
Client:canEdit = true | vshow Edit button Server:request arrives | vPolicy / authorization | vallow or rejectEven if the client incorrectly displays a button, the server must independently authorize the request.
Conversely, losing the field after a Reverb event does not mean the server permission disappeared.
isOwn Is Also a Current-User Projection
isOwn may look simpler than a policy result.
It still depends on who is viewing the record:
'isOwn' => $request->user()?->id === $model->user_id,It is therefore not a universal entity property that can be calculated once and placed into one shared event for every subscriber.
Sometimes the Correct Response Is to Fetch the Resource Again
Some entity changes can make the previous user-specific projection stale.
Ownership, status, or another policy input may have changed.
In that situation, blindly preserving the previous canEdit value can also be wrong.
Realtime event | vDoes the change affect user-specific projection? | +-- no -> merge changed shared fields | +-- yes -> refresh canonical user-specific resourceDifferent Channels Can Serve Different Contexts
Another option is a user-specific channel where data is created in the context of one recipient.
That is a different contract and should not be confused with a shared entity event.
Shared entity event:entity.updated User-specific event:user.42.entity.updatedI would introduce this only when user-specific realtime data is genuinely required, not merely to compensate for replacing an object incorrectly.
The Event Needs an Explicit Contract
After this case, I do not want every Reverb message to be interpreted as a complete resource.
An event should make it clear whether it carries a full state or only changes.
{ "entityId": 42, "changes": { "body": "New text" }}This shape is harder to accidentally treat as a complete replacement object.
Field Names Can Communicate Intent Too
If an event carries only changed fields, a name such as changes or patch communicates more accurately than item when item is not actually a complete resource.
It is a small design choice that reduces misuse on the client side.
Tests Need the HTTP Plus Reverb Sequence
Testing only the initial response or only the WebSocket handler makes this bug easy to miss.
The complete flow should be tested:
1. Load resource through HTTP2. Receive canEdit / canDelete / isOwn3. Receive Reverb event4. Apply realtime update5. Verify shared fields changed6. Verify user-specific state is still correctAn Event Triggered by Another User Is Especially Important
This scenario exposes the context difference clearly.
User A sees their own record and has isOwn = true. User B changes shared data or triggers an event received by A.
The update from B must not turn A's projection into data calculated for B or into a smaller object with those fields missing.
Check | Expected result |
|---|---|
Text changed | New value appears without reload |
| Remains correct for the current user |
| Remains correct for the current user |
| Is not overwritten by another user context |
Server authorization | Still runs independently |
Events Do Not Need to Carry Entire Eloquent Models
This case also highlights the value of small explicit event data.
Sending an entire Eloquent model with whatever relationships happen to be loaded makes the event boundary less predictable.
For realtime synchronization, I prefer explicit identifiers and fields that actually changed.
public function broadcastWith(): array{ return [ 'id' => $this->id, 'changes' => [ 'body' => $this->body, ], ];}A Revision or Timestamp Helps with Event Ordering
Once realtime updates become frequent, another issue appears: events may not always be applied in the order the interface expects.
A timestamp or another revision signal can help identify stale updates.
{ "id": 42, "updatedAt": "2026-09-18T07:15:00+03:00", "changes": { "body": "New text" }}This is a separate issue, but it belongs to the same architectural boundary: an event should be an intentional synchronization contract, not an accidental copy of a model.
What I Do Not Do After This Bug
- Do not treat every Reverb event as a complete replacement for an HTTP resource.
- Do not broadcast one
canEditvalue to users whose permissions can differ. - Do not use
canEditorcanDeleteas a replacement for server authorization. - Do not replace an entire object with partial data unless the contract explicitly says it is complete.
- Do not preserve stale derived permissions if an entity change may have invalidated them.
- Do not broadcast unnecessary Eloquent models merely for convenience.
- Do not test HTTP loading and Reverb updates independently while ignoring their sequence.
My Short Reverb Update Checklist
- Identify which fields belong to the entity and which depend on the current user.
- Do not automatically equate a WebSocket event with an HTTP resource.
- Define whether the event contains complete or partial state.
- For partial events, update only the supplied fields.
- Do not discard user-specific derived state without a reason.
- If a change affects permissions, refresh or recalculate the user-specific projection.
- Do not broadcast identical derived permissions to recipients with different authorization.
- Keep final authorization on the server.
- Use compact and explicit event data.
- Include a freshness marker when concurrent updates can occur.
- Test the complete HTTP -> Reverb -> updated interface sequence.
A realtime event tells the client what changed. It does not have to be a complete copy of a resource that was originally built for one specific user.
Conclusion
In the Laravel Reverb case in ICanUp, the symptom looked like permissions disappearing after a realtime update.
The server permissions had not disappeared. The initial HTTP resource contained canEdit, canDelete, and isOwn calculated for the current user, while the Reverb event contained a smaller set of shared entity data.
The client replaced the richer object with the event data and lost every field that the event did not contain.
After this, I separate shared entity state, current-user projections, and temporary interface state much more explicitly.
My main lesson is that an HTTP resource and a WebSocket event may describe the same entity while still living in different contexts. One should not automatically be used as a complete replacement for the other.



