diff --git a/components/admin-module-gate.tsx b/components/admin-module-gate.tsx
new file mode 100644
index 0000000..ebe0ab1
--- /dev/null
+++ b/components/admin-module-gate.tsx
@@ -0,0 +1,86 @@
+'use client';
+
+import React from 'react';
+import { useQuery } from '@tanstack/react-query';
+import { useAccount } from 'wagmi';
+import { useParams } from 'next/navigation';
+import { getApi } from '@/lib/api';
+import { queryKeys } from '@/lib/query';
+import { useSiweAuth } from '@/lib/wallet/providers';
+import { AdminGuard } from '@/components/admin-guard';
+import { FeatureUnavailable } from '@/components/feature-gate';
+import {
+ getAdminModule,
+ isAdminModuleEnabled,
+ adminRegistry,
+ type AdminModule,
+} from '@/lib/admin-modules';
+import { DeniedState } from '@/components/ui/api-states';
+
+interface AdminModuleGateProps {
+ moduleId: string;
+ children?: React.ReactNode;
+}
+
+/**
+ * Gate component that enforces AdminModule plugin requirements (feature flags & user roles).
+ * Dynamically looks up module definition from the registry.
+ */
+export function AdminModuleGate({ moduleId, children }: AdminModuleGateProps) {
+ const { address } = useAccount();
+ const { authSession, sessionStatus } = useSiweAuth();
+ const params = useParams();
+ const communitySlug = (params?.communitySlug as string) || 'guildpass-demo';
+
+ const { data: session } = useQuery({
+ queryKey: queryKeys.session.byAddress(address ?? authSession?.address ?? '', communitySlug),
+ queryFn: () => getApi(address ?? authSession?.address, authSession?.token, communitySlug).getSession(),
+ enabled: sessionStatus === 'authenticated' && !!(address ?? authSession?.address),
+ staleTime: 10_000,
+ retry: 1,
+ });
+
+ const module = getAdminModule(moduleId);
+
+ if (!module) {
+ return (
+
+
+
+ );
+ }
+
+ // Evaluate Feature Flag
+ const isFeatureOk = adminRegistry.isFeatureEnabled(module, { userIdentifier: address });
+ if (!isFeatureOk) {
+ return (
+
+
+
+ );
+ }
+
+ // Evaluate User Role
+ const isRoleOk = adminRegistry.hasRequiredRole(module, session?.roles);
+ if (sessionStatus === 'authenticated' && session && !isRoleOk) {
+ return (
+
+
+
+ );
+ }
+
+ const Component = module.component;
+
+ return (
+
+ {Component ? : children}
+
+ );
+}
diff --git a/components/nav.tsx b/components/nav.tsx
index f4ebaca..4b347de 100644
--- a/components/nav.tsx
+++ b/components/nav.tsx
@@ -12,6 +12,7 @@ import { queryKeys } from "@/lib/query";
import { features } from "@/lib/features";
import { config } from "@/lib/config";
import { useState, useRef, useEffect } from "react";
+import { getNavAdminModules } from "@/lib/admin-modules";
const AVAILABLE_COMMUNITIES = [
{ id: 'guildpass-demo', name: 'GuildPass Demo' },
@@ -112,7 +113,7 @@ function CommunitySwitcher({ activeSlug }: { activeSlug: string }) {
);
}
-function CommunitySwitcher() {
+function DisabledCommunitySwitcher() {
return (
({
+ href: item.href as Route,
+ label: item.label,
+ }));
const items = [
{ href: `${prefix}/dashboard` as Route, label: "Dashboard", enabled: true },
- { href: `${prefix}/admin` as Route, label: "Admin", enabled: isAdmin },
- {
- href: `${prefix}/admin/analytics` as Route,
- label: "Analytics",
- enabled: isAdmin && features.analytics,
- },
- {
- href: `${prefix}/admin/rewards` as Route,
- label: "Rewards",
- enabled: isAdmin && features.rewards,
- },
- {
- href: `${prefix}/admin/settings` as Route,
- label: "Settings",
- enabled: isAdmin && features.adminSettings,
- },
+ ...adminNavItems.map((item) => ({ ...item, enabled: true })),
{
href: `${prefix}/resources/alpha` as Route,
label: "Gated",
@@ -217,7 +211,7 @@ export function Nav() {
{it.label}
))}
- {features.multiCommunity && }
+ {features.multiCommunity && }
diff --git a/lib/admin-modules/README.md b/lib/admin-modules/README.md
new file mode 100644
index 0000000..2633abb
--- /dev/null
+++ b/lib/admin-modules/README.md
@@ -0,0 +1,95 @@
+# Admin Module Plugin Architecture
+
+The Admin Module Plugin System provides a modular, self-contained architecture for extending the GuildPass Admin Dashboard. Community deployments and downstream forks can add or customize admin capabilities without modifying core navigation (`components/nav.tsx`) or central routing configurations.
+
+---
+
+## Overview
+
+Each admin module is defined as a self-contained plugin conforming to the `AdminModule` interface. Modules specify their own:
+- **Identification & Meta**: Unique ID, display label, and human-readable description.
+- **Routing**: Internal path or dynamic route builder (supporting community URL prefixes).
+- **Access Control**: Role requirements (e.g. `admin`) and feature flag gating.
+- **Order Priority**: Display ordering in top-level or dashboard navigation bars.
+- **UI Component**: React component rendered for the module.
+
+Modules register with a central `AdminModuleRegistry` singleton, which evaluates user roles and feature flag rollouts to dynamically supply valid navigation links and active views.
+
+---
+
+## Interface Definition (`lib/admin-modules/types.ts`)
+
+```typescript
+export interface AdminModule {
+ /** Unique identifier for the module (e.g., 'overview', 'members', 'policies') */
+ id: string;
+
+ /** Display label shown in navigation. Set to null or omit to hide from main nav. */
+ navLabel?: string | null;
+
+ /** Path or dynamic route builder function `(prefix: string) => string` */
+ route: string | ((prefix: string) => string);
+
+ /** Optional feature flag key controlling availability */
+ featureFlag?: FeatureFlagKey;
+
+ /** Required role(s) to access this module (e.g., 'admin') */
+ requiredRole?: AdminRole | AdminRole[];
+
+ /** Navigation display priority order (lower numbers appear first) */
+ order?: number;
+
+ /** Human-readable description of module functionality */
+ description?: string;
+
+ /** Optional React component for rendering module UI */
+ component?: ComponentType;
+}
+```
+
+---
+
+## Registering a Custom Admin Module
+
+To register a new module without modifying core app files:
+
+```typescript
+import { registerAdminModule } from '@/lib/admin-modules';
+
+registerAdminModule({
+ id: 'audit-logs',
+ navLabel: 'Audit Logs',
+ route: '/admin/audit',
+ requiredRole: 'admin',
+ featureFlag: 'adminSettings', // Optional feature flag gate
+ order: 45,
+ description: 'Security audit telemetry and member access logs',
+});
+```
+
+When registered, the core navigation (`components/nav.tsx`) automatically includes "Audit Logs" for authorized admins when the specified feature flag is active.
+
+---
+
+## API Reference (`lib/admin-modules/registry.ts`)
+
+- `registerAdminModule(module: AdminModule)`: Adds or replaces a module in the registry.
+- `unregisterAdminModule(id: string)`: Removes a registered module by ID.
+- `getAdminModule(id: string)`: Returns the module matching the ID.
+- `getAllAdminModules()`: Returns all registered modules sorted by priority `order`.
+- `getEnabledAdminModules(context)`: Evaluates user roles and feature flags, returning active modules.
+- `getNavAdminModules(context)`: Returns computed `AdminNavItem[]` ready for top-level header or sidebar navigation.
+- `clearAdminModuleRegistry()`: Resets the registry (useful for test isolation).
+
+---
+
+## First-Party Modules
+
+The standard distribution includes default modules in `lib/admin-modules/modules/`:
+1. `overview` (`/admin` - Ecosystem Webhook & Telemetry Logs)
+2. `members` (`/admin/members` - Directory & Role Assignment)
+3. `policies` (`/admin/policies` - Tier Constraints & Access Policies)
+4. `analytics` (`/admin/analytics` - Growth & Tier Analytics)
+5. `rewards` (`/admin/rewards` - Distribution & Token Allocations)
+6. `settings` (`/admin/settings` - Gateway & Community Settings)
+7. `governance` (`/admin/governance` - Proposal & Voting Policy)
diff --git a/lib/admin-modules/index.ts b/lib/admin-modules/index.ts
new file mode 100644
index 0000000..7c463c6
--- /dev/null
+++ b/lib/admin-modules/index.ts
@@ -0,0 +1,36 @@
+import { adminRegistry } from './registry';
+import { overviewModule } from './modules/overview';
+import { membersModule } from './modules/members';
+import { policiesModule } from './modules/policies';
+import { analyticsModule } from './modules/analytics';
+import { rewardsModule } from './modules/rewards';
+import { settingsModule } from './modules/settings';
+import { governanceModule } from './modules/governance';
+
+export * from './types';
+export * from './registry';
+
+/**
+ * First-party admin modules.
+ */
+export const defaultAdminModules = [
+ overviewModule,
+ membersModule,
+ policiesModule,
+ analyticsModule,
+ rewardsModule,
+ settingsModule,
+ governanceModule,
+];
+
+/**
+ * Register all standard first-party admin modules into the registry.
+ */
+export function registerDefaultAdminModules(): void {
+ for (const module of defaultAdminModules) {
+ adminRegistry.register(module);
+ }
+}
+
+// Auto-register default first-party admin modules
+registerDefaultAdminModules();
diff --git a/lib/admin-modules/modules/analytics.ts b/lib/admin-modules/modules/analytics.ts
new file mode 100644
index 0000000..1269685
--- /dev/null
+++ b/lib/admin-modules/modules/analytics.ts
@@ -0,0 +1,11 @@
+import type { AdminModule } from '../types';
+
+export const analyticsModule: AdminModule = {
+ id: 'analytics',
+ navLabel: 'Analytics',
+ route: '/admin/analytics',
+ featureFlag: 'analytics',
+ requiredRole: 'admin',
+ order: 40,
+ description: 'Community growth charts, signup trends, and membership tier analytics',
+};
diff --git a/lib/admin-modules/modules/governance.ts b/lib/admin-modules/modules/governance.ts
new file mode 100644
index 0000000..260fc5a
--- /dev/null
+++ b/lib/admin-modules/modules/governance.ts
@@ -0,0 +1,11 @@
+import type { AdminModule } from '../types';
+
+export const governanceModule: AdminModule = {
+ id: 'governance',
+ navLabel: 'Governance',
+ route: '/admin/governance',
+ featureFlag: 'governance',
+ requiredRole: 'admin',
+ order: 70,
+ description: 'Community governance proposals, voting power, and delegation policy',
+};
diff --git a/lib/admin-modules/modules/members.ts b/lib/admin-modules/modules/members.ts
new file mode 100644
index 0000000..be05fe9
--- /dev/null
+++ b/lib/admin-modules/modules/members.ts
@@ -0,0 +1,10 @@
+import type { AdminModule } from '../types';
+
+export const membersModule: AdminModule = {
+ id: 'members',
+ navLabel: null, // Sub-feature accessed from admin dashboard or direct route
+ route: '/admin/members',
+ requiredRole: 'admin',
+ order: 20,
+ description: 'Member directory, search, pagination, and role assignment',
+};
diff --git a/lib/admin-modules/modules/overview.ts b/lib/admin-modules/modules/overview.ts
new file mode 100644
index 0000000..34cef4a
--- /dev/null
+++ b/lib/admin-modules/modules/overview.ts
@@ -0,0 +1,10 @@
+import type { AdminModule } from '../types';
+
+export const overviewModule: AdminModule = {
+ id: 'overview',
+ navLabel: 'Admin',
+ route: '/admin',
+ requiredRole: 'admin',
+ order: 10,
+ description: 'Ecosystem Webhook Logs, system telemetry, and operational events',
+};
diff --git a/lib/admin-modules/modules/policies.ts b/lib/admin-modules/modules/policies.ts
new file mode 100644
index 0000000..9a418c7
--- /dev/null
+++ b/lib/admin-modules/modules/policies.ts
@@ -0,0 +1,11 @@
+import type { AdminModule } from '../types';
+
+export const policiesModule: AdminModule = {
+ id: 'policies',
+ navLabel: null, // Access policy editor
+ route: '/admin/policies',
+ featureFlag: 'adminPolicies',
+ requiredRole: 'admin',
+ order: 30,
+ description: 'Access policy rules, tier constraints, and role requirements',
+};
diff --git a/lib/admin-modules/modules/rewards.ts b/lib/admin-modules/modules/rewards.ts
new file mode 100644
index 0000000..128a64f
--- /dev/null
+++ b/lib/admin-modules/modules/rewards.ts
@@ -0,0 +1,11 @@
+import type { AdminModule } from '../types';
+
+export const rewardsModule: AdminModule = {
+ id: 'rewards',
+ navLabel: 'Rewards',
+ route: '/admin/rewards',
+ featureFlag: 'rewards',
+ requiredRole: 'admin',
+ order: 50,
+ description: 'Community reward distribution, token allocations, and claim rules',
+};
diff --git a/lib/admin-modules/modules/settings.ts b/lib/admin-modules/modules/settings.ts
new file mode 100644
index 0000000..39398fc
--- /dev/null
+++ b/lib/admin-modules/modules/settings.ts
@@ -0,0 +1,11 @@
+import type { AdminModule } from '../types';
+
+export const settingsModule: AdminModule = {
+ id: 'settings',
+ navLabel: 'Settings',
+ route: '/admin/settings',
+ featureFlag: 'adminSettings',
+ requiredRole: 'admin',
+ order: 60,
+ description: 'Community settings, branding, and integration gateway configuration',
+};
diff --git a/lib/admin-modules/registry.ts b/lib/admin-modules/registry.ts
new file mode 100644
index 0000000..d1db1c1
--- /dev/null
+++ b/lib/admin-modules/registry.ts
@@ -0,0 +1,144 @@
+import type { FeatureFlagKey } from '@/lib/features';
+import type { AdminModule, AdminModuleContext, AdminNavItem } from './types';
+
+class AdminModuleRegistry {
+ private modules: Map = new Map();
+
+ /**
+ * Register a new admin module plugin.
+ * Overwrites any existing module with the same ID.
+ */
+ public register(module: AdminModule): void {
+ if (!module.id) {
+ throw new Error('AdminModule registration failed: module must have a valid "id".');
+ }
+ this.modules.set(module.id, module);
+ }
+
+ /**
+ * Unregister an admin module by ID.
+ */
+ public unregister(id: string): boolean {
+ return this.modules.delete(id);
+ }
+
+ /**
+ * Retrieve a registered admin module by ID.
+ */
+ public get(id: string): AdminModule | undefined {
+ return this.modules.get(id);
+ }
+
+ /**
+ * Retrieve all registered admin modules sorted by order priority.
+ */
+ public getAll(): AdminModule[] {
+ return Array.from(this.modules.values()).sort(
+ (a, b) => (a.order ?? 100) - (b.order ?? 100)
+ );
+ }
+
+ /**
+ * Check if a module's required feature flag is enabled.
+ */
+ public isFeatureEnabled(module: AdminModule, context?: AdminModuleContext): boolean {
+ if (!module.featureFlag) {
+ return true;
+ }
+
+ if (context?.featureFlags && module.featureFlag in context.featureFlags) {
+ return Boolean(context.featureFlags[module.featureFlag]);
+ }
+
+ const { isFeatureEnabledForIdentifier } = require('../features');
+ return isFeatureEnabledForIdentifier(module.featureFlag, context?.userIdentifier);
+ }
+
+ /**
+ * Check if the user meets the role requirements for a module.
+ */
+ public hasRequiredRole(module: AdminModule, roles?: string[]): boolean {
+ if (!module.requiredRole) {
+ return true;
+ }
+
+ if (!roles || roles.length === 0) {
+ return false;
+ }
+
+ const required = Array.isArray(module.requiredRole)
+ ? module.requiredRole
+ : [module.requiredRole];
+
+ return required.some((role) => roles.includes(role));
+ }
+
+ /**
+ * Check if a module is enabled for the given context (evaluates both feature flag & role).
+ */
+ public isEnabled(module: AdminModule, context?: AdminModuleContext): boolean {
+ const featureOk = this.isFeatureEnabled(module, context);
+ const roleOk = this.hasRequiredRole(module, context?.roles);
+ return featureOk && roleOk;
+ }
+
+ /**
+ * Resolve the full href route for a module given the community prefix.
+ */
+ public resolveHref(module: AdminModule, prefix = ''): string {
+ if (typeof module.route === 'function') {
+ return module.route(prefix);
+ }
+ return prefix ? `${prefix}${module.route}` : module.route;
+ }
+
+ /**
+ * Get all active and accessible modules for the provided context.
+ */
+ public getEnabledModules(context?: AdminModuleContext): AdminModule[] {
+ return this.getAll().filter((mod) => this.isEnabled(mod, context));
+ }
+
+ /**
+ * Get all nav items derived from enabled modules for navigation rendering.
+ */
+ public getNavItems(context?: AdminModuleContext): AdminNavItem[] {
+ const prefix = context?.prefix ?? '';
+ const enabledModules = this.getEnabledModules(context);
+
+ return enabledModules
+ .filter((mod) => mod.navLabel !== null && mod.navLabel !== undefined)
+ .map((mod) => ({
+ id: mod.id,
+ href: this.resolveHref(mod, prefix),
+ label: mod.navLabel as string,
+ order: mod.order ?? 100,
+ }))
+ .sort((a, b) => a.order - b.order);
+ }
+
+ /**
+ * Reset registry (clear all registered modules).
+ */
+ public clear(): void {
+ this.modules.clear();
+ }
+}
+
+/** Global singleton instance of the Admin Module Registry */
+export const adminRegistry = new AdminModuleRegistry();
+
+// Export convenient standalone helper functions for cleaner API usage
+export const registerAdminModule = (module: AdminModule): void => adminRegistry.register(module);
+export const unregisterAdminModule = (id: string): boolean => adminRegistry.unregister(id);
+export const getAdminModule = (id: string): AdminModule | undefined => adminRegistry.get(id);
+export const getAllAdminModules = (): AdminModule[] => adminRegistry.getAll();
+export const getEnabledAdminModules = (context?: AdminModuleContext): AdminModule[] =>
+ adminRegistry.getEnabledModules(context);
+export const getNavAdminModules = (context?: AdminModuleContext): AdminNavItem[] =>
+ adminRegistry.getNavItems(context);
+export const isAdminModuleEnabled = (module: AdminModule, context?: AdminModuleContext): boolean =>
+ adminRegistry.isEnabled(module, context);
+export const resolveAdminModuleHref = (module: AdminModule, prefix?: string): string =>
+ adminRegistry.resolveHref(module, prefix);
+export const clearAdminModuleRegistry = (): void => adminRegistry.clear();
diff --git a/lib/admin-modules/types.ts b/lib/admin-modules/types.ts
new file mode 100644
index 0000000..81cd41f
--- /dev/null
+++ b/lib/admin-modules/types.ts
@@ -0,0 +1,59 @@
+import type { ComponentType } from 'react';
+import type { FeatureFlagKey } from '@/lib/features';
+
+export type AdminRole = string;
+
+/**
+ * Self-contained definition for an Admin Module plugin.
+ * Allows nav entries, routing, feature flags, role gating, and component rendering
+ * to be declared in a single, pluggable structure.
+ */
+export interface AdminModule {
+ /** Unique identifier for the module (e.g., 'overview', 'members', 'policies', 'analytics', 'rewards', 'settings') */
+ id: string;
+
+ /** Display label shown in the navigation bar. Set to null to omit from navigation. */
+ navLabel?: string | null;
+
+ /** Route path or route builder function (e.g., '/admin/policies' or (prefix) => `${prefix}/admin/policies`) */
+ route: string | ((prefix: string) => string);
+
+ /** Optional feature flag key controlling availability of this module */
+ featureFlag?: FeatureFlagKey;
+
+ /** Required role(s) to access this module (e.g., 'admin') */
+ requiredRole?: AdminRole | AdminRole[];
+
+ /** Navigation display priority order (lower numbers appear first) */
+ order?: number;
+
+ /** Human-readable description of module functionality */
+ description?: string;
+
+ /** React Component rendered for this module route */
+ component?: ComponentType;
+}
+
+/**
+ * Evaluation context for checking module availability, navigation URLs, and access rights.
+ */
+export interface AdminModuleContext {
+ /** User roles (e.g. ['admin', 'member']) */
+ roles?: string[];
+ /** Wallet address or user ID (for feature rollout bucket calculations) */
+ userIdentifier?: string | null;
+ /** Active community slug URL prefix (e.g., '/guildpass-demo' or '') */
+ prefix?: string;
+ /** Custom feature flag state overrides (defaults to global features) */
+ featureFlags?: Record;
+}
+
+/**
+ * Computed navigation item derived from a registered AdminModule.
+ */
+export interface AdminNavItem {
+ id: string;
+ href: string;
+ label: string;
+ order: number;
+}
diff --git a/test/admin-modules.test.ts b/test/admin-modules.test.ts
new file mode 100644
index 0000000..65dce03
--- /dev/null
+++ b/test/admin-modules.test.ts
@@ -0,0 +1,135 @@
+import './setup-env';
+import { describe, it, beforeEach } from 'node:test';
+import * as assert from 'node:assert/strict';
+import {
+ adminRegistry,
+ registerAdminModule,
+ unregisterAdminModule,
+ getAdminModule,
+ getAllAdminModules,
+ getEnabledAdminModules,
+ getNavAdminModules,
+ clearAdminModuleRegistry,
+ registerDefaultAdminModules,
+ type AdminModule,
+} from '../lib/admin-modules';
+
+describe('AdminModule Plugin System & Registry', () => {
+ beforeEach(() => {
+ clearAdminModuleRegistry();
+ registerDefaultAdminModules();
+ });
+
+ it('registers default first-party modules correctly', () => {
+ const modules = getAllAdminModules();
+ const ids = modules.map((m) => m.id);
+
+ assert.ok(ids.includes('overview'));
+ assert.ok(ids.includes('members'));
+ assert.ok(ids.includes('policies'));
+ assert.ok(ids.includes('analytics'));
+ assert.ok(ids.includes('rewards'));
+ assert.ok(ids.includes('settings'));
+ assert.ok(ids.includes('governance'));
+ });
+
+ it('allows registering and looking up a custom admin module', () => {
+ const customModule: AdminModule = {
+ id: 'custom-audit',
+ navLabel: 'Audit Logs',
+ route: '/admin/audit',
+ requiredRole: 'admin',
+ order: 15,
+ description: 'Custom third-party audit logging plugin',
+ };
+
+ registerAdminModule(customModule);
+ const fetched = getAdminModule('custom-audit');
+
+ assert.ok(fetched);
+ assert.equal(fetched?.id, 'custom-audit');
+ assert.equal(fetched?.navLabel, 'Audit Logs');
+ });
+
+ it('filters enabled modules by user role', () => {
+ // Non-admin user should not receive admin-gated modules
+ const nonAdminModules = getEnabledAdminModules({ roles: ['member'] });
+ assert.equal(nonAdminModules.length, 0);
+
+ // Admin user should receive enabled admin modules
+ const adminModules = getEnabledAdminModules({
+ roles: ['admin'],
+ featureFlags: { analytics: true, rewards: true, adminSettings: true, adminPolicies: true, governance: true },
+ });
+ assert.ok(adminModules.length >= 5);
+ });
+
+ it('filters enabled modules by feature flags', () => {
+ const enabledWithFlags = getEnabledAdminModules({
+ roles: ['admin'],
+ featureFlags: {
+ analytics: true,
+ rewards: false,
+ adminSettings: false,
+ adminPolicies: true,
+ governance: false,
+ },
+ });
+
+ const enabledIds = enabledWithFlags.map((m) => m.id);
+ assert.ok(enabledIds.includes('analytics'));
+ assert.ok(enabledIds.includes('policies'));
+ assert.equal(enabledIds.includes('rewards'), false);
+ assert.equal(enabledIds.includes('settings'), false);
+ assert.equal(enabledIds.includes('governance'), false);
+ });
+
+ it('generates correct nav items with community prefix', () => {
+ const navItems = getNavAdminModules({
+ roles: ['admin'],
+ prefix: '/guildpass-demo',
+ featureFlags: { analytics: true, rewards: true, adminSettings: true },
+ });
+
+ const overviewNav = navItems.find((item) => item.id === 'overview');
+ const analyticsNav = navItems.find((item) => item.id === 'analytics');
+
+ assert.ok(overviewNav);
+ assert.equal(overviewNav?.href, '/guildpass-demo/admin');
+ assert.equal(overviewNav?.label, 'Admin');
+
+ assert.ok(analyticsNav);
+ assert.equal(analyticsNav?.href, '/guildpass-demo/admin/analytics');
+ assert.equal(analyticsNav?.label, 'Analytics');
+ });
+
+ it('allows unregistering a module', () => {
+ assert.ok(getAdminModule('analytics'));
+ const unregistered = unregisterAdminModule('analytics');
+ assert.equal(unregistered, true);
+ assert.equal(getAdminModule('analytics'), undefined);
+ });
+
+ it('adds a new admin module without modifying core nav or routes', () => {
+ const newPlugin: AdminModule = {
+ id: 'token-faucet',
+ navLabel: 'Faucet',
+ route: (prefix) => `${prefix}/admin/faucet`,
+ requiredRole: 'admin',
+ order: 45,
+ };
+
+ registerAdminModule(newPlugin);
+
+ const navItems = getNavAdminModules({
+ roles: ['admin'],
+ prefix: '/builders-collective',
+ featureFlags: { analytics: true },
+ });
+
+ const faucetNav = navItems.find((item) => item.id === 'token-faucet');
+ assert.ok(faucetNav);
+ assert.equal(faucetNav?.href, '/builders-collective/admin/faucet');
+ assert.equal(faucetNav?.label, 'Faucet');
+ });
+});
diff --git a/test/member-cache.test.ts b/test/member-cache.test.ts
index 34d73e2..3059b06 100644
--- a/test/member-cache.test.ts
+++ b/test/member-cache.test.ts
@@ -1,3 +1,4 @@
+import './setup-env'
import { describe, it } from 'node:test'
import * as assert from 'node:assert/strict'
import { QueryClient, QueryObserver } from '@tanstack/react-query'