Skip to content
v0.12.0

Security

On this page

Blix is a debugger, and it captures what a debugger has to capture: full request and response bodies, request and response headers, Redux action payloads and state diffs, and realtime frames. It captures them in plaintext — the request body before your encryption interceptor runs, the response body after your decryption interceptor. That is the whole point of it, and it means the log holds whatever your traffic holds, credentials included.

attachFetchMonitor widens that to every fetch the page makes, not only your own: an analytics snippet, a chat widget or a library calling fetch under the hood is captured the same way, bodies included. Use its ignore option to keep a third party's traffic out of the log.

By default all of that is in memory only. Nothing is written to disk, and a reload starts clean.

All of it is dev-only unless you opt a build in. Capture is gated on process.env.NODE_ENV === "development" && typeof window !== "undefined", and the database is opened only when the panel mounts — see Production elimination. A build made with NEXT_PUBLIC_BLIX=true captures exactly as development does, so everything in this section applies to whoever uses that build.

What reaches disk, and when

Preserve-log is the sole gate on disk writes. It is off by default, and while it is off nothing Blix captures reaches IndexedDB — the store is actively cleared on every panel mount.

Four ways to toggle it:

WhereNote
Header buttonKeeps its icon at every width; loses its label when the panel is narrow
⋯ More actions overflow menu—
Command palette (Ctrl/⌘ K)Listed as Preserve log across reloads / Stop preserving the log
Shift+L—

All four require the panel to be mounted.

Turning it on is retroactive. The toggle does not mean "from now on". Switching it on writes every entry already sitting in the live buffer — the session you have already captured — to disk immediately, along with everything that follows. If you have just reproduced a login flow and then reach for the toggle, you have written that login flow to disk. Read that again before you assume otherwise; it is the one behaviour here that reasonably surprises people.

With preserve-log on, this is what is kept:

CapturedPersisted
HTTP entries — bodies, headers, timingsyes
Realtime framesyes
The encrypted envelope, if you call captureEncryptedyes
Panel preferences and budget totalsyes — preferences are also mirrored to localStorage under blix:<dbName>:prefs
Redux actions, payloads and diffsonly if you pin the row
Query cache rowsonly if you pin the row

One thing is written regardless of preserve-log: opening the database records its name in the origin-wide localStorage key blix:databases, which is how Databases on this origin finds it in browsers without indexedDB.databases(). It holds database names and nothing else.

When nothing is being saved

With preserve-log on, the status bar says so when Blix has nowhere to write, rather than reporting entries as saved:

Status barWhy
not saving · waiting for other tabsAnother tab on the origin has the database open with an earlier version of Blix, so it cannot be upgraded yet. Close or reload that tab: saving starts by itself, with what was captured meanwhile.
not saving · storage unavailableIndexedDB is not available here, or the database was upgraded by a newer Blix on the origin. The log is kept for this page load only.

Blocked web storage — a sandboxed iframe, strict privacy settings — does not throw either: the panel runs with default preferences. Stored preferences that are invalid or outdated fall back to their defaults field by field.

What is redacted

Exactly four header names, and nothing else:

HeaderMatch
authorizationexact, case-insensitive
cookieexact, case-insensitive
set-cookieexact, case-insensitive
x-api-keyexact, case-insensitive

The match is exact on the full header name — not a prefix, not a substring, not a pattern. Near-miss names are not covered and are written in the clear: x-auth-token and api-key are the two that most often catch people out, and proxy-authorization, x-csrf-token and x-amz-security-token are equally uncovered. If your auth travels in a header that is not one of the four above, it is captured verbatim.

The same four are masked whichever client made the request, and whatever form the headers were passed in: AxiosHeaders, a plain object, a Headers instance or [name, value] pairs.

Masking is partial rather than total: for a value longer than 12 characters the first 8 and last 4 survive, so you can still tell which token you sent. Shorter values are replaced outright. The Headers tab tags every masked row MASKED rather than leaving you to infer it from an ellipsis, and the stored value keeps a (masked) suffix so every export path carries the fact too.

Errors are described, never copied. A failed TanStack query or mutation, and a Redux action whose payload is an error, are stored as a described error — kind, name, code, message, status, the response body, cause and stack — and never as the error object itself. An AxiosError carries the request's config and headers as ordinary properties; none of that is read.

The four names are also masked as keys, at any depth, in data Blix copies: Redux payloads and state diffs, query and mutation data, and everything written to IndexedDB. An HTTP body is still shown as captured while it is live in the panel — a key named authorization inside one is masked only in the saved copy.

Showing Authorization in full

Blix is development-only, and a developer reading their own token on their own screen is not the risk — the token leaving the machine is. So masking of authorization, and only authorization, can be switched off: command palette → Show Authorization values in full, or ⋯ More actions → Show Authorization in full. It is a saved preference, off by default, and while it is on the status bar reads UNMASKED Authorization and every such row in the Headers tab is tagged UNMASKED.

  • It applies to requests captured after it is switched on. Earlier entries were masked when they were captured — the full value was never kept — and the panel cannot reveal them; their row says so. An axios request re-reads its headers when it settles (see Which request headers the panel shows), so for one still in flight what counts is the setting at the moment it settles.
  • The full value is held in memory only. It is never written to IndexedDB, and every export and copy path — HAR, JSON, NDJSON, CSV, Markdown, the cURL script, Copy as cURL and Copy as fetch — still carries the masked value. The row's own Copy button is the one way to take the full value.
  • Masking again discards every full value held.
  • cookie, set-cookie, x-api-key and realtime connect tokens stay masked regardless.

