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

@nsxbet/admin-sdk

v0.8.1

Published

SDK for building NSX Admin modules with integrated shell

Readme

@nsxbet/admin-sdk

SDK for building admin modules for the NSX Admin platform.

Installation

bun add @nsxbet/admin-sdk @nsxbet/admin-ui

Peer dependencies:

# Shared with shell - use exact versions to avoid conflicts
bun add [email protected] [email protected] [email protected] [email protected] [email protected]

# Styling - use exact versions for design system consistency
bun add -D [email protected] [email protected] [email protected] [email protected]

# Build tools - flexible versions are fine
bun add -D @vitejs/plugin-react vite typescript

Why exact versions? React, React Router, and i18next are shared at runtime - mismatches cause conflicts. Tailwind/PostCSS versions ensure consistent styling with the design system.

Quick Start (CLI)

The fastest way to create a new module is using the CLI:

# Scaffold a new module
npx @nsxbet/admin-cli init --id "@admin/my-module" --title "My Module" --route "/my-module" --port 3010

# Generate a setup verification checklist
npx @nsxbet/admin-cli checklist --output CHECKLIST.md

# Validate an existing module
npx @nsxbet/admin-cli check

# Auto-fix common issues
npx @nsxbet/admin-cli check --fix

The CLI scaffolds all 17 required files (including 4 locale files) with correct configuration, dependency versions, and SDK integration. It also validates existing modules against the canonical reference.

Required for LLMs: After creating or modifying a module, run npx @nsxbet/admin-cli checklist --output CHECKLIST.md and verify every item in the generated file before considering the setup complete. The checklist is a self-contained verification guide—no CLI installation needed to consume it.

Static checklist: A ready-to-use CHECKLIST.md is also shipped inside the SDK package at node_modules/@nsxbet/admin-sdk/CHECKLIST.md. You can use it directly without installing the CLI.

Quick Start

This SDK enables you to build admin modules that integrate with the NSX Admin shell. Modules are loaded dynamically via React.lazy and share the shell's Router context.

Key concepts:

  • Modules export a default React component from spa.tsx
  • Modules share the shell's BrowserRouter - use useNavigate() from react-router-dom directly
  • Use @nsxbet/admin-ui for consistent UI components
  • Define module metadata in admin.module.json

Architecture Overview

┌─────────────────────────────────────────────────────────────────┐
│  Shell (BrowserRouter)                                          │
│  ├── TopBar, LeftNav, CommandPalette                           │
│  └── <Route path="/your-module/*">                             │
│       └── React.lazy(() => import(baseUrl/spa.js))             │
│            └── Your App component (shares Router context)       │
└─────────────────────────────────────────────────────────────────┘

Two entry points pattern:

| File | Purpose | When used | |------|---------|-----------| | src/spa.tsx | Default export of App | Shell loads this via React.lazy | | src/main.tsx | Full app with AdminShell wrapper | Local development (npm run dev) |

Complete Module Example

Below are all the files needed for a working module. Copy these and modify for your needs.

File: vite.config.ts

import { defineModuleConfig } from "@nsxbet/admin-sdk/vite";
import react from "@vitejs/plugin-react";

export default defineModuleConfig({
  port: 3003,
  plugins: [react()],
});

File: admin.module.json

Title, description, and command titles must be localized objects with all 4 locales (en-US, pt-BR, es, ro):

{
  "id": "@admin/my-module",
  "title": {
    "en-US": "My Module",
    "pt-BR": "Meu Módulo",
    "es": "Mi Módulo",
    "ro": "Modulul Meu"
  },
  "description": {
    "en-US": "Description of what this module does",
    "pt-BR": "Descrição do que este módulo faz",
    "es": "Descripción de lo que hace este módulo",
    "ro": "Descrierea a ceea ce face acest modul"
  },
  "category": "Tools",
  "icon": "clipboard-list",
  "routeBase": "/my-module",
  "keywords": ["my", "module", "example"],
  "navigation": {
    "style": "stacked",
    "sections": [
      { "id": "general", "label": { "en-US": "General", "pt-BR": "Geral", "es": "General", "ro": "General" } }
    ]
  },
  "commands": [
    {
      "id": "list",
      "title": {
        "en-US": "List Items",
        "pt-BR": "Listar Itens",
        "es": "Listar Elementos",
        "ro": "Lista Elemente"
      },
      "route": "/my-module/list",
      "icon": "file-text"
    },
    {
      "id": "new",
      "title": {
        "en-US": "New Item",
        "pt-BR": "Novo Item",
        "es": "Nuevo Elemento",
        "ro": "Element Nou"
      },
      "route": "/my-module/new",
      "icon": "plus"
    }
  ],
  "permissions": {
    "view": ["admin.mymodule.view"],
    "edit": ["admin.mymodule.edit"],
    "delete": ["admin.mymodule.delete"]
  },
  "owners": {
    "team": "Platform",
    "supportChannel": "#platform-support"
  }
}

