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:
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:
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:
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:
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-keyand 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
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:
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:
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
ignoreoption 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
attachFetchMonitorwithignoreif third-party scripts on the page send data you do not want in the log.