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

@specscreen/backoffice-core

v0.2.0

Published

Reusable backoffice framework with auth and RBAC for React/Next.js

Readme

@specscreen/backoffice-core

A reusable backoffice framework with authentication, RBAC, and a consistent admin UI for React / Next.js.


Installation

npm install @specscreen/backoffice-core

Include the stylesheet once in your app (Next.js: app/layout.tsx, Vite: main.tsx):

import "@specscreen/backoffice-core/styles";

Quick Start

1. Implement the providers

// authProvider.ts
import type { AuthProvider } from "@specscreen/backoffice-core";

export const authProvider: AuthProvider = {
  async login({ email, password }) {
    const res = await fetch("/api/auth/login", {
      method: "POST",
      body: JSON.stringify({ email, password }),
    });
    if (!res.ok) throw new Error("Invalid credentials");
  },

  async logout() {
    await fetch("/api/auth/logout", { method: "POST" });
  },

  async checkAuth() {
    const res = await fetch("/api/auth/me");
    return res.ok;
  },

  async getUser() {
    const res = await fetch("/api/auth/me");
    if (!res.ok) return null;
    return res.json(); // { id, email, roles, permissions }
  },
};
// dataProvider.ts
import type { DataProvider } from "@specscreen/backoffice-core";

export const dataProvider: DataProvider = {
  async getList(resource, params) {
    const res = await fetch(`/api/${resource}`);
    const data = await res.json();
    return { data, total: data.length };
  },
  async getOne(resource, id) {
    const res = await fetch(`/api/${resource}/${id}`);
    return res.json();
  },
  async create(resource, data) {
    const res = await fetch(`/api/${resource}`, {
      method: "POST",
      body: JSON.stringify(data),
    });
    return res.json();
  },
  async update(resource, id, data) {
    const res = await fetch(`/api/${resource}/${id}`, {
      method: "PUT",
      body: JSON.stringify(data),
    });
    return res.json();
  },
  async delete(resource, id) {
    await fetch(`/api/${resource}/${id}`, { method: "DELETE" });
  },
};

2. Define your config

// config.ts
import { Users, FileText, Settings } from "lucide-react";
import type { BackofficeConfig } from "@specscreen/backoffice-core";

export const config: BackofficeConfig = {
  appName: "My Backoffice",

  sidebarGroups: [
    {
      label: "Platform",
      resources: [
        {
          name: "users",
          label: "Users",
          path: "/users",
          icon: Users,
          list: UsersPage,
          meta: {
            requiredPermissions: ["users.read"],
          },
        },
        {
          name: "posts",
          label: "Posts",
          path: "/posts",
          icon: FileText,
          list: PostsPage,
          meta: {
            requiredRoles: ["editor", "admin"],
          },
        },
      ],
    },
  ],

  sidebarFooterLinks: [
    // Navigates to a route
    { label: "Analytics", path: "/analytics", icon: BarChart },
    // Opens a modal inline — no route change
    { label: "Settings", icon: Settings, onClick: () => setSettingsOpen(true) },
  ],
};

3. Mount the app

Next.js App Router (app/layout.tsx)

import {
  BackofficeApp,
  type BackofficeAppProps,
} from "@specscreen/backoffice-core";
import "@specscreen/backoffice-core/styles";
import Link from "next/link";
import { usePathname } from "next/navigation";
import { authProvider } from "@/lib/authProvider";
import { dataProvider } from "@/lib/dataProvider";
import { config } from "@/lib/config";

// Adapter: wraps Next.js <Link> into the framework's NavLink shape
const NavLink = ({ href, children, className }: any) => (
  <Link href={href} className={className}>
    {children}
  </Link>
);

export default function RootLayout({
  children,
}: {
  children: React.ReactNode;
}) {
  const currentPath = usePathname();

  return (
    <html lang="en">
      <body>
        <BackofficeApp
          config={config}
          authProvider={authProvider}
          dataProvider={dataProvider}
          NavLink={NavLink}
          currentPath={currentPath}
        >
          {children}
        </BackofficeApp>
      </body>
    </html>
  );
}

Vite / React Router

import { BackofficeApp } from "@specscreen/backoffice-core";
import "@specscreen/backoffice-core/styles";
import { Link, useLocation, Outlet } from "react-router-dom";
import { authProvider } from "./lib/authProvider";
import { dataProvider } from "./lib/dataProvider";
import { config } from "./lib/config";

