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

exguard-client

v2.1.4

Published

ExGuard RBAC client with cache-first Redis support for maximum performance in EmpowerX applications

Readme

exguard-client

ExGuard RBAC (Role-Based Access Control) client library with cache-first Redis support for EmpowerX applications.

🚀 Cache-First Performance: Prioritizes Redis cache over API calls for maximum speed and efficiency.

Features

  • 🔐 RBAC Authentication: Token verification and user access management
  • 🔄 Realtime Updates: WebSocket-based real-time RBAC changes
  • 🎯 Permission Guards: React components for route and feature protection
  • 🪝 React Hooks: Easy-to-use hooks for permission checking
  • 📦 TypeScript: Full type safety with TypeScript definitions
  • Auto-Configuration: Automatically detects API URL based on environment
  • 🚀 Zero Setup: Works out of the box with sensible defaults
  • 💾 Cache-First Redis: Prioritizes Redis cache, minimizes API calls
  • 🔗 Cross-Tab Sync: Synchronized data across browser tabs
  • Backend Integration: Seamless integration with exguard-cached backend package

Cache-First Strategy

This package implements a pure cache-first approach:

  1. Always check Redis cache first
  2. 📡 Call API ONLY if cache is completely empty
  3. 🚫 Never call API for permission checks
  4. 🔄 Rely on backend cache invalidation for updates

Environment Variables

Required for the frontend to know your backend API URL (which proxies to Redis):

# Backend API URL (required - used as proxy to Redis)
VITE_GUARD_API_URL=http://localhost:3000

# OR alternatively:
VITE_GUARD_APP_URL=http://localhost:3000

How it works: The HttpRedisClient automatically reads these env vars and constructs:

http://localhost:3000/guard/redis/*

No direct Redis connection needed - the backend handles Redis communication!

Installation

pnpm add exguard-client

Quick Start

Auto-setup (recommended)

npx exguard-setup

This command scaffolds the entire ExGuard integration in your project:

  1. Creates src/features/exguard/ module structure (index.ts, config, SystemAdminPermissionGuard)
  2. Updates protected-route.tsx — wraps <Outlet /> with <ExGuardRealtimeProvider>
  3. Updates auth-utils.ts — adds exguard:token-updated event dispatch on login
  4. Fixes provider order in main.tsx/App.tsxExGuardRealtimeProvider wraps UserAccessProvider

After the auto-setup completes, add the required environment variable:

# .env — tells the client where your backend API lives
VITE_GUARD_API_URL=http://localhost:3000

Then jump straight to Step 3: Use Cache-First Hook below.


Manual Setup

If you prefer to wire things up by hand, follow these 3 steps:

1. Set Environment Variable

File to edit: .env

VITE_GUARD_API_URL=http://localhost:3000

