npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@econneq/rbac-layout-engine

v1.0.1

Published

Headless, fully-configurable RBAC + app-shell layout engine — Sidebar, Header, AuthGuard, RbacGuard, NotAuthorized & SessionExpired pages. Plug a config, get a production app shell.

Downloads

78

Readme

@econneq/rbac-layout-engine

Headless, fully-configurable RBAC + app-shell layout engine for React 18+. Plug a config, get a production-grade app shell — sidebar, header, drawer, route guards, and a 403/session-expired flow that all honour your role & permission graph.

┌─────────────────────────────────────────────────────────┐
│   <RbacLayoutProvider config={...}>                     │
│      <AppLayout>                                        │
│        <Sidebar>  ←  modules can contribute items       │
│        <Header>   ←  modules can contribute actions     │
│        <main>     ←  your routes (guarded)              │
│      </AppLayout>                                       │
│   </RbacLayoutProvider>                                 │
└─────────────────────────────────────────────────────────┘

The engine is truly headless: it never imports your auth library directly. You pass in a useAuthState() hook (one-liner against @econneq/auth-react, NextAuth, Clerk, Auth.js, your own Redux slice — anything) and the engine takes care of access decisions, token-expiry detection, redirects, sidebar filtering, and 403 pages.


Features

  • Headless — zero hard dependencies on any auth library. Bring your own.
  • Config-driven — one defineRbacLayoutConfig({...}) call and you're done.
  • Strict JWT validationexp/nbf checked, configurable refresh threshold, auto-redirect to a session-expired page when the token lapses.
  • Env-var control — toggle the auth and RBAC subsystems via NEXT_PUBLIC_RBAC_ENABLED / NEXT_PUBLIC_AUTH_ENABLED (configurable names). Useful for staging environments and feature gates.
  • Module registry — feature modules can register sidebar items and header actions, statically in config or dynamically via hooks at runtime.
  • Three denial strategies per item — hide (default), disable, or show (block on click). Configurable globally.
  • Permission wildcards"*" (super-admin), "billing:*" (namespace).
  • Theme system — CSS custom properties scoped under [data-rle-root], defaults to a polished dark-blue palette. Light, dark, and auto modes.
  • Responsive — desktop (sidebar + header), tablet (auto-collapsed), mobile (drawer overlay). All breakpoints configurable.
  • Zero UI dependencies — pure inline styles. No CSS-in-JS runtime, no Tailwind requirement, no Radix, no styled-components.

Install

npm install @econneq/rbac-layout-engine
# or
pnpm add @econneq/rbac-layout-engine
# or
yarn add @econneq/rbac-layout-engine

Peer dependencies (required):

npm install react react-dom

Optional peer dependencies — wire to these if you're using the Econneq auth stack:

npm install @econneq/auth-core @econneq/auth-react

The package works without them — you only need them if you want useAuthState to be a one-liner over Econneq's auth.


Quick start

// app/layout.tsx (Next.js App Router) — or your top-level component
'use client'

import {
  RbacLayoutProvider,
  AppLayout,
  defineRbacLayoutConfig,
} from '@econneq/rbac-layout-engine'
import { useAuth } from '@econneq/auth-react' // or your own auth hook
import { usePathname } from 'next/navigation'

const config = defineRbacLayoutConfig({
  appName: 'Acme Console',
  usePathname,

  auth: {
    enabled: 'env', // respect NEXT_PUBLIC_AUTH_ENABLED, default true
    useAuthState: () => {
      const a = useAuth()
      return {
        isAuthenticated: a.isAuthenticated,
        loading: a.loading,
        user: a.user,
        token: a.token,
        roles: a.user?.roles ?? [],
        permissions: a.user?.permissions ?? [],
      }
    },
    loginUrl: '/auth/login',
    sessionExpiredUrl: '/auth/session-expired',
    refreshThresholdSec: 30,
  },

  rbac: {
    enabled: 'env', // respect NEXT_PUBLIC_RBAC_ENABLED
    superRoles: ['superadmin'],
    itemDeniedStrategy: 'hide',
  },

  theme: {
    mode: 'dark', // 'dark' | 'light' | 'auto'
  },

  layout: {
    initialMenu: [
      { id: 'home',     label: 'Dashboard', href: '/' },
      { id: 'billing',  label: 'Billing',   href: '/billing',
        access: { permissions: ['billing:read'] } },
      { id: 'admin',    label: 'Admin',     href: '/admin',
        access: { roles: ['admin'] } },
    ],
  },
})