const NavLink = ({ href, children, className }: any) => (
  <Link to={href} className={className}>
    {children}
  </Link>
);

export function App() {
  const { pathname } = useLocation();

  return (
    <BackofficeApp
      config={config}
      authProvider={authProvider}
      dataProvider={dataProvider}
      NavLink={NavLink}
      currentPath={pathname}
    >
      <Outlet />
    </BackofficeApp>
  );
}

Authentication

AuthProvider interface

| Method | Signature | Description | | ----------- | --------------------------------------- | ----------------------------------------------- | | login | ({ email, password }) → Promise<void> | Throw on failure | | logout | () → Promise<void> | Clear session | | checkAuth | () → Promise<boolean> | Return false (never throw) on invalid session | | getUser | () → Promise<User \| null> | Return null if unauthenticated |

Boot flow

App mount
  → checkAuth()            true → getUser() → render app
                           false → render <LoginPage />

useAuth hook

import { useAuth } from "@specscreen/backoffice-core";

function MyComponent() {
  const { user, isAuthenticated, isLoading, login, logout } = useAuth();

  return <p>Logged in as {user?.email}</p>;
}

| Property | Type | Description | | ----------------- | ----------------------------------- | ---------------------------------- | | user | User \| null | Current authenticated user | | isAuthenticated | boolean | Session is active | | isLoading | boolean | Auth boot is in progress | | error | string \| null | Last auth error message | | login | (email, password) → Promise<void> | Delegates to authProvider.login | | logout | () → Promise<void> | Delegates to authProvider.logout | | refreshUser | () → Promise<void> | Re-fetch user profile |


RBAC

Permission model

// User object returned by authProvider.getUser()
{
  id: "u1",
  email: "[email protected]",
  roles: ["admin", "editor"],
  permissions: ["users.read", "users.create", "posts.publish"]
}
  • Roles — coarse-grained team/tier ("admin", "editor")
  • Permissions — fine-grained actions ("users.delete", "posts.publish")
  • Both are optional strings — the framework never hardcodes meaning
  • Your backend is the source of truth; the UI is decoration only

usePermissions hook

import { usePermissions } from "@specscreen/backoffice-core";

function ActionsBar() {
  const { can, canAny, canAll, hasRole, roles, permissions } = usePermissions();

  return (
    <div>
      {can("users.create") && <CreateButton />}
      {canAny(["posts.edit", "posts.create"]) && <PostsToolbar />}
      {canAll(["orders.view", "orders.export"]) && <ExportButton />}
      {hasRole("admin") && <AdminSettings />}
    </div>
  );
}

| Method | Description | | ----------------------- | --------------------------------- | | can(permission) | User has this specific permission | | canAny(permissions[]) | User has at least one of these | | canAll(permissions[]) | User has all of these | | hasRole(role) | User has this role | | roles | string[] — all user roles | | permissions | string[] — all user permissions |

<Can> guard component

import { Can } from "@specscreen/backoffice-core";

// Hide when no permission (default)
<Can permission="users.delete">
  <DeleteButton />
</Can>

// Show fallback instead of hiding
<Can permission="users.create" fallback={<CreateButton disabled />}>
  <CreateButton />
</Can>

// Role-based
<Can role="admin">
  <AdminPanel />
</Can>

// Require all permissions
<Can permissions={["orders.view", "orders.export"]}>
  <ExportButton />
</Can>

Resource-level RBAC

Protect entire resources (pages + sidebar items) via meta:

{
  name: "billing",
  label: "Billing",
  path: "/billing",
  meta: {
    requiredRoles: ["admin"],          // must have at least one
    requiredPermissions: ["billing.read"], // must have all
    hideIfUnauthorized: true,          // hide from sidebar (default: true)
  }
}
  • When hideIfUnauthorized: true (default) — the item disappears from the sidebar
  • Navigating directly to the path renders <AccessDenied />

Components

<BackofficeApp>

The root component. Wraps everything with auth context, guards, and layout.

<BackofficeApp
  config={config}
  authProvider={authProvider}
  dataProvider={dataProvider} // optional
  NavLink={NavLink} // optional, pass your router's Link
  currentPath={pathname} // optional, for active sidebar item
  loginPageProps={{ logo: "/logo.svg", appName: "My App" }}
  headerActionLabel="New Record" // fallback — used when no page overrides via useHeaderActions
  onHeaderAction={() => router.push("/new")}
  onLoginSuccess={() => router.push("/dashboard")}
>
  {children}
</BackofficeApp>

useHeaderActions — page-controlled header button

