Skip to main content

Devtools panel

Floating React panel for inspecting the engine - flow recorder, decision tester, metrics, policies / roles / subjects. Dev-only, tree-shaken in prod.

duck-iam ships an in-app developer-tools panel that lets you inspect the engine without leaving the page. It mounts as a floating button that opens a side panel with five sub-views: Flow, Decision, Policies, Roles, Subjects, Metrics.

import { IamDevtools } from '@gentleduck/iam/dt'
<IamDevtools engine={engine} />

Tree-shaken out by bundlers in production builds because the import sits behind a guard that checks process.env.NODE_ENV !== 'production' (or import.meta.env.PROD === false in Vite / Astro / SvelteKit).

Quick start

import { useEffect, useState } from 'react'
import { IamEngine } from '@gentleduck/iam'
import { IamMemoryAdapter } from '@gentleduck/iam/adapters/memory'
import { IamDevtools } from '@gentleduck/iam/dt'

const engine = new IamEngine({ adapter: new IamMemoryAdapter({ policies, roles }) })

export function App() {
  return (
    <>
      <YourApp />
      {process.env.NODE_ENV !== 'production' && <IamDevtools engine={engine} />}
    </>
  )
}

That single call gives you a floating button in the bottom-right. Open it, hit "Run", and you've got a live decision tester wired to your real engine.

Panel layout

PanelWhat it shows
FlowA live stream of every can / check / authorize call: subject, action, resource, the resulting decision, and the matched policy / rule. Backed by iamCreateFlowRecorder.
DecisionA request builder: pick a subject, action, resource, environment. Hit "Run" to see the decision + the rule that matched. Calls engine.explain().
PoliciesAll policies loaded into the engine. Click one to see its rules + targets.
RolesAll roles loaded into the engine, including inheritance chains and the synthetic policies they expand to.
SubjectsSubjects you've inspected, with their resolved roles + attributes.
MetricsiamCreateMetricsAggregator() snapshot: p50 / p95 / p99 + allow / deny counts.

Position & layout

IamDevtools is positional - you choose where the button lives and where the panel slides in from.

<IamDevtools
  engine={engine}
  buttonPosition="top-right"       // bottom-right (default) | bottom-left | top-right | top-left | relative
  position="left"                  // bottom (default) | top | left | right
  inset={12}                       // floating gutter in px around the panel
  initialIsOpen={false}            // pre-open the panel on mount
  hideButton                       // hide the FAB - use your own UI hook to toggle
  storagePrefix="myapp-iam-dt"     // localStorage namespace for panel size + open state
  initialPanel="metrics"           // which sub-view to start on
/>

Set embedded to render the panel inline (no FAB, no slide animation) - useful when you want the devtools as a docked tab in your admin UI rather than a floating overlay.

<IamDevtoolsInner engine={engine} embedded />

Wiring metrics

The Metrics panel reads from any IDevtoolsMetrics instance. The shipping iamCreateMetricsAggregator from @gentleduck/iam/observability/metrics matches the shape, so the two compose:

import { iamCreateMetricsAggregator } from '@gentleduck/iam/observability/metrics'
import { IamDevtools } from '@gentleduck/iam/dt'

const metrics = iamCreateMetricsAggregator()
const engine = new IamEngine({
  adapter,
  hooks: { onMetrics: metrics.record },
})

<IamDevtools engine={engine} metrics={metrics} />

See metrics aggregator for the full hook surface.

Wiring the flow recorder

The Flow panel needs an IFlowRecorder. Build one via iamCreateFlowRecorder and pass it both as the engine hook bus and as the devtools prop:

import { iamCreateFlowRecorder } from '@gentleduck/iam/dt'

const flow = iamCreateFlowRecorder({ maxEntries: 500 })

const engine = new IamEngine({
  adapter,
  hooks: {
    onAuthorizeStart: flow.recordStart,
    onAuthorizeEnd: flow.recordEnd,
  },
})

<IamDevtools engine={engine} flow={flow} />

maxEntries bounds the in-memory ring; older entries are evicted. The recorder exposes subscribe() so panels other than the bundled Flow view can render the same data.

Embedding into an existing admin UI

For when you don't want a floating overlay - export the inner component and use the three sub-panel renderers directly:

import { IamDevtoolsInner, IamDecisionInspector, IamFlowPanel, IamMetricsPanel } from '@gentleduck/iam/dt'

<Tabs>
  <Tab title="Decision"><IamDecisionInspector engine={engine} /></Tab>
  <Tab title="Flow"><IamFlowPanel flow={flow} /></Tab>
  <Tab title="Metrics"><IamMetricsPanel metrics={metrics} /></Tab>
</Tabs>

Production safety

A production build that accidentally imports @gentleduck/iam/dt won't render the panel - the entry runs isDevtoolsBlocked() first which returns true whenever a production marker is detected (NODE_ENV, import.meta.env.PROD, or an explicit window.__GENTLEDUCK_IAM_DT_DISABLED__ = true). To force-disable in any environment, set the window flag before the panel mounts.

See also