export default function RootLayout({ children }: { children: React.ReactNode }) {
  return (
    <html>
      <body>
        <RbacLayoutProvider config={config}>
          <AppLayout>{children}</AppLayout>
        </RbacLayoutProvider>
      </body>
    </html>
  )
}

That's it. You now have:

  • A dark-themed app shell with a sidebar, header, and content area.
  • An auto-collapsed sidebar on tablet, drawer on mobile.
  • Sidebar items filtered by the current user's roles/permissions.
  • Automatic redirect to /auth/login?next=/... for unauthenticated visitors.
  • Automatic redirect to /auth/session-expired when the JWT expires.
  • A 403 page when an RBAC rule denies access.

RBAC rules

Every gate-able thing (route, sidebar item, header action) accepts an AccessRule:

interface AccessRule {
  roles?: string[]
  permissions?: string[]
  mode?: 'all' | 'any' // default: 'all' (configurable globally)
  check?: (state: AuthState) => boolean  // custom predicate
}
// Need ALL of these
{ roles: ['admin'], permissions: ['users:read'] }

// Need ANY of these
{ permissions: ['billing:read', 'billing:write'], mode: 'any' }

// Wildcards — namespace match
{ permissions: ['reports:*'] } // matches reports:read, reports:write, ...

// Custom logic
{ check: (auth) => auth.user?.tenantId === 'acme' }

Super-admin bypass — anyone with a role in superRoles (default ['superadmin']) or a permission in superPermissions (default ['*']) passes every rule. Use this for a single "god-mode" account during ops.


Guards

Four guards, each composable:

import {
  AuthGuard,
  RbacGuard,
  RouteGuard,
  Protected,
} from '@econneq/rbac-layout-engine'

// Whole-page: requires login.
<AuthGuard>
  <AccountSettings />
</AuthGuard>

// Whole-page: requires login + rule.
<RouteGuard rule={{ roles: ['admin'] }}>
  <AdminPage />
</RouteGuard>

// Just the rule (skip the auth check).
<RbacGuard rule={{ permissions: ['billing:read'] }}>
  <BillingTable />
</RbacGuard>

// Inline — render nothing if denied (no 403 page).
<Protected rule={{ permissions: ['users:delete'] }}>
  <button onClick={onDelete}>Delete user</button>
</Protected>

Programmatic version:

import { useRbac, useAuthGuard } from '@econneq/rbac-layout-engine'

function DeleteButton() {
  const { can } = useRbac()
  if (!can({ permissions: ['users:delete'] })) return null
  return <button>Delete</button>
}

function MyPage() {
  const { allowed, loading, reason } = useAuthGuard({ roles: ['admin'] })
  if (loading) return <Spinner />
  if (!allowed && reason === 'unauthenticated') return <Redirect to="/login" />
  if (!allowed) return <Forbidden />
  return <AdminUI />
}

Sidebar items

Four kinds: link (default), group, divider, section.

{
  layout: {
    initialMenu: [
      // Section header (uppercase label)
      { id: 'sec-main', kind: 'section', label: 'Main' },

      // Plain link
      { id: 'home', label: 'Dashboard', href: '/', icon: <HomeIcon/> },

      // Group with collapsible children
      { id: 'reports', kind: 'group', label: 'Reports', icon: <ChartIcon/>,
        children: [
          { id: 'reports-sales', label: 'Sales', href: '/reports/sales' },
          { id: 'reports-churn', label: 'Churn', href: '/reports/churn',
            access: { permissions: ['reports:churn'] } },
        ] },

      // Visual separator
      { id: 'div-1', kind: 'divider' },

      // External link
      { id: 'docs', label: 'Docs', href: 'https://docs.acme.io', external: true },

      // Item with a badge
      { id: 'inbox', label: 'Inbox', href: '/inbox', badge: 12 },
    ],
  },
}

