Skip to content
v0.12.0

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:

tsxtsx
// 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" />;
}
tsxtsx
// app/layout.tsx — stays a Server Component; imports only the mount
import 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

PropEffect when omitted
storeThe State tab renders — Redux store not provided —, and Re-dispatch is disabled with the reason Redux store not provided. Everything else works.
apiClientReplay request is disabled with the reason HTTP client not provided. Everything else works.
dbNameFalls back to the shared database blix:default, and the panel warns in the console. See dbName.

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.

ModeLayoutA press outside the panel
FloatOverlays the page; drag it by the header, resize from the cornerCloses it
Bottom, right, top, leftTakes that side of the screen — the page is pushed over, not coveredLeaves it open

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

PropertyValue
--blix-dock-top, --blix-dock-right, --blix-dock-bottom, --blix-dock-leftThe docked panel's size on that side, in px; 0px on the other three. Set only while a panel is docked — absent while it is closed or floating, so always read them with a fallback

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:

csscss
.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:

TokenMatches
method:postThe method, as a substring
status:500, status:5xxAn exact status, or a status class
is:errorerror, ok, pending, aborted, pinned, replay, encrypted
type:wsThe source: http, ws, redux, query
client:fetchfetch or axios — which capture path recorded the request
url:checkoutThe URL, as a substring
larger-than:10kSize above a threshold — b, k/kb, m/mb
slower-than:500Duration above a threshold, in ms — or slower-than:1.5s
has:framesframes, initiator, error, replay, body

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.

FormatAnswers
TreeWhat's in here? Collapsible and searchable; a collapsed subtree costs one row, so it stays fast on multi-megabyte responses.
TableHow do these records compare? A grid, offered when the payload is a list of records or a keyed map. Sparse and surplus columns are hidden with a count.
JSONWhat exactly came back? Raw and syntax-coloured, with a fold caret on every object and array — plus Collapse all, which leaves the top-level keys readable and their contents folded.
YAMLWhat shape is this? Indentation instead of punctuation, and multi-line strings — stack traces, SQL — shown as text rather than escapes.
TextIt isn't JSON. An HTML error page, a CSV body, a proxy's plain-text response.

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 cart look 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 is path | op | before | after — and Tree lets you open up a before that 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.

FormatFor
ToolHARChrome DevTools, Charles, Insomnia, Postman — with the decrypted bodies. HTTP entries only.
JSONEverything captured: frames, Redux diffs, timings.
NDJSONOne entry per line — pipe it into jq.
PersonMarkdownA table plus failure bodies, copied to the clipboard for an issue, a PR or Slack.
CSVOne row per entry, no bodies — sort and count in a spreadsheet. Opens as UTF-8 in Excel.
ShellcURL scriptEvery request in order, runnable against another environment.

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:

Theme
DarkBlixGraphite — the default
MidnightDeep blue-black
CarbonTrue black, high contrast; for OLED displays
NordMuted arctic blues
Tokyo NightDeep indigo, soft neon
One DarkAtom's classic slate
MochaCatppuccin — gentle pastels
DraculaVivid purples and pinks
GruvboxWarm retro browns and amber
LightBlix LightPaper and ink — the light default
DaylightClean white
GitHubThe light theme you already read all day
LatteCatppuccin — soft pastel light
SolarWarm paper, low blue light

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:

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

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

Edit this page on GitHubLast updated October 16, 2026
Docs