Let pages declare their own header action without wiring everything through the config. The button appears while the page is mounted and clears automatically on unmount.

import { SPC_useHeaderActions } from "@specscreen/backoffice-core";

function UsersPage() {
  const [open, setOpen] = useState(false);

  SPC_useHeaderActions({
    label: "Add Team Member",
    onClick: () => setOpen(true),
  });

  return (
    <>
      <UserTable />
      {open && <AddTeamMemberModal onClose={() => setOpen(false)} />}
    </>
  );
}

The page-level context action always takes precedence over the static headerActionLabel/onHeaderAction props on <BackofficeApp>. Those props remain as a global fallback.

<AppShell>

The layout shell used internally. Use it directly only when you need full control.

<AppShell
  config={config}
  NavLink={NavLink}
  currentPath={pathname}
  pageTitle="Users"
  headerActionLabel="Invite"
  onHeaderAction={handleInvite}
>
  <UsersPage />
</AppShell>

<AuthGuard>

Redirect unauthenticated users to the login page:

<AuthGuard renderLogin={() => <MyCustomLogin />}>
  <ProtectedPage />
</AuthGuard>

<ResourceGuard>

Protect a page by resource meta:

<ResourceGuard meta={{ requiredPermissions: ["users.read"] }}>
  <UsersPage />
</ResourceGuard>

<AccessDenied>

Default access-denied page. Shown automatically by <ResourceGuard>.

<AccessDenied
  message="You need the 'admin' role to access this page."
  redirectPath="/dashboard"
  redirectLabel="Go to Dashboard"
/>

<LoadingScreen>

Shown during the auth boot phase. Override via renderLoading:

<BackofficeApp renderLoading={() => <MySpinner />} ... />

Custom login page

<BackofficeApp
  renderLogin={() => <MyCustomLoginPage />}
  ...
/>

Inside MyCustomLoginPage, use useAuth() to call login():

function MyCustomLoginPage() {
  const { login } = useAuth();

  const handleSubmit = async (email: string, password: string) => {
    await login(email, password);
  };
  // ...
}

Type reference

User

interface User {
  id: string;
  email: string;
  name?: string;
  roles?: string[];
  permissions?: string[];
}

Resource

interface Resource {
  name: string;
  label: string;
  path: string;
  icon?: ComponentType<{ className?: string }>;
  list?: ComponentType;
  create?: ComponentType;
  edit?: ComponentType;
  show?: ComponentType;
  meta?: ResourceMeta;
}

ResourceMeta

interface ResourceMeta {
  requiredRoles?: string[]; // at least one role must match
  requiredPermissions?: string[]; // all permissions must match
  hideIfUnauthorized?: boolean; // default true
}

BackofficeConfig

interface BackofficeConfig {
  appName: string;
  logo?: ComponentType<{ className?: string }> | string;
  resources?: Resource[];
  sidebarGroups?: SidebarGroup[];
  sidebarFooterLinks?: Array<{
    label: string;
    /** Navigate to this path. Omit when using `onClick`. */
    path?: string;
    icon?: ComponentType;
    /** Open a modal or trigger an action instead of navigating. */
    onClick?: () => void;
  }>;
  loginRedirect?: string;
  logoutRedirect?: string;
}

Security note

The RBAC system in this framework is purely for UI/UX. It hides and shows elements; it does not enforce security. Your backend API must validate every request independently.


Exports

// Root
export { BackofficeApp } from "@specscreen/backoffice-core";

// Hooks
export {
  useAuth,
  usePermissions,
  useHeaderActions,
} from "@specscreen/backoffice-core";

// Guards
export { AuthGuard, ResourceGuard, Can } from "@specscreen/backoffice-core";

// Layout
export { AppShell, Sidebar, SidebarToggle } from "@specscreen/backoffice-core";

// Feedback
export {
  LoginPage,
  AccessDenied,
  LoadingScreen,
} from "@specscreen/backoffice-core";

// RBAC utilities (pure functions, no React)
export {
  canAccessResource,
  evaluateCan,
  evaluateHasRole,
  evaluateCanAny,
  evaluateCanAll,
} from "@specscreen/backoffice-core";

// Types
export type {
  User,
  AuthProvider,
  AuthState,
  Resource,
  ResourceMeta,
  SidebarGroup,
  BackofficeConfig,
  DataProvider,
  HeaderAction,
} from "@specscreen/backoffice-core";

// Styles
import "@specscreen/backoffice-core/styles";