File: src/spa.tsx

/**
 * Module entry point for shell mode.
 * The module shares the shell's Router context.
 */
import { App } from "./App";

export default App;

File: src/main.tsx

import React from "react";
import ReactDOM from "react-dom/client";
import {
  AdminShell,
  initI18n,
  createInMemoryAuthClient,
  createMockUsersFromRoles,
} from "@nsxbet/admin-sdk";
import type { AdminModuleManifest } from "@nsxbet/admin-sdk";
import { App } from "./App";
import manifest from "../admin.module.json";

import "./index.css";

// Initialize i18n BEFORE shell renders.
// The Vite plugin (admin-module-i18n) auto-injects module translation registration into spa.tsx and main.tsx.
initI18n();

const moduleManifest = manifest as AdminModuleManifest;

const mockUsers = createMockUsersFromRoles({
  admin: ["admin.mymodule.view", "admin.mymodule.edit", "admin.mymodule.delete"],
  editor: ["admin.mymodule.view", "admin.mymodule.edit"],
  viewer: ["admin.mymodule.view"],
  noAccess: [],
});

const authClient = createInMemoryAuthClient({ users: mockUsers });

ReactDOM.createRoot(document.getElementById("root")!).render(
  <React.StrictMode>
    <AdminShell
      modules={[moduleManifest]}
      authClient={authClient}
    >
      <App />
    </AdminShell>
  </React.StrictMode>
);

Module standalone dev uses the in-memory client and UserSelector, which fetches JWTs via GET {gateway}/auth/token. Do not pass bff here — BFF / Okta cookie auth is for the platform shell in production, not for local module dev servers.

File: src/App.tsx

import { Routes, Route, Navigate } from "react-router-dom";
import { ItemList } from "./ItemList";
import { NewItem } from "./NewItem";

export function App() {
  return (
    <Routes>
      {/* Redirect root to list */}
      <Route path="/" element={<Navigate to="list" replace />} />
      <Route path="list" element={<ItemList />} />
      <Route path="new" element={<NewItem />} />
    </Routes>
  );
}

File: src/ItemList.tsx

import { useEffect } from "react";
import { useNavigate } from "react-router-dom";
import { useAuth, usePlatformAPI, useTelemetry } from "@nsxbet/admin-sdk";
import { Button, Card, CardContent, Badge } from "@nsxbet/admin-ui";

export function ItemList() {
  const navigate = useNavigate();
  const { hasPermission } = useAuth();
  const { api } = usePlatformAPI();
  const { track } = useTelemetry();

  const canEdit = hasPermission("admin.mymodule.edit");

  useEffect(() => {
    // Set breadcrumbs
    api?.nav.setBreadcrumbs([
      { label: "My Module" },
      { label: "List" }
    ]);
    // Track page view
    track("page.viewed", { page: "item_list" });
  }, [api, track]);

  return (
    <div className="p-6 max-w-5xl mx-auto">
      <div className="mb-6 flex items-center justify-between">
        <h1 className="text-3xl font-bold">Items</h1>
        {canEdit && (
          <Button onClick={() => navigate("/my-module/new")}>
            New Item
          </Button>
        )}
      </div>

      <Card>
        <CardContent className="p-4">
          <p className="text-muted-foreground">No items yet.</p>
        </CardContent>
      </Card>
    </div>
  );
}

File: src/index.css

@import "@nsxbet/admin-ui/styles.css";

@tailwind base;
@tailwind components;
@tailwind utilities;

Directory: src/i18n/ (required for translations)

All 4 locale files are required (en-US, pt-BR, es, ro). The Vite plugin validates this at build time.

Structure:

src/i18n/
  locales/
    en-US.json
    pt-BR.json
    es.json
    ro.json

No manual registration needed. The admin-module-i18n Vite plugin (included in defineModuleConfig) auto-injects registerModuleTranslations into spa.tsx and main.tsx at build/serve time. Just create the locale files.

Example src/i18n/locales/en-US.json (content translations only — nav titles live in admin.module.json):

{
  "list": {
    "title": "All Items",
    "noItems": "No items found.",
    "create": "Create Item"
  },
  "form": {
    "titleLabel": "Title",
    "titlePlaceholder": "Enter title",
    "cancel": "Cancel",
    "submit": "Submit"
  }
}

Use translations in components:

import { useI18n } from "@nsxbet/admin-sdk";

function MyComponent() {
  const { t } = useI18n("my-module");
  
  return <h1>{t("list.title")}</h1>;
}

The namespace is derived from your module id (e.g. @admin/my-modulemy-module).

File: tailwind.config.js

Use withAdminSdk which automatically includes the UI preset and SDK/UI content paths:

import { withAdminSdk } from "@nsxbet/admin-sdk/tailwind";

/** @type {import('tailwindcss').Config} */
export default withAdminSdk({
  content: ["./index.html", "./src/**/*.{ts,tsx}"],
});

File: package.json