JWT claims

When an Authorization value is a JWT, Blix decodes it — for axios when the request settles, for fetch when it is made — and keeps only alg, sub, iss, iat and exp, never the token or its signature. The JWT chip on that row opens them, with the time left until exp, in red once it has passed. This works with masking on. The signature is not verified, and a value that is not a well-formed JWT stores no claims at all. The claims say where they were read: when a fetch call was made, from the headers an axios request settled with, or — for an axios request that never settled, or used the auth option — when it started.

alg is shown but never acted on: a missing, empty or non-string alg still yields every other claim, and no value of it withholds anything. The XML-DSig URIs .NET writes into alg — http://www.w3.org/2001/04/xmldsig-more#hmac-sha256 and its HMAC, RSA, RSA-PSS and ECDSA siblings — are shown by name, as HMAC-SHA256 (HS256), with the raw value in the tooltip; the entry and the exports keep the raw value. An alg of none, in any letter case, is flagged as an unsigned token. The claims are part of the entry, so — unlike the token — they are saved with preserve-log on and included in the JSON and NDJSON exports.

Beyond those four key names, nothing inside a body is redacted. Request bodies, response bodies, error payloads, the encrypted request/response values, Redux payloads and diffs, and realtime frames pass through a walker that truncates large values and looks for those four names only. A password, ssn or refreshToken field is captured, and with preserve-log on written to disk, exactly as it appears. Blix has no mechanism to do otherwise: it has no schema for your payloads and no way to tell a secret from any other string.

Retention and clearing

BoundValue
Records200
Total size24 MB — HTTP entries only
Per payload field512 KB
Evictionoldest first, once either bound is exceeded
Time-based expirynone

There is no TTL of any kind. A record leaves the database when 200 newer records or 24 MB of newer traffic push it out, or when you clear it yourself. On a low-traffic app with preserve-log left on, a captured token stays in the browser profile indefinitely.

Pinning does not exempt a saved record from those bounds. A pinned row already in a preserved log is kept when the log is loaded, but a pin older than the newest ~200 saved requests is evicted from storage like any other record, and does not come back after a reload.

Three ways to purge:

WhereNote
The persisted-size label in the status bar — the one reading 12 saved · 3.4 MBThe label is the control, and it is the only one that asks twice: click once to arm it, at which point it changes to Purge saved log?, and click again. It disarms itself after three seconds. Rendered only while preserve-log is on
⋯ More actions → Purge saved logDeletes on a single press, with no confirmation
Command palette (Ctrl/⌘ K) → Purge the saved logDeletes on a single press, with no confirmation

The menu and palette entries are disabled when nothing is on disk, but unlike the status-bar label they do not depend on preserve-log being on — so a log written earlier in the session can still be deleted after you have switched the toggle off.

Switching preserve-log off also clears the stored entries, so turning it off is itself a way to drop everything Blix has written.

A purge can be blocked: IndexedDB will not delete a database that another tab still holds open. The panel reports that, naming the database, instead of reporting the log purged while it is still on disk. Close the other tabs running the app and purge again.

Other projects' databases on the same origin — and the legacy nm-devtools database that 0.5.x and earlier wrote — are deleted from Databases on this origin, not by Purge.

Every path clears the captured entries; Purge additionally deletes the IndexedDB database itself. Your panel preferences survive either way — they are mirrored to localStorage, and a fresh database is re-seeded from that mirror on the next boot. Both the database and its mirror key are scoped to this project's dbName, so a purge affects only the project that ran it and cannot restore — or destroy — another project's preferences on the same origin. There is no UI or API for clearing them, and no programmatic API for purging either.

Threat model

IndexedDB is scoped per origin, not per app, and it is not encrypted at rest. Any script running on that origin can read Blix's database — including browser extension content scripts with access to the origin. Whatever you capture is readable by whatever you have installed.

A per-project dbName does not change that. It keeps projects from mixing their logs; it does not isolate them. The panel itself can open another project's database on the same origin — Peek in Databases on this origin lists its newest URLs, methods and statuses — and so can any other script there.

Export and copy move captured data out of the browser entirely:

PathCarries
HAR exportDecrypted request and response bodies
JSON / NDJSON exportEverything captured — frames, Redux diffs, timings
Copy as cURL / Copy as fetchHeaders and bodies, with the four redacted headers masked — so the output is not a working request

HAR is the one to watch. It is a plain JSON file carrying your decrypted bodies, and it is the artifact most likely to end up attached to a ticket.

If you handle sensitive data

  • Leave preserve-log off unless you actively need it. It is off by default. In-memory capture already gives you the entire panel; the toggle buys you nothing but survival across a reload.
  • Clear the log after any session that captured an auth flow — switching preserve-log off is enough; Purge additionally deletes the database, though neither removes your panel preferences.
  • Use the Redux ignore option for action types that carry credentials or personal data, so they are never captured in the first place. See Redux.
  • Treat an exported HAR as a credential-bearing file. Do not attach one to a public issue, and do not commit one.
  • After upgrading from 0.5.x or earlier, delete nm-devtools. Blix no longer reads or writes that database, and it does not delete it for you: anything it holds stays on disk until you remove it from Databases on this origin.
  • Scope attachFetchMonitor with ignore if third-party scripts on the page send data you do not want in the log.
Edit this page on GitHubLast updated October 16, 2026
Docs