0% прочитано

Laravel Reverb: Why Realtime Updates Lost canEdit, canDelete and isOwn

In ICanUp, the initial resource contained not only entity fields but also canEdit, canDelete, and isOwn calculated for the current user. After a Laravel Reverb event, the client replaced that object with a smaller event payload and those fields disappeared. This article explains why an HTTP resource and a WebSocket event have different contexts and how to preserve user-specific derived state.

19 вересня 2026 р. 10 хв читанняLaravel

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.

If an event contains fewer fields than the initial resource, replacing the entire object can remove valid local state that the event never contained.
Laravel Reverb data flow where a full HTTP resource contains canEdit canDelete and isOwn while a smaller WebSocket event updates only shared entity fields
The initial resource contains entity data plus fields derived for the current user. A Reverb event may contain only shared entity changes. Replacing one with the other loses user-specific context.

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:

JSON - Full resource for the current user
{    "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

One entity with different user-specific projections
Entity 42 User A:canEdit   = truecanDelete = trueisOwn     = true User B:canEdit   = falsecanDelete = falseisOwn     = false

This 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:

JSON - Event data describing an entity change
{    "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:

JavaScript - Replacing the complete object with event data
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.

The fields did not become false, they disappeared with the previous object
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.

A missing canEdit field in client state is not proof that server authorization changed. It may only mean that a partial update replaced a richer object.

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?

One value cannot represent every recipient
Same event   |   +--> User A: canEdit = true   |   +--> User B: canEdit = false

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

JavaScript - Merge changed data into existing state
items[index] = {    ...items[index],    ...event.item,};

The event fields change while existing user-specific fields remain available:

Only fields supplied by the event are updated
Existing:idbodycanEditcanDeleteisOwn Event:idbody Result:idbody           <- updatedcanEdit        <- preservedcanDelete      <- preserved

A 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

id, text, status, updated timestamp

Current-user projection

canEdit, canDelete, isOwn

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.

UI hints do not replace server authorization
Client:canEdit = true    |    vshow Edit button Server:request arrives    |    vPolicy / authorization    |    vallow or reject

Even 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:

PHP - Example current-user projection
'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.

The update strategy depends on what changed
Realtime event    |    vDoes the change affect user-specific projection?    |    +-- no  -> merge changed shared fields    |    +-- yes -> refresh canonical user-specific resource

Different 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 and user-specific contexts
Shared entity event:entity.updated User-specific event:user.42.entity.updated

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

JSON - An explicit partial update
{    "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:

Regression scenario
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 correct

An 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

canEdit

Remains correct for the current user

canDelete

Remains correct for the current user

isOwn

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.

PHP - Example of compact event data
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.

JSON - Event with a freshness marker
{    "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 canEdit value to users whose permissions can differ.
  • Do not use canEdit or canDelete as 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.