{
  "name": "@admin/my-module",
  "version": "1.0.0",
  "type": "module",
  "scripts": {
    "dev": "vite",
    "build": "tsc && vite build",
    "preview": "vite preview"
  },
  "dependencies": {
    "@nsxbet/admin-sdk": "latest",
    "@nsxbet/admin-ui": "latest",
    "react": "18.2.0",
    "react-dom": "18.2.0",
    "react-router-dom": "6.20.1",
    "i18next": "25.0.0",
    "react-i18next": "16.0.0"
  },
  "devDependencies": {
    "@types/react": "^18.2.0",
    "@types/react-dom": "^18.2.0",
    "@vitejs/plugin-react": "^4.2.0",
    "autoprefixer": "10.4.16",
    "postcss": "8.4.32",
    "tailwindcss": "3.4.0",
    "tailwindcss-animate": "1.0.7",
    "typescript": "^5.2.0",
    "vite": "^5.0.0"
  }
}

Note: Exact versions for runtime deps (react, i18next) avoid conflicts with the shell. Exact versions for styling (tailwindcss, postcss) ensure design system consistency.

Note: module.manifest.json is generated automatically by defineModuleConfig at build time. No manual copy step is needed.

File: tsconfig.json

{
  "compilerOptions": {
    "target": "ES2020",
    "useDefineForClassFields": true,
    "lib": ["ES2020", "DOM", "DOM.Iterable"],
    "module": "ESNext",
    "skipLibCheck": true,
    "moduleResolution": "bundler",
    "allowImportingTsExtensions": true,
    "resolveJsonModule": true,
    "isolatedModules": true,
    "noEmit": true,
    "jsx": "react-jsx",
    "strict": true,
    "noUnusedLocals": true,
    "noUnusedParameters": true,
    "noFallthroughCasesInSwitch": true
  },
  "include": ["src", "admin.module.json"]
}

File: index.html

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>My Module - Standalone</title>
  </head>
  <body>
    <div id="root"></div>
    <script type="module" src="/src/main.tsx"></script>
  </body>
</html>

File: postcss.config.js

export default {
  plugins: {
    tailwindcss: {},
    autoprefixer: {},
  },
};

File: src/globals.d.ts

TypeScript declarations for environment variables and platform API:

/// <reference types="vite/client" />

declare global {
  interface ImportMetaEnv {
    readonly VITE_ALLOWED_MODULE_ORIGINS?: string;
  }

  interface ImportMeta {
    readonly env: ImportMetaEnv;
  }

  interface Window {
    __ADMIN_PLATFORM_API__?: import("@nsxbet/admin-sdk").PlatformAPI;
    __ENV__?: {
      ENVIRONMENT: string;
    };
  }
}

export {};

File: tsconfig.node.json

Separate TypeScript config for Vite configuration file:

{
  "compilerOptions": {
    "composite": true,
    "skipLibCheck": true,
    "module": "ESNext",
    "moduleResolution": "bundler",
    "allowSyntheticDefaultImports": true,
    "strict": true
  },
  "include": ["vite.config.ts"]
}

File: .env.local (optional)

Environment configuration for development:

# Use mock authentication with user selector (default: true)
MOCK_AUTH=true

# Module URL allowlist (shell mode only). Comma-separated patterns.
# Supports *.domain wildcard (e.g. *.nsx.dev matches modules.nsx.dev, nsx.dev).
# In dev mode, localhost and 127.0.0.1 are always allowed.
# VITE_ALLOWED_MODULE_ORIGINS=*.nsx.dev,*.nsx.services

Manifest Schema (admin.module.json)

| Field | Type | Required | Description | |-------|------|----------|-------------| | id | string | ✅ | Unique identifier (e.g., @admin/tasks) | | title | string | ✅ | Human-readable title | | routeBase | string | ✅ | Base route path (must start with /) | | description | string | | What the module does | | category | string | | Navigation grouping | | icon | string | | Lucide icon name in kebab-case | | keywords | string[] | | Search keywords | | navigation | object | | Navigation config (style, sections) | | commands | Command[] | | Available actions | | permissions | object | | Permission configuration | | owners | object | | Team ownership info |

Command Schema

| Field | Type | Required | Description | |-------|------|----------|-------------| | id | string | ✅ | Unique command identifier | | title | string | ✅ | Command title | | route | string | ✅ | Full route path | | icon | string | | Lucide icon name | | keywords | string[] | | Search keywords | | section | string | | Section ID for stacked navigation grouping |

Stacked Navigation

Modules with many commands can use stacked navigation for a dedicated sidebar panel with sectioned command grouping.

Enable stacked navigation by adding a navigation field to admin.module.json:

{
  "navigation": {
    "style": "stacked",
    "sections": [
      {
        "id": "general",
        "label": { "en-US": "General", "pt-BR": "Geral", "es": "General", "ro": "General" }
      },
      {
        "id": "advanced",
        "label": { "en-US": "Advanced", "pt-BR": "Avançado", "es": "Avanzado", "ro": "Avansat" }
      }
    ]
  },
  "commands": [
    { "id": "list", "title": {...}, "route": "/mod/list", "section": "general" },
    { "id": "settings", "title": {...}, "route": "/mod/settings", "section": "advanced" }
  ]
}
  • style: "stacked" enables the dedicated panel; "collapsible" (default) keeps the inline expand behavior
  • sections: Array of section definitions with id and localized label
  • Commands reference sections via the section field matching a section id
  • Commands without a section appear in an implicit top-level group
  • The stacked panel is URL-driven: navigating to the module's routeBase activates it

Icon Names

Use Lucide icon names in kebab-case:

| Category | Icons | |----------|-------| | Navigation | home, settings, menu, search, chevron-right | | Actions | plus, edit, trash, copy, check | | Content | file, file-text, folder, clipboard-list | | Users | user, users | | Status | alert-circle, info, ban, lock |

Vite Configuration

The SDK provides two ways to configure Vite for admin modules:

  1. defineModuleConfig() — returns a complete Vite config. Best for new modules with no existing Vite setup.
  2. adminModule() — returns a Plugin[] you spread into an existing config. Best for Lovable projects or any existing Vite config.

Both produce identical build artifacts. Both support @vitejs/plugin-react and @vitejs/plugin-react-swc.

Note: module.manifest.json is generated automatically at build time by both defineModuleConfig and adminModule. No manual cp admin.module.json dist/ step is needed.

defineModuleConfig() — Full Config

import { defineModuleConfig } from "@nsxbet/admin-sdk/vite";
import react from "@vitejs/plugin-react";

export default defineModuleConfig({
  port: 3003,
  plugins: [react()], // REQUIRED: you must add a React plugin
});