Active-link detection is automatic (compares pathname to href). Override with match: (pathname, item) => boolean on any link.


Module registration

Feature modules can contribute menu items and header actions without touching the root config:

// Static — declared up-front
defineRbacLayoutConfig({
  modules: [
    {
      id: 'billing-module',
      menuItems: [
        { id: 'billing', label: 'Billing', href: '/billing' },
      ],
      headerActions: [
        { id: 'cart', label: 'Cart', icon: <CartIcon/>, href: '/cart', badge: 3 },
      ],
    },
  ],
})
// Dynamic — at runtime, from inside a feature provider
import { useRegisterMenuItems } from '@econneq/rbac-layout-engine'

function BillingProvider({ children }) {
  useRegisterMenuItems('billing-module', [
    { id: 'invoices', label: 'Invoices', href: '/billing/invoices' },
  ])
  return children
}

Items unmount cleanly when their owning component unmounts.


Theming

Defaults to a polished dark-blue palette. Override per-mode or globally:

defineRbacLayoutConfig({
  theme: {
    mode: 'dark',           // 'dark' | 'light' | 'auto'
    tokens: {               // global override (wins over per-mode)
      colors: {
        primary: '#22d3ee',
        primaryForeground: '#0b1220',
      },
      radii: { md: '10px' },
      layout: { sidebarWidth: 280 },
    },
    dark: {                 // dark-mode-only overrides
      colors: { sidebarBackground: '#000' },
    },
    light: {                // light-mode-only overrides
      colors: { background: '#fafafa' },
    },
    // Optional — drive `mode` from your own theme switcher hook
    useThemeMode: () => useMyThemeStore((s) => s.mode),
  },
})

All tokens are emitted as CSS custom properties under [data-rle-root]:

[data-rle-root] {
  --rle-color-primary: #3b82f6;
  --rle-color-background: #0b1220;
  --rle-color-sidebar-background: #0d1424;
  --rle-radius-md: 8px;
  --rle-sidebar-width: 260px;
  /* ... */
}

You can override these from your own stylesheet, scoped or globally — no need to go through the config object for ad-hoc tweaks.


Environment-variable control

Both subsystems can be toggled by environment variable:

# .env
NEXT_PUBLIC_AUTH_ENABLED=true
NEXT_PUBLIC_RBAC_ENABLED=true

Set the enabled field to 'env' (the default) to consult the variable. The engine probes, in order: process.env, import.meta.env, and globalThis.__ENV__. Default variable names:

  • Auth: NEXT_PUBLIC_AUTH_ENABLED, VITE_AUTH_ENABLED, AUTH_ENABLED
  • RBAC: NEXT_PUBLIC_RBAC_ENABLED, VITE_RBAC_ENABLED, RBAC_ENABLED

Override the list per-app via envVarNames:

defineRbacLayoutConfig({
  envVarNames: {
    auth: ['MY_AUTH_FLAG'],
    rbac: ['MY_RBAC_FLAG'],
  },
})

When the variable is unset, the default is enabled. Use explicit auth.enabled: false for "public-by-default" apps.


Token expiry

For any state.token that looks like a JWT, the engine decodes the exp claim and:

  1. Compares it immediately on every auth-state change.
  2. Sets a setTimeout alarm at exp - refreshThresholdSec.
  3. Redirects to auth.sessionExpiredUrl when the timer fires.

If the token isn't a JWT, supply auth.expiresAt directly as a Unix-ms timestamp, or provide your own auth.validateToken: (token) => result.

The redirect is loop-safe — already on the session-expired or login URL, it stays put.


Customising the default pages

defineRbacLayoutConfig({
  pages: {
    notAuthorized: MyForbiddenPage,        // 403
    sessionExpired: MySessionExpiredPage,  // /auth/session-expired
    loading: MyLoadingPage,                // shown while auth.loading
  },
})

