skip to content

Why is a Livewire public property the wrong place for an API token or an employee's bank details, even with a checksummed snapshot?

level: middleimportance: should knowfreq 40%

answer

  1. integrity is not confidentiality
  2. wire:snapshot attribute in the HTML
  3. protected properties are not persisted
  4. #[Computed] stays out of the snapshot
  5. #[Locked] is read-only, not hidden

basics

~20 s

Every Livewire public property is serialized into the page's wire:snapshot attribute and every update response as readable JSON. The checksum only proves the snapshot was not altered; it does not hide anything, so secrets must stay server-side.

solid answer

~40 s

Livewire persists state by sending public properties to the browser: the first render embeds them as JSON in the component root's `wire:snapshot` attribute, and each update response returns a fresh snapshot. The checksum is an HMAC, so it gives integrity, not confidentiality: anyone can read the values in view-source or the network tab. So a vendor API token, an IBAN or an employee's full record in a public property is published to the user. `#[Locked]` does not help, since it only makes a value read-only. Keep secrets out of state: read them from config inside the action, load sensitive data in a `#[Computed]` method or in `render()` so only what the view prints is sent, and store an id or a model, whose snapshot entry is just class and key.

go deeper

for a junior

Remember that every public property of a Livewire component is visible in the page source, so never put tokens or personal data there.

for a middle

Explain the wire:snapshot attribute, why an HMAC gives integrity but not secrecy, and why protected properties are not persisted.

for a senior

Review components for leaked state and move sensitive data into actions, computed methods or ids, masking what the view prints.

for a principal

Set rules for what classes of data may live in client-held component state, and how code review or tooling enforces them across teams.

## Where Livewire state lives A **Livewire component** has no server-side session of its own. Between requests its state lives in the browser as a **snapshot**: a JSON object with `data` (the public property values), `memo` (id, name, path and other metadata) and a `checksum`. - On the **first render**, Livewire puts that JSON into an HTML attribute on the component's root element, `wire:snapshot`. - On every **update**, the response carries a new snapshot, and the browser sends it back with the next request. So every public property value, including nested arrays and the output of synthesizers for collections, enums and models, is plainly visible in view-source, in devtools, in the network tab and in any browser extension with page access. ## What the checksum does and does not do The checksum is an **HMAC-SHA256** of the snapshot, keyed with the application's `APP_KEY`. It lets the server detect edits to the snapshot. It is not encryption: the data is sent as-is next to the signature. | Property | Readable by the user? | Changeable by the user? | |---|---|---| | plain public property | Yes | Yes, through an update | | `#[Locked]` public property | Yes | No, the update throws | | public model property | Class (or alias) and key only | No, rebuilt from signed meta | | `protected` / `private` property | No | No, but not persisted either | | `#[Computed]` method | Only what the view renders | No | ## Why protected properties are not the answer Livewire persists **only public properties**. A protected `$apiToken` set in `mount()` is not in the snapshot, but it is also gone on the next request, because the component is rebuilt from the snapshot each time. Protected properties suit constants or values you re-derive in `boot()` on every request. ## Safer patterns 1. **Read secrets where they are used.** An action that calls a payroll provider reads `config('services.payroll.token')` inside the method; the token never becomes state. 2. **Store identity, not data.** Keep `public Employee $employee` or a locked `$employeeId`; load the bank details only in the action that needs them. 3. **Compute what the view needs.** A `#[Computed]` method runs on the server each request and is not part of the snapshot; only the HTML the template prints reaches the browser. Mask values there, printing the last four digits of an IBAN, for example. 4. **Hide class names if they matter.** A model property's snapshot entry includes its class, `App\Models\Payroll\BankAccount`, unless a `Relation::morphMap()` alias is registered. 5. **Mind arrays.** `public array $employee = $model->toArray()` publishes every attribute that `toArray()` returns, far more than the view shows. ## Before and after on a payroll screen ```php // Leaks: both values are in wire:snapshot on every response public string $providerToken; public array $bankDetails; public function mount(Employee $employee) { $this->providerToken = config('services.payroll.token'); $this->bankDetails = $employee->bankAccount->toArray(); } ``` The fixed component keeps `public Employee $employee` for identity, reads the token inside the `pay()` action, and exposes a `#[Computed]` method returning only a masked account number for the view. The browser now receives the employee's class and key, plus the four digits the template prints. ## What reviewers look for - Public properties named `*token*`, `*secret*`, `*password*`, `*iban*`, or holding `->toArray()` output. - Arrays built from `toArray()` of whole records, where the array, not the view, decides what ships. - Assumptions that `#[Locked]` or the checksum hides data. The general rule that a server-rendered page must not serialize server-only values applies to every framework; the Livewire-specific part is knowing that **public means shipped**, and that the snapshot is the vehicle.

  • Does a #[Computed] property with persist: true expose its value to the browser in Livewire?
    No. `persist: true` caches the computed result in Laravel's cache store across requests, 3600 seconds by default, instead of the snapshot. The browser still sees only what the view renders from it. The trade-off is staleness, not exposure.
  • Why can't a Livewire component just store the token in a protected property set in mount()?
    Livewire persists only public properties. The component is rebuilt from the snapshot on every request and `mount()` runs only on the first, so the protected token is null on the next click. Read the token from config inside the action instead.

A tamper-evident seal on a clear plastic bag: anyone can see what is inside, the seal only tells you whether someone opened it.

saying these in an interview costs you the question

  • The snapshot checksum encrypts the property values.
  • #[Locked] properties are hidden from the browser.
  • Protected properties keep their values between Livewire requests.
  • Only properties bound with wire:model are sent to the browser.
  • Only the values a Blade view prints are sent to the browser.