Skip to content

@stratum-hq/react

@stratum-hq/react provides a React context provider and pre-built components for tenant switching, hierarchy visualization, and config/permission editing.

npm install @stratum-hq/react react react-dom

The components use plain class names (stratum-*) and read their look from optional stylesheets: a neutral base, and at most one theme with its fonts:

import "@stratum-hq/react/styles/base.css"; // neutral look; also "@stratum-hq/react/styles"
// optional: one theme, and its fonts from Google Fonts
import "@stratum-hq/react/styles/theme-ansi-strata.css";
import "@stratum-hq/react/styles/fonts-ansi-strata.css";
// or
import "@stratum-hq/react/styles/theme-bedrock.css";
import "@stratum-hq/react/styles/fonts.css";

base.css does not change the host page:

  • Every rule is in the stratum cascade layer, so any unlayered rule of yours wins over it.
  • Every custom property starts with --stratum-, and every selector matches a stratum-* element.
  • It makes no network request and loads no font.
  • It follows prefers-color-scheme. To fix the palette, set data-theme="light" or data-theme="dark" on an ancestor.
  • Its reduced-motion rule stops the motion of stratum-* elements only.

theme-ansi-strata.css is the look of stratum-hq.org: a bulletin board screen cut through the earth. It draws double-rule frames, bracketed menu keys, state as a bracketed word, and one rock band per tenant depth with the name cut into it. Its fonts are VT323 and Atkinson Hyperlegible Next.

theme-bedrock.css is the earlier Stratum identity: rock bands for tenant depth, ragged edges, grain and a display face. Bedrock is its dark palette and Daylight its light palette. Its fonts are Big Shoulders Display, Instrument Sans and Martian Mono.

Both themes follow prefers-color-scheme and data-theme the same way base.css does. The two fonts files are the only files that make a request, to fonts.googleapis.com. If your Content Security Policy blocks it, do not import them, and host the fonts yourself under the same family names.

The tokens are declared on the outermost stratum-* element, and nested components inherit them. Set a token on a component root, on TenantThemeProvider (its primaryColor sets --stratum-accent), or on your own wrapper with the class stratum-scope:

.my-admin-panel.stratum-scope {
--stratum-accent: #0055aa;
--stratum-font-body: "Inter", sans-serif;
}
Token Use
--stratum-surface-0 … --stratum-surface-3 Page, panel, hover and active surfaces
--stratum-text-primary, -secondary, -tertiary Text
--stratum-accent, -hover, -text, --stratum-on-accent Primary action and LOCKED; ink on it
--stratum-flow, --stratum-on-flow INHERITED and resolved values; ink on it
--stratum-success, --stratum-warning, --stratum-warning-fill, --stratum-on-warning, --stratum-error, --stratum-info Status colors
--stratum-focus, --stratum-rule, --stratum-border, --stratum-border-hover Focus ring, control borders, hairlines
--stratum-code-bg, --stratum-code-text Inline code
--stratum-tree-band-0 … --stratum-tree-band-4 Tenant tree row color by depth (depth 4 repeats below)
--stratum-tree-on-band-0 … --stratum-tree-on-band-4 Text on each band
--stratum-tree-indent Indent per tree level (28px, 14px on phones)
--stratum-font-body, --stratum-font-display, --stratum-font-mono Font stacks
--stratum-font-size-xs … --stratum-font-size-2xl Type scale
--stratum-space-1 … --stratum-space-12 Spacing in 4px steps (1, 2, 3, 4, 6, 12)
--stratum-radius-sm, --stratum-radius Control and panel corners (0 in both themes)
--stratum-shadow, --stratum-shadow-raised Panel and pop-over shadows (base only)
--stratum-duration-fast, -normal, -slow, --stratum-ease-out, --stratum-ease-in-out Motion
--stratum-edge Bedrock only. Set it to none to remove the ragged edges.

--stratum-accent-muted, --stratum-flow-muted, --stratum-lock, --stratum-lock-muted and the --stratum-*-bg status tints follow the tokens above.

Wrap your application in StratumProvider to enable all components and hooks:

import { StratumProvider } from "@stratum-hq/react";
function App() {
return (
<StratumProvider
controlPlaneUrl="/api/stratum"
initialTenantId="optional-default-tenant"
>
<YourApp />
</StratumProvider>
);
}

The provider manages the current tenant state and sends its requests to controlPlaneUrl.

Never pass a control-plane API key to StratumProvider from browser code, including through NEXT_PUBLIC_*, REACT_APP_* or VITE_* variables, which are bundled into the page. Point controlPlaneUrl at a route on your own server (here /api/stratum) that authenticates the user and forwards the request with the key added. stratum scaffold nextjs and stratum scaffold react generate one.

Access the current tenant context from any child component:

import { useStratum } from "@stratum-hq/react";
function Dashboard() {
const {
currentTenant, // TenantNode | null
tenantContext, // TenantContext | null
loading, // boolean
error, // Error | null
switchTenant, // (id: string) => Promise<void>
apiCall, // <T>(path, options?) => Promise<T>
toast, // { success, error, warning, info }; error(message, detail?)
} = useStratum();
if (loading) return <div>Loading...</div>;
if (!currentTenant) return <div>Select a tenant</div>;
return (
<div>
<h1>{currentTenant.name}</h1>
<p>Depth: {currentTenant.depth}</p>
<p>Strategy: {tenantContext?.isolation_strategy}</p>
<pre>{JSON.stringify(tenantContext?.resolved_config, null, 2)}</pre>
</div>
);
}