The HttpRedisClient automatically reads this and constructs proxy URLs like /guard/redis/* against your backend. No direct Redis connection is needed — the backend handles Redis communication via the exguard-cached package.

Backend Setup Required: Your NestJS backend must run npx exguard-cached setup to provide the Redis proxy endpoints.

2. Wrap with Providers

File to edit: src/main.tsx or src/App.tsx

import { ExGuardRealtimeProvider, UserAccessProvider } from 'exguard-client';

function App() {
  return (
    <ExGuardRealtimeProvider>
      <UserAccessProvider>
        <Router>
          <Routes>
            <Route path="/roles" element={<RolesPage />} />
            <Route path="/users" element={<UsersPage />} />
          </Routes>
        </Router>
      </UserAccessProvider>
    </ExGuardRealtimeProvider>
  );
}

Order matters: ExGuardRealtimeProvider must wrap UserAccessProvider so real-time RBAC events can trigger cache invalidation.

3. Use Cache-First Hook in Components

File to edit: Any component that needs user access or permission checks

import { useUserAccessCacheFirst } from 'exguard-client';

function UsersPage() {
  const { userAccess, isLoading, hasPermission, hasModulePermission } = useUserAccessCacheFirst();

  // Permission checking uses ONLY cached data - no API calls!
  // Supports both formats:
  const canViewUsers = hasPermission('user_groups:view');  // Format: 'resource:action'
  const canEditRoles = hasPermission('exGUARD', 'roles:edit');  // Format: module, 'resource:action'
  const hasUserGroupModule = hasModulePermission('user_groups');  // Check if user has ANY permission in module

  if (isLoading) return <div>Loading from cache...</div>;

  return (
    <div>
      <h1>Welcome {userAccess?.user?.username}</h1>
      {canViewUsers && <UserList />}
      {canEditRoles && <RoleEditor />}
    </div>
  );
}

Next Steps After Setup

  • Protect routes — use <PermissionGuard> in your route definitions
  • Check permissions inline — use hasPermission() from useUserAccessCacheFirst()
  • React to RBAC changesExGuardRealtimeProvider auto-invalidates cache on backend updates
  • Add elevated privileges — use the generated ElevatedPrivilegePermissionGuard for admin-only sections

Cache-First Hook API

useUserAccessCacheFirst()

Returns an object with the following properties:

interface UseUserAccessCacheFirstReturn {
  userAccess: UserAccessData | null;           // Cached user data
  isLoading: boolean;                          // Loading from cache
  isFetching: boolean;                        // Fetching from API (rare)
  error: Error | null;                        // Error state
  hasPermission: (permission: string, action?: string) => boolean;
  hasModuleAccess: (module: string) => boolean;
  hasModulePermission: (module: string) => boolean;  // Alias for hasModuleAccess
  getModulePermissions: (module: string) => string[];
  refetch: (force?: boolean) => Promise<void>; // Force API call
  refetchSilent: () => Promise<void>;         // Silent refresh
  invalidateCache: () => void;                // Invalidate cache
  clearCache: () => Promise<void>;            // Clear cache
}

Permission Checking

const { hasPermission, hasModulePermission, getModulePermissions } = useUserAccessCacheFirst();

// Format 1: hasPermission('resource:action') - RECOMMENDED
// Searches across ALL modules for the permission
const canViewUsers = hasPermission('user_groups:view');
const canUpdateGroup = hasPermission('user_groups:update_group');
const canDeleteUser = hasPermission('exGUARD:users:delete');

// Format 2: hasPermission('module', 'resource:action')
const canEditRoles = hasPermission('exGUARD', 'roles:edit');

// Check if user has ANY permission in a module
const hasUserGroupModule = hasModulePermission('user_groups');

// Get all permissions for a module
const permissions = getModulePermissions('exGUARD');

Getting User Data from Cache

import { useMemo } from 'react';
import { useUserAccessCacheFirst } from 'exguard-client';

const { userAccess } = useUserAccessCacheFirst();

// Get user roles (from cache - no API call!)
const userRoles = useMemo(() => userAccess?.roles ?? [], [userAccess?.roles]);
// Returns: ["system administrator", "regional admin", ...]

// Get user groups (from cache - no API call!)
const userGroups = useMemo(() => userAccess?.groups ?? [], [userAccess?.groups]);
// Returns: ["administrator", "national access", ...]

// Get user details (from cache - no API call!)
const user = userAccess?.user;
// Returns: { id, username, email, givenName, familyName, ... }

// Get module permissions (from cache - no API call!)
const modules = userAccess?.modules;
// Returns: [{ key: 'exGUARD', permissions: ['user_groups:view', ...] }, ...]

// Example: Check if user is admin
const isAdmin = userGroups.some((g: string) => g.toLowerCase() === 'administrator');
const isSystemAdmin = userRoles.some((r: string) => r.toLowerCase() === 'system administrator');

How It Works

  1. Redis First: Always checks Redis cache first (via backend HTTP proxy)
  2. API Fallback: Only calls /guard/verify-token + /guard/me if NO cache exists
  3. Auto-Cache: API results are automatically cached in Redis
  4. Auth Token: HttpRedisClient automatically sends auth token with Redis requests
// ⚡ INSTANT - Uses cached data (0ms)
const canView = hasPermission('user_groups:view');

// 📡 SLOW - API call only when cache empty
// (happens once per session or on cache invalidation)
const { userAccess } = useUserAccessCacheFirst();

Cache Behavior

When API is Called 📡

The API endpoint is called ONLY in these rare cases:

  1. First visit with completely empty cache
  2. Cache expired and real-time update invalidates
  3. Manual refetch is explicitly called
  4. Cache corruption detected

When API is NOT Called 🚫

The API is NEVER called for:

  1. Permission checks - uses cached data only
  2. Module access checks - uses cached data only
  3. User roles/groups - uses cached data only
  4. Field office data - uses cached data only

Cache Performance

// ✅ INSTANT - Uses cached data (0ms)
const canView = hasPermission('exGUARD', 'users:view');

// 📡 SLOW - API call only when cache empty
// (happens once per session or on cache invalidation)
const { userAccess } = useUserAccessCacheFirst();

Backend Integration

For optimal performance, ensure your backend uses the exguard-cached package:

Backend Setup

# Install backend package
pnpm add exguard-cached

# Auto-configure (recommended)
npx exguard-cached setup

Backend Endpoint

import { Controller, Get, UseGuards } from '@nestjs/common';
import { RequireCachedUserPermission, CachedUser } from 'exguard-cached';

@Controller('guard')
export class GuardController {
  
  @Get('me')
  @RequireCachedUserPermission('exGUARD', 'profile:view')
  async getMe(@CachedUser() cachedUser: CachedUserData) {
    return {
      success: true,
      data: {
        user: cachedUser.user,
        groups: cachedUser.groups,
        roles: cachedUser.roles,
        modules: cachedUser.modules,
        fieldOffices: cachedUser.fieldOffices,
      }
    };
  }
}

Cache Synchronization

The backend automatically invalidates cache when:

  • User permissions change
  • Roles are updated
  • Groups are modified
  • Real-time WebSocket updates are received

Advanced Usage

Direct Redis Client Access

import { getCacheFirstRedisClient } from 'exguard-client';

const redisClient = getCacheFirstRedisClient();

// Direct Redis operations
await redisClient.set('custom:key', data, { ttl: 300 });
const cached = await redisClient.get('custom:key');
await redisClient.del('custom:key');

Custom Cache Keys

import { RedisCacheClient } from 'exguard-client';

const customClient = new RedisCacheClient({
  host: 'localhost',
  port: 6379,
  keyPrefix: 'myapp:custom:',
});

await customClient.set('user:123', userData, { ttl: 600 });

Fallback to LocalStorage

If Redis is unavailable, the client gracefully falls back to localStorage:

// The hook automatically handles Redis failures
const { userAccess } = useUserAccessCacheFirst();
// Works even if Redis is down (uses localStorage fallback)

Migration Guide

From localStorage

// Before (localStorage)
import { useUserAccessSingleton } from 'exguard-client';
const { userAccess } = useUserAccessSingleton();

// After (Cache-First Redis)
import { useUserAccessCacheFirst, initializeCacheFirstRedisClient } from 'exguard-client';

// Initialize once
initializeCacheFirstRedisClient({
  host: 'localhost',
  port: 6379,
});

// Use in components - cache-first approach
const { userAccess, hasPermission } = useUserAccessCacheFirst();

From Basic Redis

// Before (basic Redis)
import { useUserAccessRedis, initializeRedisClient } from 'exguard-client';
initializeRedisClient(config);

// After (Cache-First Redis)
import { useUserAccessCacheFirst, initializeCacheFirstRedisClient } from 'exguard-client';
initializeCacheFirstRedisClient(config);

Performance Benefits

| Feature | localStorage | Basic Redis | Cache-First Redis | |---------|-------------|-------------|-------------------| | API Calls | Frequent | Moderate | Minimal | | Cross-tab Sync | ❌ | ✅ | ✅ | | Backend Sync | ❌ | ✅ | ✅ | | Permission Speed | ⚡ Fast | 🚀 Fast | ⚡⚡ Instant | | Cache Hits | 30% | 70% | 95%+ |

Troubleshooting

Common Issues

Permission Check Returns False

Your cached permissions might be in format exGUARD:user_groups:update_group but you're checking user_groups:update.

Solution: Use the full permission string:

// ✅ CORRECT - Searches across ALL modules
const canUpdate = hasPermission('user_groups:update_group');
const canUpdate = hasPermission('exGUARD:user_groups:update_group');

// Also works with module + permission
const canUpdate = hasPermission('exGUARD', 'user_groups:update_group');

// Debug: Check what permissions user has
const { userAccess } = useUserAccessCacheFirst();
console.log('User modules:', userAccess?.modules);

Redis Connection Failed (401 Unauthorized)

The HttpRedisClient now automatically sends the auth token. If you still get 401:

# Check if user is logged in
echo $localStorage.getItem('access_token')

# Check backend API is running
curl http://localhost:3000/guard/me

Cache Not Updating

// Force cache refresh
const { refetch } = useUserAccessCacheFirst();
await refetch(true); // Force API call

Debug Mode

Enable debug logging:

// Add to environment
EXGUARD_DEBUG=true

// Check browser console for cache logs
// [ExGuard Cache-First] ✅ Cache hit - using Redis data
// [ExGuard Cache-First] 📡 No cache found, calling API as last resort

Changelog

v2.1.0

  • Updated Quick Start with step-by-step auto-setup and manual flow
  • Added npx exguard-setup CLI auto-setup section
  • Reorganized documentation for clarity

License

MIT License - see LICENSE file for details.

Support

For issues and questions:

  1. Check the troubleshooting section
  2. Verify Redis connectivity
  3. Ensure backend uses exguard-cached package
  4. Check environment variables

🚀 Cache-First Performance: Built for speed, designed for scale.