The built-in pages are themed by CSS variables. Drop them into your own chrome by reusing NotAuthorizedPage, SessionExpiredPage, LoadingPage from this package.


Responsive behaviour

| Breakpoint | Default range | Sidebar behaviour | | --- | --- | --- | | Mobile | ≤ 767px | Hidden; replaced by drawer overlay (hamburger toggle). Escape & backdrop close. | | Tablet | 768px – 1023px | Auto-collapses to icon rail unless persisted. | | Desktop | ≥ 1024px | Full sidebar; user can collapse via the toggle. |

Breakpoints are configurable via theme.tokens.layout.mobileBreakpoint and tabletBreakpoint.


Integration recipes

Without @econneq/auth-react (e.g. NextAuth)

import { useSession, signIn, signOut } from 'next-auth/react'

const config = defineRbacLayoutConfig({
  auth: {
    useAuthState: () => {
      const { data, status } = useSession()
      return {
        isAuthenticated: status === 'authenticated',
        loading: status === 'loading',
        user: data?.user,
        token: (data as any)?.accessToken ?? null,
        roles: (data?.user as any)?.roles ?? [],
        permissions: (data?.user as any)?.permissions ?? [],
      }
    },
    redirect: (url) => signIn(undefined, { callbackUrl: url }),
  },
})

Public-by-default with selective gating

const config = defineRbacLayoutConfig({
  auth: { enabled: false },
  rbac: { enabled: false },
})

// Then use RouteGuard / Protected only on the pages that need it.
<RouteGuard rule={{ roles: ['admin'] }}>
  <AdminPanel />
</RouteGuard>

Custom user menu

defineRbacLayoutConfig({
  layout: {
    renderUserMenu: ({ user, logout }) => (
      <DropdownMenu>
        <DropdownMenuTrigger>{user?.name ?? 'Account'}</DropdownMenuTrigger>
        <DropdownMenuContent>
          <DropdownMenuItem href="/profile">Profile</DropdownMenuItem>
          <DropdownMenuItem onSelect={logout}>Sign out</DropdownMenuItem>
        </DropdownMenuContent>
      </DropdownMenu>
    ),
  },
})

Custom brand block

defineRbacLayoutConfig({
  layout: {
    renderBrand: () => (
      <a href="/" style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
        <img src="/logo.svg" width={28} height={28} />
        <strong>Acme</strong>
      </a>
    ),
  },
})

Persist sidebar collapsed state

defineRbacLayoutConfig({
  layout: {
    sidebar: {
      persistKey: 'acme.sidebar.open', // → localStorage
    },
  },
})

API summary

// Provider
<RbacLayoutProvider config={...}>...</RbacLayoutProvider>

// Layout
<AppLayout>{children}</AppLayout>
<Sidebar />          // standalone, advanced
<Header />           // standalone, advanced
<MobileDrawer />     // standalone, advanced

// Guards
<AuthGuard fallback={...}>...</AuthGuard>
<RbacGuard rule={...} fallback={...}>...</RbacGuard>
<RouteGuard rule={...}>...</RouteGuard>
<Protected rule={...}>...</Protected>

// Hooks
useRbacLayout()        // full context
useRbac()              // { can, hasRole, hasPermission, ... }
useAuthState()         // raw auth state
useAuthGuard(rule)     // { allowed, loading, reason }
useSidebar()           // open/toggle/drawer controls
useLayoutConfig()      // resolved config
usePathname()          // current path

useRegisterModule({ id, menuItems, headerActions })
useRegisterMenuItems(id, items)
useRegisterHeaderActions(id, actions)

// Config helpers
defineRbacLayoutConfig({...})
resolveConfig({...})

// Token utilities
validateJwt(token, { refreshThresholdSec })
decodeJwtPayload(token)
getJwtExpiresAt(token)
isExpired(timestamp)
resolveExpiresAt(explicit, token)

License

MIT — © Econneq