Important: Each module must use a unique port. The default is 8080 (matching Lovable's default). Standard assignments:

  • Shell: 3000
  • API: 4000
  • Modules: 3002, 3003, 3004, etc.

defineModuleConfig Options

| Option | Type | Default | Description | |--------|------|---------|-------------| | port | number | 8080 | Dev server port | | entry | string | "./src/spa.tsx" | Entry file path | | outDir | string | "dist" | Output directory | | plugins | Plugin[] | [] | Additional Vite plugins | | additionalExternals | string[] | [] | Extra externals | | overrides | object | {} | Override any Vite config |

adminModule() — Composable Plugin (Lovable / Custom Vite Config)

Use adminModule() when you already have a vite.config.ts and want to add admin module support without replacing it. This is the recommended approach for Lovable projects.

adminModule() only affects vite build — during vite dev, your app runs as a normal SPA with HMR working as expected.

import { defineConfig } from "vite";
import react from "@vitejs/plugin-react-swc";
import path from "path";
import { componentTagger } from "lovable-tagger";
import { adminModule } from "@nsxbet/admin-sdk/vite";

export default defineConfig(({ mode }) => ({
  server: {
    host: "::",
    port: 8080,
    hmr: { overlay: false },
  },
  plugins: [
    react(),
    mode === "development" && componentTagger(),
    ...adminModule(),
  ].filter(Boolean),
  resolve: {
    alias: {
      "@": path.resolve(__dirname, "./src"),
    },
  },
}));

adminModule Options

| Option | Type | Default | Description | |--------|------|---------|-------------| | entry | string | "./src/spa.tsx" | Entry file path | | outDir | string | "dist" | Output directory | | additionalExternals | string[] | [] | Extra externals | | gatewayUrl | string \| null | undefined | Admin gateway URL for env injection. undefined = auto-inject staging URL, string = inject custom URL, null = disable injection. Explicit env vars always take precedence. |

Automatic Environment Injection

adminModule() includes a plugin that serves /env.js in dev/preview and injects <script src="/env.js"> into index.html. The script sets window.__ENV__ (including ADMIN_GATEWAY_URL from your .env or a default staging URL). The SDK reads this via import { env } from "@nsxbet/admin-sdk".

Precedence order (highest to lowest):

  1. Explicit env var (.env, .env.local, shell environment)
  2. gatewayUrl option passed to adminModule() or defineModuleConfig()
  3. Default: https://admin-bff-stg.nsx.dev (staging)

Known gateway URLs:

| Environment | URL | |---|---| | Staging | https://admin-bff-stg.nsx.dev | | Homol | https://admin-bff-homol.nsx.dev | | Local | http://localhost:8080 |

Examples:

// Default: staging URL auto-injected
...adminModule()

// Custom URL
...adminModule({ gatewayUrl: "http://localhost:8080" })

// Disable injection entirely
...adminModule({ gatewayUrl: null })

Shared Externals

These dependencies are provided by the shell (do not bundle them):

["react", "react-dom", "react-router-dom", "i18next", "react-i18next"]

Getting Started in Lovable

Lovable projects come with a default vite.config.ts, index.html, and src/main.tsx entry point. To turn a Lovable project into an admin module:

  1. Install the SDK:
npm install @nsxbet/admin-sdk @nsxbet/admin-ui
  1. Add adminModule() to your existing vite.config.ts:
import { adminModule } from "@nsxbet/admin-sdk/vite";

// Inside your plugins array:
plugins: [
  react(),
  mode === "development" && componentTagger(),
  ...adminModule(),
].filter(Boolean),
  1. Create admin.module.json at the project root with your module metadata.

  2. Create src/spa.tsx — the shell entry point:

import { App } from "./App";
export default App;
  1. Update src/main.tsx — wrap your app with AdminShell for standalone dev:
import { AdminShell, createInMemoryAuthClient, createMockUsersFromRoles } from "@nsxbet/admin-sdk";
import manifest from "../admin.module.json";
import { App } from "./App";

const mockUsers = createMockUsersFromRoles({
  admin: ["admin.mymodule.view", "admin.mymodule.edit"],
  editor: ["admin.mymodule.view", "admin.mymodule.edit"],
  viewer: ["admin.mymodule.view"],
  noAccess: [],
});

const authClient = createInMemoryAuthClient({ users: mockUsers });

ReactDOM.createRoot(document.getElementById("root")!).render(
  <AdminShell modules={[manifest]} authClient={authClient}>
    <App />
  </AdminShell>
);
  1. Build: npm run build produces dist/assets/spa-[hash].js + dist/module.manifest.json

Your Lovable preview (vite dev) continues to work as before — adminModule() only changes the vite build output.

ESLint Plugin

Install @nsxbet/eslint-plugin-admin (requires ESLint 9+) to catch common mistakes at development time:

// eslint.config.js
import adminPlugin from "@nsxbet/eslint-plugin-admin";

export default [adminPlugin.configs.recommended];

This enables all recommended rules:

  • @nsxbet/no-raw-fetch (error) — flags direct fetch()/window.fetch() calls that bypass authentication. Use useFetch() instead.
  • @nsxbet/no-raw-date-format (warn) — flags direct date formatting (toLocaleDateString, toLocaleTimeString, Intl.DateTimeFormat, dayjs/moment .format(), date-fns format) that bypasses the platform timezone preference. Use <Timestamp /> or useTimestamp() instead.

SDK Hooks

useAuth()

Access authentication and permissions.

import { useAuth } from "@nsxbet/admin-sdk";

function MyComponent() {
  const { hasPermission, getUser, getAccessToken, logout } = useAuth();

  if (!hasPermission("admin.mymodule.view")) {
    return <div>Access denied</div>;
  }

  const user = getUser();
  console.log(user.email, user.displayName);
}

| Method | Returns | Description | |--------|---------|-------------| | hasPermission(perm) | boolean | Check if user has permission. Returns false during auth initialization until auth completes. | | getUser() | User | Get current user info | | getAccessToken() | Promise<string> | Get JWT token | | logout() | void | Log out user |

usePlatformAPI()

Access the shell's platform API.

import { usePlatformAPI } from "@nsxbet/admin-sdk";

function MyComponent() {
  const { api, isShellMode } = usePlatformAPI();

  useEffect(() => {
    api?.nav.setBreadcrumbs([
      { label: "My Module" },
      { label: "Current Page" }
    ]);
  }, [api]);
}

useFetch()

Make authenticated API requests.

import { useFetch } from "@nsxbet/admin-sdk";

function MyComponent() {
  const fetch = useFetch();

  const loadData = async () => {
    const response = await fetch("/api/data");
    const data = await response.json();
  };
}

useTelemetry()

Track events and errors.

import { useTelemetry } from "@nsxbet/admin-sdk";

function MyComponent() {
  const { track, trackError } = useTelemetry();

  const handleClick = () => {
    track("button.clicked", { buttonId: "save" });
  };

  const handleError = (error: Error) => {
    trackError(error, { context: "save_operation" });
  };
}

useI18n()

Manage translations and locale.

import { useI18n } from "@nsxbet/admin-sdk";

function MyComponent() {
  const { t, locale, setLocale } = useI18n();

  return (
    <div>
      <h1>{t("common.title")}</h1>
      <p>Current locale: {locale}</p>
    </div>
  );
}

useTimestamp()

Timezone-aware date formatting that respects the shell's UTC/Local preference.

import { useTimestamp, Timestamp } from "@nsxbet/admin-sdk";

function MyComponent() {
  const { mode, setMode, formatDate, timezone } = useTimestamp();

  return (
    <div>
      <p>Timezone: {timezone} ({mode})</p>
      <p>Formatted: {formatDate(new Date(), "datetime")}</p>
      <Timestamp value={new Date()} format="date" />
    </div>
  );
}

| Property | Type | Description | |----------|------|-------------| | mode | "utc" \| "local" | Current timezone mode | | setMode | (mode) => void | Change the timezone mode | | formatDate | (date, format?) => string | Format a date with current mode and locale | | timezone | string | Resolved IANA timezone string ("UTC" or browser local) |

Format presets:

| Preset | Example (UTC, en-US) | Description | |--------|----------------------|-------------| | "datetime" (default) | "Mar 16, 2026, 2:30:05 PM UTC" | Full date and time | | "date" | "Mar 16, 2026" | Date only | | "time" | "2:30:05 PM UTC" | Time only | | "relative" | "5 minutes ago" | Relative to now (timezone-independent) |

In shell mode, reads from window.__ADMIN_PLATFORM_API__.timestamp. In standalone mode, falls back to localStorage key admin-timezone-mode (default: "local").

<Timestamp /> Component

Renders a formatted date that automatically respects the shell's timezone preference.

import { Timestamp } from "@nsxbet/admin-sdk";

// Basic usage (datetime format)
<Timestamp value={new Date("2026-03-16T14:30:05Z")} />

// Date only
<Timestamp value={createdAt} format="date" />

// Relative time
<Timestamp value={updatedAt} format="relative" />

// String input (auto-parsed)
<Timestamp value="2026-03-16T14:30:05Z" format="time" />

| Prop | Type | Default | Description | |------|------|---------|-------------| | value | Date \| string | required | The date to display | | format | TimestampFormat | "datetime" | Format preset | | className | string | | CSS class for the <time> element |

Renders a semantic <time> element with a dateTime attribute. Hovering shows a tooltip with the date in the opposite timezone mode.

Migration guide:

| Before | After | |--------|-------| | date.toLocaleDateString() | <Timestamp value={date} format="date" /> | | date.toLocaleString() | <Timestamp value={date} /> | | date.toLocaleTimeString() | <Timestamp value={date} format="time" /> | | new Intl.DateTimeFormat(...).format(date) | <Timestamp value={date} /> |

useRegistryPolling()

Detect catalog changes via lightweight version polling. Used by the shell to show an "Updates available" banner without forcing a reload.

import { useRegistryPolling } from "@nsxbet/admin-sdk";

const { hasUpdates, dismiss } = useRegistryPolling({
  registryClient,
  initialVersion: catalog.version,
  interval: 60000, // poll every 60s, 0 to disable
});

| Property | Type | Description | |----------|------|-------------| | hasUpdates | boolean | true when the server version differs from the loaded version | | dismiss | () => void | Hide the banner for the current version; re-shows on newer versions |

Options:

| Option | Type | Description | |--------|------|-------------| | registryClient | RegistryClient | The registry client instance | | initialVersion | string | Version string from the initial catalog.get() response | | interval | number | undefined | Polling interval in ms. 0 or undefined disables polling |

The hook integrates the Page Visibility API — polling pauses when the tab is hidden and resumes with an immediate check when the tab becomes visible again. Network errors are silently ignored.

catalog.version()

Both HTTP and in-memory registry clients expose a catalog.version() method:

const { version, generatedAt } = await registryClient.catalog.version();

The HTTP client calls GET /api/catalog/version. The in-memory client derives a version from the local mutation state.

Environment configuration

The polling interval is resolved from REGISTRY_POLL_INTERVAL (milliseconds):

| Source | Example | |--------|---------| | Explicit registryPollInterval prop on AdminShell | Shell passes from env config | | window.__ENV__.REGISTRY_POLL_INTERVAL | Fallback for non-shell consumers | | Environment default | local: 60s, staging: 5min, production: 15min |

Set REGISTRY_POLL_INTERVAL=0 to disable polling entirely.

Navigation

Use useNavigate from react-router-dom directly:

import { useNavigate } from "react-router-dom";

function MyComponent() {
  const navigate = useNavigate();

  return (
    <Button onClick={() => navigate("/my-module/new")}>
      New Item
    </Button>
  );
}

Custom Mock Users for Development

During standalone development, the SDK uses an in-memory auth client with default mock users. You can customize these users to match your module's specific permission requirements.

Using createMockUsersFromRoles()

The simplest way to create custom mock users is with the createMockUsersFromRoles() factory:

import {
  AdminShell,
  createInMemoryAuthClient,
  createMockUsersFromRoles,
} from "@nsxbet/admin-sdk";

// Define roles for your module
const mockUsers = createMockUsersFromRoles({
  admin: [
    "admin.payments.view",
    "admin.payments.edit",
    "admin.payments.delete",
  ],
  editor: ["admin.payments.view", "admin.payments.edit"],
  viewer: ["admin.payments.view"],
  noAccess: [],
});

// Create auth client with custom users
const authClient = createInMemoryAuthClient({ users: mockUsers });

// Use in main.tsx
ReactDOM.createRoot(document.getElementById("root")!).render(
  <AdminShell authClient={authClient} modules={[manifest]}>
    <App />
  </AdminShell>
);

This creates 4 standard user types with your custom roles:

  • Admin User - Full access (all roles in admin array)
  • Editor User - View/edit access (roles in editor array)
  • Viewer User - View-only access (roles in viewer array)
  • No Access User - No permissions (roles in noAccess array)

Using Custom Users Directly

For more control, pass custom MockUser objects directly:

import { createInMemoryAuthClient, type MockUser } from "@nsxbet/admin-sdk";

const customUsers: MockUser[] = [
  {
    id: "super-admin",
    email: "[email protected]",
    displayName: "Super Admin",
    roles: ["*"], // Wildcard: all permissions
  },
  {
    id: "payments-admin",
    email: "[email protected]",
    displayName: "Payments Admin",
    roles: ["admin.payments.view", "admin.payments.edit"],
  },
];

const authClient = createInMemoryAuthClient({ users: customUsers });

Options

| Option | Type | Default | Description | |--------|------|---------|-------------| | users | MockUser[] | required | Mock users available for selection | | storageKey | string | "@nsxbet/auth" | localStorage key for persistence | | gatewayUrl | string | null | null | Admin gateway URL for real JWT tokens | | tokenTimeout | number | 5000 | Timeout in ms for gateway token fetch |

Gateway Token Integration (BFF)

When gatewayUrl is passed explicitly, the InMemory auth client fetches real signed JWTs from the admin gateway instead of returning mock token strings. This enables end-to-end testing against backends that validate tokens.

How it works:

  1. Developer clicks a user card in the UserSelector
  2. The client calls GET {gatewayUrl}/auth/token?sub=...&email=...&roles=...&scopes=...
  3. The gateway returns a signed JWT which is cached in memory
  4. getAccessToken() returns the cached JWT for all subsequent API calls
  5. When the token nears expiry (within 60s), a background refresh is triggered

Gateway URL resolution order:

  1. Explicit gatewayUrl option passed to createInMemoryAuthClient() (or null to disable gateway tokens)
  2. Otherwise env.adminGatewayUrl from window.__ENV__ (set by /env.js from the shell or the adminModule() dev server)

Vite plugin: When using adminModule() or defineModuleConfig(), /env.js is served in dev and lists ADMIN_GATEWAY_URL from your .env or the default staging gateway. See the Vite Configuration section for override options (gatewayUrl: null disables /env.js entirely).

Error handling:

The UserSelector shows loading, error, and timeout states during the token fetch. If the gateway is unreachable, the developer can:

  • Retry the token fetch
  • Continue with mock token to fall back to legacy behavior
  • Go back to the user selection list
# Add to your .env file to enable gateway token integration
ADMIN_GATEWAY_URL=https://admin-bff-stg.nsx.dev

Module URL Allowlist (Shell Mode)

When the shell loads modules dynamically from URLs, it validates each URL against VITE_ALLOWED_MODULE_ORIGINS before import() or loadScript().

Format: Comma-separated patterns supporting *.domain wildcard (e.g. *.nsx.dev matches modules.nsx.dev, cdn.nsx.dev, and apex nsx.dev).

Dev mode: localhost and 127.0.0.1 are always allowed regardless of the allowlist, so local dev servers work without configuration.

Production: Set VITE_ALLOWED_MODULE_ORIGINS in your build environment. If unset or empty, all module loads fail with a clear error.

BFF / Okta cookie auth (recommended)

Production auth should go through admin-bff: the BFF sets an HttpOnly access_token cookie and exposes GET /me for identity. The shell does not read the JWT in the browser.

AdminShell

<AdminShell
  modules={[manifest]}
  bff
  // or: bff={{ baseUrl: "https://admin.example.com" }}
/>

Use the same origin as the BFF (or a dev proxy) so credentials: 'include' sends the cookie. getAccessToken() returns null for this client — use useFetch / api.fetch, which send credentialed requests when the token is null.

Production Guard

In production deployments, configure bff (admin-bff) for real authentication. If neither authClient nor bff is provided, AdminShell uses in-memory mock authentication.

DO NOT (Common Mistakes)

❌ DO NOT use MemoryRouter

// WRONG - don't create your own router
import { MemoryRouter, Routes, Route } from "react-router-dom";

function App() {
  return (
    <MemoryRouter> {/* ❌ WRONG */}
      <Routes>
        <Route path="/" element={<List />} />
      </Routes>
    </MemoryRouter>
  );
}
// CORRECT - use Routes directly, shell provides BrowserRouter
import { Routes, Route } from "react-router-dom";

function App() {
  return (
    <Routes>
      <Route path="/" element={<List />} />
    </Routes>
  );
}

❌ DO NOT create BrowserRouter in spa.tsx

// WRONG - shell already provides BrowserRouter
import { BrowserRouter } from "react-router-dom";

export default function App() {
  return (
    <BrowserRouter> {/* ❌ WRONG */}
      <MyRoutes />
    </BrowserRouter>
  );
}
// CORRECT - just export the component
import { App } from "./App";
export default App;

❌ DO NOT forget the React plugin

// WRONG - missing React plugin
export default defineModuleConfig({
  port: 3003,
  // ❌ No plugins!
});
// CORRECT - include React plugin
import react from "@vitejs/plugin-react";

export default defineModuleConfig({
  port: 3003,
  plugins: [react()], // ✅
});

❌ DO NOT use external databases

// WRONG - no Supabase, Firebase, or external BaaS
import { createClient } from "@supabase/supabase-js"; // ❌
import { initializeApp } from "firebase/app"; // ❌
// CORRECT - use authenticated fetch for internal APIs
import { useFetch } from "@nsxbet/admin-sdk";

const fetch = useFetch();
const data = await fetch("/api/internal-endpoint");

❌ DO NOT format dates directly

// WRONG - bypasses platform timezone preference
date.toLocaleDateString(); // ❌
date.toLocaleTimeString(); // ❌
new Intl.DateTimeFormat("en-US").format(date); // ❌
dayjs(date).format("YYYY-MM-DD"); // ❌
import { format } from "date-fns"; format(date, "PP"); // ❌
// CORRECT - use <Timestamp /> or useTimestamp()
import { Timestamp, useTimestamp } from "@nsxbet/admin-sdk";

<Timestamp value={date} format="date" /> // ✅

const { formatDate } = useTimestamp();
formatDate(date, "datetime"); // ✅

Direct formatting bypasses the platform timezone preference, causing inconsistent timestamp display across modules. The @nsxbet/no-raw-date-format ESLint rule enforces this.

❌ DO NOT import useNavigate from SDK

// WRONG - useNavigate is not exported from SDK
import { useNavigate } from "@nsxbet/admin-sdk"; // ❌
// CORRECT - import from react-router-dom
import { useNavigate } from "react-router-dom"; // ✅

Troubleshooting

First step: Run npx @nsxbet/admin-cli checklist and verify all items. The checklist covers every setup requirement and often identifies misconfigurations quickly.

"Failed to resolve module specifier 'react'"

Cause: Module is bundling React instead of using shell's version.

Solution: Ensure vite.config.ts uses defineModuleConfig or has external: ["react", "react-dom", "react-router-dom"].

"process is not defined"

Cause: Module code references Node.js globals.

Solution: defineModuleConfig handles this automatically. If using custom config, add:

define: {
  "process.env": {},
  "process.env.NODE_ENV": JSON.stringify("production"),
}

Module not loading in shell

Checklist:

  1. spa.tsx exports default component
  2. admin.module.json has valid id, title, routeBase
  3. ✅ Build outputs to dist/ with spa-[hash].js
  4. ✅ Module is registered in shell's catalog

Styles not working

Run npx @nsxbet/admin-cli checklist to verify Tailwind/PostCSS configuration. Key items:

  1. index.css imports @nsxbet/admin-ui/styles.css
  2. tailwind.config.js uses withAdminSdk from @nsxbet/admin-sdk/tailwind
  3. postcss.config.js exists with tailwindcss plugin

Running Your Module

# Development (standalone with shell UI)
bun run dev
# Open http://localhost:3003

# Build for production
bun run build
# Output: dist/assets/spa-[hash].js

# Preview built module
bun run preview

Error Boundary with Module Ownership

When a module crashes at runtime, the error boundary displays ownership information to help with incident triage. The DynamicModule component accepts an optional moduleInfo prop that provides module metadata to the error boundary.

<DynamicModule
  baseUrl="http://localhost:5001"
  moduleInfo={{
    id: "@admin/payments",
    title: { "en-US": "Payments", "pt-BR": "Pagamentos", "es": "Pagos", "ro": "Plăți" },
    owners: { team: "Payments", supportChannel: "#payments-support" },
  }}
/>

When a module error occurs, the error boundary will:

  • Display the module name (localized for the current locale)
  • Show the owner team and support channel (if provided)
  • Render the support channel as a clickable link if it starts with http/https
  • Provide both "Try Again" (re-render) and "Reload Page" (full reload) buttons
  • Report the error to telemetry with module attribution (moduleId, ownerTeam, errorType)
  • Capture unhandled promise rejections while the module is mounted and report them via telemetry

If owners.team is empty, the ownership section is omitted and the error boundary shows a standard error UI.

Platform API — Timestamp Namespace

The shell exposes window.__ADMIN_PLATFORM_API__.timestamp for timezone preference management:

| Property | Type | Description | |----------|------|-------------| | mode | "utc" \| "local" | Current timezone mode | | setMode(mode) | (TimezoneMode) => void | Change the timezone mode | | onModeChange(cb) | (callback) => () => void | Subscribe to changes; returns unsubscribe |

Note: Modules should use useTimestamp() or <Timestamp /> rather than accessing the Platform API directly.

Types

import type {
  AdminModuleManifest,
  ModuleCommand,
  PlatformAPI,
  User,
  Breadcrumb,
  ModuleInfo,
  ErrorBoundaryProps,
  TimezoneMode,
  TimestampFormat,
  UseTimestampResult,
} from "@nsxbet/admin-sdk";

License

UNLICENSED - Internal use only