Mounting the panel
On this page
Render <Blix /> exactly once. Mounting it more than once gives you
duplicate panels reading the same log.
Mount it from a small client component of its own, and import that component — and nothing else Blix-related — from your layout:
// app/BlixMount.tsx"use client"; import { Blix } from "@hakam-aldeen-kh/blix";import { apiClient } from "@/src/network/axios";import { store } from "@/src/store"; // This file is the client boundary on purpose. `apiClient` and `store` are// module-scope singletons that build themselves during module evaluation —// importing them from a Server Component pulls axios, cookie access, your// encryption service and any "use client" helpers they touch into the RSC// module graph, which fails the production build. Importing them here keeps// that evaluation on the client side of the boundary.export default function BlixMount() { if ( process.env.NODE_ENV !== "development" && process.env.NEXT_PUBLIC_BLIX !== "true" ) return null; return <Blix store={store} apiClient={apiClient} dbName="my-app" />;}// app/layout.tsx — stays a Server Component; imports only the mountimport BlixMount from "./BlixMount"; export default function RootLayout({ children }: { children: React.ReactNode }) { return ( <html lang="en"> <body> {children} <BlixMount /> </body> </html> );}The client boundary is a leaf. It does not make your layout or your children client components — only this one file and what it imports. There is no bundle cost to isolating it this way, and it is what keeps your store and HTTP client out of the server graph.
<Blix /> itself carries a "use client" directive and can be rendered from a
server component directly. That is not the reason for the wrapper file: the
directive governs Blix's own module, not the modules you import alongside it.
Importing store and apiClient into app/layout.tsx is what breaks the
build, and it breaks it with an opaque module-resolution error from
next build, far from the mount site.
The directive on the root entry also means everything re-exported from
@hakam-aldeen-kh/blix — capture functions included — is inside that client
boundary. That is the reason for the split entry point: import capture from
@hakam-aldeen-kh/blix/capture in any module that runs on the server. See
Entry points.
Props — all optional
store and apiClient are structurally typed — they need
getState/subscribe/dispatch and request respectively. A redux-toolkit
store and an axios instance satisfy them as-is. (The interfaces are named
StoreLike and HttpClientLike in the source, but they are not exported from
the package; only Blix and BlixProps are. You never need to name them —
structural typing means you just pass your store and client.)
Passing neither still gives you a fully working capture log; you only lose the two features that need a live handle on the app.
Finding your way around
The panel is one header, one rail and two panes.
The rail on the left is the four sources — Network, Realtime, Redux,
Query. They are not filters over one table: each has its own columns and its
own notion of a row, so switching source switches the whole view. 1–4 jump
between them, and each keeps its own selection, so stepping to Redux and back
returns you to the request you were reading. Each source has its own icon and
colour — Network blue, Realtime teal, Redux violet, Query orange — and, once
something in it has failed, a red count behind a square marker beside its
total; the number key that selects it is in its tooltip. Below the sources it
carries the session totals; collapsed, it keeps the sources and their counts
and drops the totals, since four numbers without their labels are four
unattributed numbers.
The header is the session, not the entry: whether capture is running, what
is being filtered out, and where the panel lives. It starts with the Blix
mark, which says two of those at a glance: the green blade is the edge the
panel is docked to (bottom for a floating panel), and the red dot in the
centre is there while capture is running. Beside it, the capture chip says the
same in words — RECORDING with a red dot, or PAUSED with an amber
square — and pauses or resumes capture when clicked. While a filter is on, the
field carries its own count — 8 of 56 — and a clear button, so you never
have to read the status bar to know one is applied. .* widens the search to
request and response bodies.
Each row starts with a status dot — round when the request worked, square and red when it failed, hollow while it is still in flight — so the outcome reads by shape as well as colour. Failing rows are tinted red, and stay red when selected; any other selected row gets a green edge. The bars on the right are drawn in one neutral colour so they compare as lengths: green for the selected row, red for failures.
The red bin beside Export clears the source you are reading, and only
that one: emptying Redux leaves Network alone. Pinned entries survive it, and
it is disabled when the source has nothing left to clear. To empty every
source at once, use Clear log in the overflow menu, or ⇧C.
The launcher badge summarizes the log while the panel is closed. It shows one count per source that has captured anything — Network, Realtime, Redux, Query, each with its rail icon — and, in red beside a count, how many of that source's entries failed. The mark at its left carries the same state as the header's: its green blade is the edge the panel will open on, and the red dot is there while capture is running — gone while paused, pulsing while something is in flight. Failures never show on the mark; they are the red counts.
Dragging the launcher shows the four corners it can go to, each drawn as the slot it would land in; the one under the pointer is outlined in green.
The panel opens floating the first time. From the header it can be docked to any edge instead; the choice, and each dock's size, is saved per project.
A docked panel is there so the app can be used while the log is read, so it stays put; a floating one is in the way of whatever is under it, so a press elsewhere dismisses it. In every mode a press outside still closes an open menu or the command palette.
Top and bottom docks respect the safe-area insets. When the list and the detail are stacked and the panel is too short for both, it shows one at a time: picking a row opens the detail, and Back returns to the list.
Docked panels reserve their side with inline padding on <html>, removed
again — and whatever inline value was there before restored — when the panel
closes, floats, moves to another side or unmounts. The same numbers are
published as four custom properties on <html>:
Padding moves what is in the page's flow. An element with position: fixed
or sticky is positioned against the viewport, so it is not pushed
automatically — a fixed header still spans the full width, and ends up under a
side dock. Opt those elements in with the variables:
.app-header { left: var(--blix-dock-left, 0px); right: var(--blix-dock-right, 0px); top: var(--blix-dock-top, 0px); }.chat-launcher { bottom: calc(16px + var(--blix-dock-bottom, 0px)); }The fallback matters: the properties exist only while <Blix /> is mounted,
which in production it is not. The same goes for a layout sized with 100vh
or 100vw — those ignore padding; 100% of a padded <html> does not.
Shortcuts belong to the panel. Single-key shortcuts — j/k, r,
⇧C, Space and the rest — act only while focus is inside the panel, which it
is as soon as you open it or click anywhere in it. Typing in your app, in an
input, a <select>, a rich-text editor or through an IME, never triggers
one. Combinations with Ctrl, ⌘ or Alt are left to the browser and to your app,
with two exceptions: Ctrl/⌘ + backtick opens and closes the panel from anywhere
on the page, and Ctrl/⌘ K opens the palette from inside it. Shortcuts
follow the physical key on non-Latin layouts such as Arabic — the toggle and
Ctrl/⌘ K included — so the same key works without switching layout.
Esc closes the innermost open thing — palette, menu, then the panel — and
only when focus is in the panel; an Escape meant for a dialog in your app is
not Blix's. r re-sends a read at once and asks for a second press before
re-sending a write, exactly as the Replay button does.
Ctrl/⌘ K opens the command palette, and for several things it is the only
way in — sort order, row density, dock position, the copy formats, the filter
syntax. The header spends its width on what you read constantly; everything you
reach for occasionally lives one keystroke away instead of costing a button
each. Every row shows its key binding where it has one, so the palette teaches
its own shortcuts. ? still opens the full cheatsheet.
The columns are yours. Right-click the column head — or open Columns in the overflow menu — and switch off any of Pin, Method, Size, Status, Time, Linked and Timing. The choice persists. ROUTE has no switch: it is the column that says which entry a row is, and a list of rows you cannot identify is not a list. A narrow pane drops columns on its own, worst-affordable first, so the route keeps the width it needs.
Linked events tie the sources together. When Blix can see that a query caused a request, that a request came from a query, or that one entry is a replay of another, the row grows a coloured tick and the foot of the detail pane grows a chip you can click to step straight to the other side. The relation is observed, never inferred: no tick means not known, not unrelated.
Filtering the log
Plain text in the filter field matches the URL; with .* on, it matches
request and response bodies too. On top of that the field takes
field:value tokens:
Tokens are ANDed. A leading - negates any of them — -polling,
-status:2xx — and "double quotes" keep a value with spaces together. Only
plain text reaches into bodies; a field token never does.
Viewing payloads
Every payload pane has a format switch. The choice is remembered, so you pick it once rather than per request.
Copy gives you what you're looking at: Table copies CSV, YAML copies YAML.
The switch is on every payload pane — request and response bodies, realtime frames, the encrypted envelope, Redux actions, and both Redux state views:
- State shows the live store, scoped by a slice picker. Slices the
selected action wrote to are marked with a dot, so "what does
cartlook like now" is one click rather than a hunt through a collapsed root object. - Diff keeps its
+ / − / ±rows as the default view and adds the other five alongside. Table is the natural one — a diff ispath | op | before | after— and Tree lets you open up abeforethat was an object, which the one-line rows could only ever summarise. Copying the rows view gives you a plain-text diff for a ticket, not JSON.
Right-click any request for Copy as cURL or Copy as fetch — the latter
pastes straight into the browser console, with the body as an editable object
literal rather than a pre-serialized string. The cURL snippet is quoted for
POSIX shells (bash, zsh, Git Bash); it is not safe to paste into Windows
cmd or PowerShell, which quote differently.
Exporting the log
The Export button in the header offers six formats and a scope toggle — Selected (the one entry open in the detail pane: a request, a Redux action or a query), Shown (what the current section and filters leave visible) or All. It defaults to Shown, with every count on the control, so an export says what it will contain before you pick a format. Selected is disabled until you select an entry; if it was your last choice and nothing is selected, the menu opens on Shown instead.
Captured Authorization headers are masked, so cURL and fetch output carries a
placeholder rather than a working token — use Replay for a real re-run.
Every export and snippet stays masked even with Authorization shown in full in
the panel (see What is redacted).
Themes
Fourteen themes, under the theme button in the header — it names the one currently applied:
Hovering a theme applies it to the panel behind the menu, so you can see a real payload in it before committing; moving away puts back the one you had. Arrow keys preview the same way.
The default is System: the panel reads the light/dark class off <html>
and paints Blix or Blix Light to match, re-checking whenever your app's theme
changes. (Before 0.12 these were Midnight and Daylight; both are still in the
list.) Picking a specific theme overrides that. The choice is stored with the
panel's other preferences and survives a reload.
The panel never inherits your app's styling — it portals outside every stacking context and ships its own palettes, so nothing you do to your own theme can distort it. Themes are complete rather than partial: every colour the panel paints, down to the JSON syntax highlighting and the timing bars, comes from the active theme. The two tones that carry text are held to 4.5:1 against every ground they land on, which is why a few of the ported palettes differ by a shade from the originals in the slots used for dense monospace. One tone is deliberately below it and may never hold text: it is for separators, payload punctuation and disabled glyphs.
There is no API for adding your own; a theme is 33 colour slots in
src/ui/themes/bx.ts if you are working from source. Every theme fills the
same set. A development-time check reports any theme that cannot, along with
any whose text tones fall under 4.5:1 on a ground they actually land on. Since
0.11 the report is opt-in rather than printed on every mount — set
globalThis.__BLIX_DEBUG__ = true before the panel mounts to see it.
dbName — when you need it
The panel can persist its log to IndexedDB so it survives a reload, but only when you opt in: preserve-log is off by default, and while it is off nothing is written to disk. See Security for what the toggle does and what lands there.
All of Blix's storage is scoped to the origin, not to your app. That is how
IndexedDB and localStorage both work, and it covers the captured log and
your panel preferences — dock position, theme, density, the preserve-log
toggle. Two apps served from the same origin (different ports in dev are
different origins, but path-based routing, multi-zone Next.js setups and
anything behind one reverse proxy are not) share every one of them: entries
from one project appear in the other's panel, and whichever you opened last
decides where the panel is docked.
dbName is how projects on one origin are kept apart. Give each app its own:
<Blix store={store} apiClient={apiClient} dbName="checkout" />You can also set it from the capture side, which is useful when capture starts before the panel mounts:
attachHttpMonitor(apiClient, { dbName: "checkout" });Either call must happen before the database is first opened, which the panel
does on mount. If both are set, the <Blix /> prop wins, since render runs
after module init. A call that arrives after the database is open is ignored —
Blix does not switch databases at runtime — and says so in the console rather
than failing quietly.
The name you pass is prefixed: dbName="checkout" gives you the database
blix:checkout and the preferences key blix:checkout:prefs. Passing an
already-prefixed name is fine and does not double it. Omitting dbName
entirely gives you blix:default, shared with every other app on the origin
that also omits it — and in development the panel warns once per mount when
that happens, naming the fix.
Renaming is not a migration: the old database is left where it is rather than moved or deleted, and the new one starts empty at default preferences.
Databases on this origin
The status bar always shows which database the panel is on. On the shared
default it turns amber and adds a shared tag, which is the visible form of
the console warning above — click it to open the screen below. The same screen
is in ⋯ More actions and in the command palette.
Open the command palette (Ctrl/⌘ K) → Databases on this origin to see
every Blix database the origin holds — one per project, plus nm-devtools,
the single unprefixed database that all projects shared before names were
prefixed. Each row shows an approximate size and can be deleted; the one this
panel is using is marked and is not deletable from there, since it is open —
use Purge saved log for that.
Peek on a row lists the 50 newest entries in that database — time, method, URL and status — so you can tell whose log it is before deleting it. It is a read-only snapshot: Blix opens the database, reads, and closes it again, so the list does not update and there is no detail pane, replay or export. The panel itself always stays on its own database; to work with another project's log properly, run that project and open the panel there.
The screen enumerates with indexedDB.databases(), which Firefox does not
implement. There it falls back to the databases Blix has itself opened in that
browser and labels the list as possibly incomplete.