toast.error(message, detail) shows message and keeps detail, for example the raw API error, behind a Details control. Write message in plain language. Each toast is its own live region: an error toast has the alert role, and the other types have the status role.

Dropdown to select the active tenant:

import { TenantSwitcher } from "@stratum-hq/react";
function Header() {
return (
<nav>
<TenantSwitcher />
</nav>
);
}

Automatically fetches the tenant list from the control plane and calls switchTenant() on selection. Displays the current tenant name and a dropdown with all available tenants.

Hierarchical tree view of the tenant structure:

import { TenantTree } from "@stratum-hq/react";
function Sidebar() {
return <TenantTree />;
}

Displays the full tenant hierarchy with expandable nodes. Clicking a tenant calls onSelect with its id. Nodes show:

  • The tenant name, in its own case. The title holds the full name when the row truncates it.
  • A color band for the depth.
  • A badge for the isolation strategy: RLS, Schema or Database.
  • An archived marker.
  • Edit, add-child and archive buttons, when you pass onEdit, onAddChild or onArchive.

The tree follows the WAI-ARIA tree pattern. One tree item is in the tab order. ArrowUp, ArrowDown, Home and End move the focus. ArrowRight expands a tenant, then moves to its first child. ArrowLeft collapses a tenant, then moves to its parent. Enter calls onSelect. DraggableTenantTree works the same way.

Editor for tenant configuration key-value pairs:

import { ConfigEditor } from "@stratum-hq/react";
function ConfigPage() {
return <ConfigEditor />;
}

Displays the resolved config for the current tenant with:

  • Key-value editing with type-appropriate inputs
  • Lock status indicators. A key that an ancestor locked is read-only, and the row says “Locked by” and the name of the tenant that locked the key.
  • Lock control. On a key that the current tenant owns, Lock blocks overrides in descendant tenants and Unlock removes that lock. The add row has a Lock for descendants option. An edit keeps the lock and the sensitive flag of the key. An override of an inherited sensitive key, from an edit or from the add row, is saved as sensitive.
  • Inheritance source information: the name of the tenant that set each value, read from GET /api/v1/tenants/:id/ancestors. If that request fails, the editor shows the first 8 characters of the tenant ID.
  • Confirmation before Remove. The first click shows “Remove {key}?” with Yes, remove and Keep. Escape or Keep cancels. PermissionEditor uses the same confirmation for Remove, and WebhookEditor uses it for Delete.
  • JSON validation. A value that is not valid JSON is not saved. The editor shows an inline error and a Save as string button that saves the text as a string.
  • Escape cancels an inline edit.
  • Below 640px wide, each row shows as a card, with the status tag next to the key.
  • Masked sensitive values: a sensitive value inherited from an ancestor shows as “Sensitive value set by an ancestor”, never its value, unless the API key belongs to the tenant that set it
  • Add, update, and delete operations

Editor for tenant permission policies:

import { PermissionEditor } from "@stratum-hq/react";
function PermissionsPage() {
return <PermissionEditor />;
}

Displays resolved permissions with:

  • Mode selection (LOCKED / INHERITED / DELEGATED)
  • Revocation mode selection (CASCADE / SOFT / PERMANENT)
  • Source tenant tracking. The Source column shows the name of the tenant that set each policy, with the same ancestors lookup as ConfigEditor. It shows a short tenant ID when that request fails.
  • Remove only on the current tenant’s own policies, because the API deletes only those. A policy set by an ancestor has no Remove, and the row says “Set by” and the name of that tenant.
  • Confirmation before Remove, the same as in ConfigEditor: “Remove {key}?” with Yes, remove and Keep
  • Add, update, and delete operations

The apiCall function from the context is a convenience wrapper that automatically adds authentication headers:

const { apiCall } = useStratum();
// GET request
const tenants = await apiCall<TenantNode[]>("/api/v1/tenants");
// POST request
const newTenant = await apiCall<TenantNode>("/api/v1/tenants", {
method: "POST",
body: JSON.stringify({
name: "New Tenant",
slug: "new_tenant",
}),
});
import {
StratumProvider,
useStratum,
TenantSwitcher,
TenantTree,
ConfigEditor,
PermissionEditor,
} from "@stratum-hq/react";
function App() {
return (
<StratumProvider controlPlaneUrl="/api/stratum">
<Layout />
</StratumProvider>
);
}
function Layout() {
const { currentTenant, loading } = useStratum();
return (
<div style={{ display: "flex" }}>
<aside style={{ width: 240 }}>
<TenantSwitcher />
<TenantTree />
</aside>
<main style={{ flex: 1, padding: 24 }}>
{loading ? (
<p>Loading...</p>
) : !currentTenant ? (
<p>Select a tenant from the sidebar.</p>
) : (
<>
<h1>{currentTenant.name}</h1>
<h2>Configuration</h2>
<ConfigEditor />
<h2>Permissions</h2>
<PermissionEditor />
</>
)}
</main>
</div>
);
}

Use the CLI to generate React integration boilerplate:

npx @stratum-hq/cli scaffold react --out src/components

This generates:

  • stratum-provider.tsx: AppStratumProvider wrapper with your config
  • tenant-guard.tsx: PermissionGuard and ConfigGuard components
  • use-tenant.ts: usePermission(), useConfig(), useIsRootTenant() hooks