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

@nlabs/metropolisjs

v1.1.1

Published

Type-safe frontend integration for authentication, data, messaging, realtime events, billing, and Reaktor APIs.

Readme

MetropolisJS: Seamless Frontend-Backend Integration Framework

The Ultimate Frontend Integration Library for Modern Web Applications

npm version npm downloads Documentation Issues TypeScript MIT license Chat

MetropolisJS is the bridge that connects your frontend dreams to backend reality. Built on the powerful combination of Reaktor (backend services) and ArkhamJS (frontend data store), MetropolisJS provides a seamless, real-time integration layer that handles everything from user authentication to real-time messaging and notifications.

Why MetropolisJS?

Seamless Integration

Connect your React frontend to Reaktor-powered backend services with zero configuration headaches. MetropolisJS handles all the complex data flow, state management, and real-time communication.

Real-Time Everything

Built-in WebSocket and Server-Sent Events (SSE) support for instant messaging, live notifications, and real-time data synchronization. Your users will never miss a beat.

Type-Safe & Reliable

Full TypeScript support with comprehensive type definitions. Catch errors at compile time, not runtime.

Developer Experience First

Clean, intuitive APIs that make complex operations feel simple. Focus on building features, not boilerplate.

What Can You Build?

MetropolisJS powers applications that need:

  • User Authentication & Authorization
  • Role-Based Access Control (RBAC) with 5-level permission system
  • Real-Time Messaging Systems
  • Live Notifications
  • Social Media Features (posts, reactions, tags)
  • Location-Based Services
  • Media Management (images, files)
  • Event Management
  • User Connections & Relationships

Quick Start

Installation

npm install @nlabs/metropolisjs @nlabs/arkhamjs @nlabs/arkhamjs-utils-react

Basic Setup

import {Metropolis, useUserActions, useMessageActions, useRestActions, useWebsocketActions} from '@nlabs/metropolisjs';

const App = () => {
  return (
    <Metropolis config={{
      development: {
        environment: 'development',
        app: {
          api: {
            url: 'http://localhost:3000/app',
            public: 'http://localhost:3000/public'
          }
        }
      }
    }}>
      <YourApp />
    </Metropolis>
  );
};

const YourApp = () => {
  // Use specialized hooks for better performance
  const userActions = useUserActions();
  const messageActions = useMessageActions();
  const websocketActions = useWebsocketActions();

  // Start building amazing features!
  return <div>Your app content</div>;
};

Using Actions

MetropolisJS provides multiple ways to access actions:

// Option 1: Specialized hooks (recommended - best performance)
const userActions = useUserActions();
const postActions = usePostActions();
const restActions = useRestActions();

// Option 2: Selective creation
const {userActions, postActions, restActions} = useMetropolis(['user', 'post', 'rest']);

// Option 3: Create an explicit action group
const {messageActions} = useMetropolis(['message']);

External REST Endpoints

MetropolisJS includes a rest action group for APIs that are not part of Reaktor. It delegates to @nlabs/rip-hunter, so app code can keep using Metropolis actions instead of importing rip-hunter directly.

<Metropolis
  config={{
    development: {
      app: {
        api: {
          endpoints: {
            weather: 'https://api.example.com/weather'
          },
          public: 'http://localhost:3000/public',
          url: 'http://localhost:3000/app'
        }
      }
    }
  }}
>
  <YourApp />
</Metropolis>

const WeatherPanel = () => {
  const restActions = useRestActions();

  const loadWeather = async () => {
    const weather = await restActions.get('weather', {zip: '60601'}, {cache: true});
    return weather;
  };
};

Use authenticate: true when an external endpoint should receive the current Metropolis session token:

const profile = await restActions.request(
  'https://api.example.com/profile',
  'PATCH',
  {displayName: 'Ada'},
  {authenticate: true}
);

Configuration

The Metropolis component accepts three main props: config, adapters, and translations. Here's how to configure each:

RUM analytics

Configure the public analytics identifier returned by Reaktor under app.rum.analyticsId and the collection endpoint under app.api.endpoints.rum. MetropolisJS sends the identifier in the JSON batch body; it is not placed in the URL or an authorization header.

<Metropolis
  config={{
    production: {
      app: {
        name: 'My App',
        api: {
          endpoints: {
            rum: 'https://events.example.com/track'
          }
        },
        rum: {
          analyticsId: '00000000-0000-4000-8000-000000000000',
          debounceMs: 250,
          dedupeMs: 1000,
          enabled: true,
          respectPrivacySignals: true,
          throttleMs: 1000
        }
      }
    }
  }}>
  <YourApp />
</Metropolis>

Configure the endpoint at app.api.endpoints.rum. RUM delivery is unauthenticated and each batch contains analyticsId and up to 50 sanitized events.

Beacon delivery

When the page is hidden or receives pagehide, the Metropolis provider automatically flushes pending RUM events with navigator.sendBeacon(). This gives terminal analytics a chance to finish without delaying navigation or page shutdown.

Beacon delivery is best-effort:

  • If navigator.sendBeacon is unavailable, throws, or declines the payload, MetropolisJS immediately falls back to its normal asynchronous RUM request.
  • A batch accepted by sendBeacon is not sent again through the normal request path.
  • Scheduled and explicit flush() calls continue to use the normal request path unless useBeacon is requested.
  • Privacy signals and the enabled option are respected for both delivery paths.

Use the specialized hook when an application needs to track or flush events directly:

import {useAwsRum} from '@nlabs/metropolisjs';

const SaveButton = () => {
  const rum = useAwsRum();

  const onSave = () => {
    rum.track({
      name: 'settings_saved',
      path: '/settings',
      properties: {section: 'profile'},
      type: 'click'
    });
  };

  return <button onClick={onSave}>Save</button>;
};

For an application-controlled terminal flush, request beacon delivery explicitly:

await rum.flush({useBeacon: true});

Calling flush({useBeacon: true}) is safe in non-browser environments and older browsers because it falls back to the normal RUM request when the Beacon API cannot be used.

Configuration Object

The config prop accepts a MetropolisConfiguration object that supports environment-specific settings:

<Metropolis
  config={{
    // Environment-specific configurations
    development: {
      environment: 'development',
      app: {
        api: {
          url: 'http://localhost:3000/app',
          public: 'http://localhost:3000/public',
          uploadImage: 'http://localhost:3000/upload'
        },
        urls: {
          websocket: 'ws://localhost:3000'
        },
        session: {
          maxMinutes: 1440, // 24 hours
          minMinutes: 15
        },
        name: 'My App',
        version: '1.0.0'
      },
      isAuth: () => {
        // Custom authentication check
        const session = flux.getState('user.session', {});
        return !!session.userActive;
      }
    },
    production: {
      environment: 'production',
      app: {
        api: {
          url: 'https://api.example.com/app',
          public: 'https://api.example.com/public',
          uploadImage: 'https://api.example.com/upload'
        },
        urls: {
          websocket: 'wss://api.example.com'
        },
        session: {
          maxMinutes: 2880, // 48 hours
          minMinutes: 30
        }
      }
    }
  }}
>
  <YourApp />
</Metropolis>

Configuration Options

Environment Configuration (MetropolisEnvironmentConfiguration):

  • environment: 'development' | 'production' | 'test' | 'local' - Current environment
  • app: Application-specific configuration
    • api: API endpoint configuration
      • endpoints: Named external REST endpoints for restActions
      • url: Main API endpoint
      • public: Public API endpoint
      • uploadImage: Image upload endpoint
    • urls: Additional URL configurations
      • websocket: WebSocket server URL
    • session: Session management settings
      • maxMinutes: Maximum session duration in minutes
      • minMinutes: Minimum session duration in minutes
    • name: Application name
    • version: Application version
  • isAuth: Function that returns a boolean indicating if the user is authenticated
  • adapters: Custom adapters (can also be passed as a separate prop)

Custom Adapters

Pass custom adapters to override default data transformation behavior:

import {parseUser, parseMessage} from '@nlabs/metropolisjs';

<Metropolis
  adapters={{
    User: parseUser,
    Message: parseMessage,
    // Add other custom adapters as needed
    Content: customContentAdapter,
    Event: customEventAdapter,
    Image: customImageAdapter,
    Location: customLocationAdapter,
    Post: customPostAdapter,
    Persona: customPersonaAdapter,
    Reaction: customReactionAdapter,
    Tag: customTagAdapter,
    Translation: customTranslationAdapter
  }}
>
  <YourApp />
</Metropolis>

Translations

MetropolisJS supports both simple and complex translation formats:

Simple Translations

<Metropolis
  translations={{
    'welcome': 'Welcome to MetropolisJS!',
    'save': 'Save',
    'cancel': 'Cancel',
    'hello_user': 'Hello {{name}}!',
    'items_count': 'You have {{count}} items'
  }}
>
  <YourApp />
</Metropolis>

Complex Translations (with locale and namespace)

<Metropolis
  translations={{
    'welcome': {
      value: 'Welcome to MetropolisJS!',
      locale: 'en',
      namespace: 'common'
    },
    'save': {
      value: 'Save',
      locale: 'en',
      namespace: 'actions'
    }
  }}
>
  <YourApp />
</Metropolis>

Complete Configuration Example

import {Metropolis} from '@nlabs/metropolisjs';
import {parseUser, parseMessage} from '@nlabs/metropolisjs';

const App = () => {
  return (
    <Metropolis
      config={{
        development: {
          environment: 'development',
          app: {
            api: {
              url: 'http://localhost:3000/app',
              public: 'http://localhost:3000/public'
            },
            urls: {
              websocket: 'ws://localhost:3000'
            },
            session: {
              maxMinutes: 1440,
              minMinutes: 15
            }
          },
          isAuth: () => {
            // Your custom auth logic
            return true;
          }
        },
        production: {
          environment: 'production',
          app: {
            api: {
              url: 'https://api.example.com/app',
              public: 'https://api.example.com/public'
            },
            urls: {
              websocket: 'wss://api.example.com'
            }
          }
        }
      }}
      adapters={{
        User: parseUser,
        Message: parseMessage
      }}
      translations={{
        'welcome': 'Welcome!',
        'save': 'Save',
        'cancel': 'Cancel'
      }}
    >
      <YourApp />
    </Metropolis>
  );
};

Environment Detection

MetropolisJS automatically detects the environment based on:

  1. The environment property in your config
  2. process.env.stage (if set)
  3. process.env.NODE_ENV (fallback)
  4. Defaults to 'local' if none are set

The configuration system will merge environment-specific settings with default values, allowing you to override only what you need.

User Authentication Example

import {useUserActions} from '@nlabs/metropolisjs';

const LoginForm = () => {
  const userActions = useUserActions(); // Specialized hook - only creates user actions
  const [username, setUsername] = useState('');
  const [password, setPassword] = useState('');

  const handleLogin = async () => {
    try {
      const session = await userActions.signIn({password, username});
      console.log('User logged in successfully!', session);
    } catch (error) {
      console.error('Login failed:', error);
    }
  };

  return (
    <form onSubmit={handleLogin}>
      <input
        value={username}
        onChange={(e) => setUsername(e.target.value)}
        placeholder="Username"
      />
      <input
        type="password"
        value={password}
        onChange={(e) => setPassword(e.target.value)}
        placeholder="Password"
      />
      <button type="submit">Sign In</button>
    </form>
  );
};

Billing Setup Sessions

Billing cards are collected through a hosted setup session, so raw card details never pass through application code. Start the flow with an authenticated user action and redirect the browser to the returned checkout URL:

import {useUserActions} from '@nlabs/metropolisjs';

const AddBillingCardButton = () => {
  const userActions = useUserActions();

  const addBillingCard = async () => {
    const returnUrl = `${window.location.origin}/settings/billing/complete`;
    const checkoutUrl = await userActions.createBillingSetupSession(returnUrl);
    window.location.assign(checkoutUrl);
  };

  return <button onClick={addBillingCard}>Add billing card</button>;
};

On the return page, read the provider's setup-session identifier and complete the flow. The action returns the updated user, refreshes the matching session data, dispatches the standard user update event, and clears related user request caches:

const sessionId = new URLSearchParams(window.location.search).get('session_id');

if(sessionId) {
  const user = await userActions.completeBillingSetupSession(sessionId, [
    'stripeCardBrand',
    'stripeCardLast4'
  ]);
}

Use deleteBillingCard() to remove the saved billing method. Both completion and deletion return sanitized billing metadata; MetropolisJS does not accept raw card numbers or tokens.

Real-Time Messaging

import {useMessageActions, useWebsocketActions} from '@nlabs/metropolisjs';

const ChatComponent = () => {
  const messageActions = useMessageActions();
  const websocketActions = useWebsocketActions();
  const [messages, setMessages] = useState([]);

  useEffect(() => {
    // Initialize WebSocket connection
    websocketActions.wsInit();

    // Load existing messages
    messageActions.list().then(setMessages);
  }, []);

  const sendMessage = async (content) => {
    await messageActions.add({ content });
    // Message automatically appears in real-time for all connected users!
  };

  return (
    <div>
      {messages.map(message => (
        <div key={message.messageId}>{message.content}</div>
      ))}
      <button onClick={() => sendMessage('Hello World!')}>
        Send Message
      </button>
    </div>
  );
};

Architecture

MetropolisJS sits between React and your services. The provider supplies configuration and action access, adapters keep data typed, Rip-Hunter handles transport, and ArkhamJS keeps application state reactive.

Core Components

  • Actions: Handle all API interactions and business logic (factory pattern)
  • Adapters: Transform data between frontend and backend formats
  • Stores: Manage application state with ArkhamJS
  • WebSocket Actions: Handle real-time communication
  • Configuration: Context-based configuration (React best practices)
  • Hooks: Specialized hooks for accessing actions and configuration

Request and State Lifecycle

Actions hide the full request lifecycle behind a typed method. They validate inputs, use the request cache when configured, communicate through Rip-Hunter, update the ArkhamJS store, and then dispatch Flux events to subscribers. WebSocket and SSE messages enter the same reactive state flow.

Modern Architecture Features

MetropolisJS follows React best practices:

  • Context-Based Configuration: No global state, configuration through React Context
  • Factory Pattern: Functional action creation with dependency injection
  • Selective Action Creation: Only create actions you need for better performance
  • Type Safety: Full TypeScript support with proper type inference
  • Specialized Hooks: Individual hooks for each action type
  • Flux State Integration: Configuration stored in flux state for non-React code access

Available Actions

MetropolisJS provides comprehensive actions for all your needs. Access them using specialized hooks or useMetropolis(actionTypes):

Specialized Hooks (Recommended)

  • useAwsRum() - Analytics tracking, batching, and terminal beacon delivery
  • useUserActions() - Authentication, personas, user management
  • useMessageActions() - Real-time messaging and conversations
  • usePermissionActions() - Permission and role management (RBAC)
  • usePostActions() - Social media posts and content
  • useReactionActions() - Likes, reactions, and interactions
  • useTagActions() - Content categorization and discovery
  • useEventActions() - Event management and scheduling
  • useImageActions() - Media upload and management
  • useLocationActions() - Geolocation and location-based features
  • useWebsocketActions() - Real-time communication setup
  • useContentActions() - Content management
  • usePersonaActions() - Persona management
  • useTranslationActions() - Translation management

Using Collection Actions

// Recommended: Use specialized hooks (only creates what you need)
const userActions = useUserActions();
const postActions = usePostActions();
const permissionActions = usePermissionActions();

// Alternative: Selective creation
const {userActions, postActions, permissionActions} = useMetropolis(['user', 'post', 'permission']);

// Create every action you use explicitly
const {messageActions, permissionActions} = useMetropolis(['message', 'permission']);

Factory Pattern Guide

MetropolisJS uses a factory function pattern for actions. This provides functional composition, straightforward testing, and dependency injection.

Key Benefits

  1. Functional Programming: Pure functions instead of classes with side effects
  2. Better Testability: Easier to mock and test individual functions
  3. Composability: Actions can be easily combined and extended
  4. Dependency Injection: Custom adapters can be injected and merged with defaults

Basic Usage

import {createAction, createActions, createUserActions} from '@nlabs/metropolisjs';

const userActions = createUserActions(flux);
const user = await userActions.addUser(userData);

const postActions = createAction('post', flux);
const post = await postActions.add({content: 'Hello!'});

const actions = createActions(['user', 'post', 'message'], flux);
await actions.message.sendMessage({
  content: 'Welcome!',
  recipientId: user.userId
});

Factory results preserve their selected types. createAction('post', flux) returns PostActions, while createActions(['user', 'post'], flux) returns only typed user and post keys. createAllActions(flux) returns the complete ActionMap.

Advanced Usage with Custom Adapters

Custom Validation Adapter

import type {User} from '@nlabs/metropolisjs';

// Custom adapter that extends default behavior
const customUserAdapter = (input: unknown): User => {
  // input is already validated by default adapter
  const user = input as User;

  // Add business-specific validation
  if (user.email && !user.email.includes('@company.com')) {
    throw new Error('Only company emails allowed');
  }

  // Add computed fields
  return {
    ...user,
    fullName: `${user.firstName || ''} ${user.lastName || ''}`.trim(),
    isAdmin: (user.userAccess || 0) >= 3
  };
};

const userActions = createUserActions(flux, {
  userAdapter: customUserAdapter
});

Configuration-based Adapters

const userActions = createUserActions(flux, {
  userAdapterOptions: {
    strict: true,
    environment: 'production',
    customValidation: (input) => {
      // Additional validation logic
      return input;
    }
  }
});

Runtime Adapter Updates

const userActions = createUserActions(flux);

// Update adapter at runtime
userActions.updateUserAdapter(customUserAdapter);

// Update options at runtime
userActions.updateUserAdapterOptions({
  strict: true,
  environment: 'production'
});

Available Factory Functions

All action files now export factory functions:

  • createUserActions(flux, options?) - User management
  • createPostActions(flux, options?) - Post management
  • createEventActions(flux, options?) - Event management
  • createMessageActions(flux, options?) - Messaging
  • createImageActions(flux, options?) - Image handling
  • createLocationActions(flux, options?) - Location services
  • createReactionActions(flux, options?) - Reactions
  • createTagActions(flux, options?) - Tag management
  • createWebsocketActions(flux) - WebSocket connections

Adapter Options Interface

All adapters support the same options interface:

interface AdapterOptions {
  strict?: boolean;                    // Enable strict validation
  allowPartial?: boolean;              // Allow partial data
  environment?: 'development' | 'production' | 'test';
  customValidation?: (input: unknown) => unknown;
}

Using Actions in Components

The recommended approach is to use specialized hooks:

// Recommended: Use specialized hooks
import {useUserActions, usePostActions} from '@nlabs/metropolisjs';

const MyComponent = () => {
  const userActions = useUserActions();
  const postActions = usePostActions();
  // ...
};

// Alternative: Use useMetropolis with selective creation
import {useMetropolis} from '@nlabs/metropolisjs';

const MyComponent = () => {
  const {userActions, postActions} = useMetropolis(['user', 'post']);
  // ...
};

Configuration Access

Access configuration using React hooks:

import {useMetropolisConfig} from '@nlabs/metropolisjs';

const MyComponent = () => {
  const config = useMetropolisConfig();
  const apiUrl = config.app?.api?.url;
  // ...
};

For non-React code (actions, utilities), use:

import {getConfigFromFlux} from '@nlabs/metropolisjs';

const config = getConfigFromFlux(flux);
const apiUrl = config.app?.api?.url || '';

Note: The Config class has been removed. Use useMetropolisConfig() in React components or getConfigFromFlux() in non-React code.

Testing Examples

Unit Testing Actions

import {createUserActions, type UserActions} from '@nlabs/metropolisjs';

describe('userActions', () => {
  let flux: FluxFramework;
  let userActions: UserActions;

  beforeEach(() => {
    flux = createMockFlux();
    // Store config in flux state for actions to access
    flux.setState('app.config', {
      app: { api: { url: 'http://localhost:3000/app' } }
    });
    userActions = createUserActions(flux);
  });

  it('should add user with validation', async () => {
    const userData = {username: 'test', email: '[email protected]'};
    const result = await userActions.addUser(userData);
    expect(result).toBeDefined();
  });
});

Testing with Custom Adapters

const mockAdapter = vi.fn((input) => ({
  ...input,
  validated: true
}));

const userActions = createUserActions(flux, {
  userAdapter: mockAdapter
});

expect(mockAdapter).toHaveBeenCalled();

Testing React Components

import {renderHook} from '@testing-library/react';
import {Metropolis, useUserActions} from '@nlabs/metropolisjs';

describe('useUserActions', () => {
  it('should return user actions', () => {
    const wrapper = ({children}) => (
      <Metropolis config={{development: {app: {api: {url: 'http://localhost'}}}}}>
        {children}
      </Metropolis>
    );

    const {result} = renderHook(() => useUserActions(), {wrapper});
    expect(result.current).toBeDefined();
    expect(result.current.add).toBeDefined();
  });
});

Best Practices

  1. Use Specialized Hooks: Prefer useUserActions() over useMetropolis(['user']) when you only need one action type
  2. Selective Creation: Use useMetropolis(['user', 'post']) when you need multiple specific actions
  3. Context-Based Config: Use useMetropolisConfig() for accessing configuration
  4. Leverage Adapter Injection: Pass custom adapters through the Metropolis component
  5. Type Safety: Always use TypeScript interfaces for better type checking
  6. Error Handling: Custom adapters should throw meaningful errors
  7. Wrap Components: Always wrap components using hooks with <Metropolis> provider

Removed APIs

The following APIs have been removed:

  • Config.get() - Use useMetropolisConfig() in React components or getConfigFromFlux(flux) in non-React code
  • Config.set() - Pass configuration to the <Metropolis> component instead

Performance Considerations

  • Specialized hooks only create the specific action type (best performance)
  • Selective creation creates only requested actions
  • Factory functions are lightweight and create minimal overhead
  • Adapter validation is only performed when needed
  • Options are merged efficiently without deep cloning
  • Better tree-shaking opportunities with specialized hooks

Adapters

Customize data transformation with powerful adapters. Pass adapters to the Metropolis component:

  • User: User account and authentication data
  • Permission: Permission and role data with RBAC support
  • Message: Chat and messaging data
  • Post: Social media content
  • Event: Event and scheduling data
  • Image: Media and file data
  • Location: Geolocation data
  • Tag: Categorization data
  • Reaction: User interaction data
  • Content: Content management
  • Persona: Persona management
  • Translation: Translation data

Real-Time Features

WebSocket Integration

import {useWebsocketActions} from '@nlabs/metropolisjs';

const MyComponent = () => {
  const websocketActions = useWebsocketActions();

  useEffect(() => {
    // Initialize real-time connections
    websocketActions.wsInit();
  }, []);

  // Messages, notifications, and data updates
  // are automatically synchronized across all clients
};

Server-Sent Events

Built-in SSE support for lightweight real-time updates without the overhead of WebSocket connections.

Customization

Custom Data Adapters

You can customize data transformation by providing custom adapters. Adapters are functions that transform and validate data:

// Custom adapter function
const customUserAdapter = (input: unknown, options?: UserAdapterOptions) => {
  const user = input as any;

  // Add custom transformation logic
  return {
    ...user,
    displayName: `${user.firstName || ''} ${user.lastName || ''}`.trim(),
    customField: 'custom value',
    isVerified: user.email?.endsWith('@company.com')
  };
};

// Pass adapters through the Metropolis component (recommended)
<Metropolis
  adapters={{
    User: customUserAdapter,
    Message: customMessageAdapter
  }}
>
  <YourApp />
</Metropolis>

The adapters will be automatically used by all action hooks. For more details on the factory pattern and adapter customization, see the Factory Pattern Guide section above.

Accessing Configuration

import {useMetropolisConfig} from '@nlabs/metropolisjs';

const MyComponent = () => {
  const config = useMetropolisConfig();
  const apiUrl = config.app?.api?.url;
  const websocketUrl = config.app?.urls?.websocket;

  // Use configuration values
  return <div>API: {apiUrl}</div>;
};

Note: useMetropolisConfig() must be used within a component wrapped by <Metropolis>.

Performance Features

  • Selective Action Creation - Only create actions you need with specialized hooks
  • Debounced API calls - Prevent excessive requests
  • Intelligent caching - With ArkhamJS
  • Optimistic updates - Instant UI feedback
  • Connection pooling - WebSocket efficiency
  • Lazy loading - Support for large datasets
  • Tree-shaking friendly - Better bundle optimization

Security

  • Automatic token refresh for seamless sessions
  • Secure WebSocket connections with authentication
  • Input validation and sanitization
  • CSRF protection built-in
  • Session management with configurable timeouts
  • 5-Level RBAC permission system for granular access control

Permission System (RBAC)

MetropolisJS includes a comprehensive 5-level Role-Based Access Control (RBAC) system that integrates seamlessly with the Reaktor backend. This system provides granular control over user permissions and access levels throughout your application.

Permission Levels

The system defines five hierarchical permission levels:

enum PermissionLevel {
  GUEST = 0,        // Unauthenticated users
  USER = 1,         // Authenticated users
  MODERATOR = 2,    // Content moderators
  ADMIN = 3,        // Application administrators
  SUPER_ADMIN = 4   // System administrators
}

| Level | Name | Value | Description | |-------|------|-------|-------------| | 0 | Guest | PermissionLevel.GUEST | Unauthenticated users with limited read access | | 1 | User | PermissionLevel.USER | Authenticated users who can create and edit their own content | | 2 | Moderator | PermissionLevel.MODERATOR | Can moderate content and manage users | | 3 | Admin | PermissionLevel.ADMIN | Full access to application features | | 4 | Super Admin | PermissionLevel.SUPER_ADMIN | Complete system access, can manage admins |

Using the Permission System

1. Permission Hook

The usePermissions() hook provides comprehensive permission checking capabilities:

import { usePermissions, PermissionLevel } from '@nlabs/metropolisjs';

const MyComponent = () => {
  const {
    userLevel,      // Current user's permission level
    isGuest,        // Boolean checks
    isUser,
    isModerator,
    isAdmin,
    isSuperAdmin,
    hasPermission,  // Function to check specific level
    checkResource   // Function to check resource-specific permissions
  } = usePermissions();

  return (
    <div>
      <p>Your level: {userLevel}</p>
      {isAdmin && <button>Admin Panel</button>}
      {hasPermission(PermissionLevel.MODERATOR) && (
        <button>Moderate Content</button>
      )}
    </div>
  );
};

2. Permission Guard Component

Conditionally render components based on permission requirements:

import { PermissionGuard, PermissionLevel } from '@nlabs/metropolisjs';

const ProtectedContent = () => {
  return (
    <>
      <PermissionGuard
        requiredLevel={PermissionLevel.USER}
        fallback={<p>Please log in to view this content</p>}
      >
        <p>This content is visible to authenticated users</p>
      </PermissionGuard>

      <PermissionGuard
        requiredLevel={PermissionLevel.ADMIN}
        fallback={null} // Hides content completely
      >
        <button>Admin Settings</button>
      </PermissionGuard>

      <PermissionGuard
        requiredLevel={PermissionLevel.MODERATOR}
        resource="posts" // Resource-specific permission
        fallback={<p>Moderator access required</p>}
      >
        <button>Moderate Posts</button>
      </PermissionGuard>
    </>
  );
};

3. Managing Permissions with Actions

Use usePermissionActions() to manage permissions programmatically:

import { usePermissionActions, PermissionLevel } from '@nlabs/metropolisjs';

const PermissionManager = () => {
  const permissionActions = usePermissionActions();

  const grantModeratorRole = async (userId: string) => {
    try {
      const permission = await permissionActions.add({
        userId,
        name: 'Moderator Role',
        level: PermissionLevel.MODERATOR,
        resource: 'posts',
        description: 'Can moderate posts and comments'
      });
      console.log('Permission granted:', permission);
    } catch (error) {
      console.error('Failed to grant permission:', error);
    }
  };

  const checkUserAccess = async (userId: string) => {
    const hasAccess = await permissionActions.check(
      userId,
      'posts',
      PermissionLevel.MODERATOR
    );
    return hasAccess;
  };

  const loadUserPermissions = async (userId: string) => {
    const permissions = await permissionActions.listByUser(userId);
    return permissions;
  };

  return (
    <div>
      <button onClick={() => grantModeratorRole('user123')}>
        Grant Moderator Permission
      </button>
    </div>
  );
};

4. Resource-Specific Permissions

The permission system supports resource-specific access control:

import { usePermissions, PermissionLevel } from '@nlabs/metropolisjs';

const ResourceProtectedComponent = () => {
  const { checkResource } = usePermissions();

  const canEditPosts = checkResource('posts', PermissionLevel.MODERATOR);
  const canDeleteUsers = checkResource('users', PermissionLevel.ADMIN);
  const canViewReports = checkResource('reports', PermissionLevel.MODERATOR);

  return (
    <div>
      {canEditPosts && <button>Edit Post</button>}
      {canDeleteUsers && <button>Delete User</button>}
      {canViewReports && <button>View Reports</button>}
    </div>
  );
};

Permission Levels

The userAccess field uses the numeric PermissionLevel values from 0 through 4.

Available Permission Actions

The usePermissionActions() hook provides the following methods:

  • add(permissionData) - Grant a new permission
  • check(userId, resource, requiredLevel) - Check if a user has access
  • itemById(permissionId) - Retrieve a specific permission
  • list(from?, to?) - List all permissions with pagination
  • listByUser(userId) - Get all permissions for a specific user
  • remove(permissionId) - Revoke a permission
  • update(permission) - Update an existing permission

Integration with Reaktor Backend

The permission system integrates with the Reaktor backend through GraphQL mutations and queries:

# Grant permission
mutation {
  permissions {
    add(permission: {
      userId: "user123"
      level: 2
      resource: "posts"
      name: "Moderator"
    }) {
      permissionId
      level
      resource
    }
  }
}

# Check permission
query {
  permissions {
    check(
      userId: "user123"
      resource: "posts"
      requiredLevel: 2
    )
  }
}

# List user permissions
query {
  permissions {
    listByUser(userId: "user123") {
      permissionId
      name
      level
      resource
      description
    }
  }
}

Best Practices

  1. Use Permission Guards for UI: Wrap sensitive UI elements with <PermissionGuard> components
  2. Check Permissions in Actions: Always verify permissions before executing sensitive operations
  3. Resource-Specific Permissions: Use resource-based permissions for fine-grained control
  4. Leverage Boolean Helpers: Use isAdmin, isModerator, etc. for cleaner code
  5. Test Permission Logic: Write tests for permission checks to ensure security
  6. Document Required Levels: Clearly document what permission level each feature requires

Example: Complete Permission Workflow

import {
  Metropolis,
  usePermissions,
  usePermissionActions,
  PermissionGuard,
  PermissionLevel
} from '@nlabs/metropolisjs';

const App = () => {
  return (
    <Metropolis config={{/* your config */}}>
      <Dashboard />
    </Metropolis>
  );
};

const Dashboard = () => {
  const { isAdmin, isModerator, userLevel } = usePermissions();
  const permissionActions = usePermissionActions();

  return (
    <div>
      <h1>Dashboard - Level: {userLevel}</h1>

      {/* Everyone sees this */}
      <section>
        <h2>Public Content</h2>
      </section>

      {/* Only authenticated users */}
      <PermissionGuard requiredLevel={PermissionLevel.USER}>
        <section>
          <h2>User Content</h2>
          <button>Create Post</button>
        </section>
      </PermissionGuard>

      {/* Only moderators and above */}
      {isModerator && (
        <section>
          <h2>Moderation Tools</h2>
          <button>Review Content</button>
        </section>
      )}

      {/* Only admins */}
      {isAdmin && (
        <section>
          <h2>Admin Panel</h2>
          <button>Manage Users</button>
          <button>System Settings</button>
        </section>
      )}
    </div>
  );
};

For more detailed examples, see examples/permission-system-usage.tsx.

📦 Installation & Setup

Prerequisites

  • Node.js 16+
  • React 19+
  • TypeScript 7+

Full Installation

# Install MetropolisJS and dependencies
npm install @nlabs/metropolisjs @nlabs/arkhamjs @nlabs/arkhamjs-utils-react

# For development
npm install --save-dev @types/react @types/node

Environment Setup

MetropolisJS automatically detects the environment from:

  • process.env.stage (if set)
  • process.env.NODE_ENV (fallback)
  • Defaults to 'local' if none are set

Configure your environment-specific settings in the config prop of the Metropolis component.

Contributing

Before opening a pull request, run the same quality gates used for source, tests, examples, and the published declarations:

npm run lint
npm run typecheck
npm test
npm run build

npm run typecheck checks the production source, unit and integration tests, lint inputs, and every file under examples/.

To contribute:

  1. Fork the repository
  2. Create a feature branch (git checkout -b feature/amazing-feature)
  3. Commit your changes (git commit -m 'Add amazing feature')
  4. Push to the branch (git push origin feature/amazing-feature)
  5. Open a Pull Request

License

This project is licensed under the MIT License - see the LICENSE file for details.

Additional Documentation

Support

About Nitrogen Labs

MetropolisJS is proudly developed by Nitrogen Labs, a team passionate about building powerful, developer-friendly tools that make web development faster, more reliable, and more enjoyable.


Ready to build the future? Start with MetropolisJS today and experience the power of seamless frontend-backend integration! 🚀

CRUD Operations

MetropolisJS now provides comprehensive CRUD (Create, Read, Update, Delete) operations for all Reaktor collections:

Supported Collections

  • users - User accounts and authentication data
  • posts - User posts and content
  • groups - Group entities and communities
  • messages - Direct messages
  • conversations - Conversation threads
  • files - File attachments
  • images - Image media
  • videos - Video media
  • apps - Application entities
  • personas - Extended user persona details
  • tags - Tag entities for categorization

Basic CRUD Example

import {useFlux} from '@nlabs/arkhamjs-utils-react';
import {createPostActions, createGroupActions} from '@nlabs/metropolisjs';

const MyComponent = () => {
  const flux = useFlux();
  const postActions = createPostActions(flux);
  const groupActions = createGroupActions(flux);

  // Create
  const createPost = async () => {
    const post = await postActions.add({
      content: "Hello World",
      userId: "user123",
      name: "My First Post"
    });
  };

  // Read
  const getPost = async (postId: string) => {
    const post = await postActions.itemById(postId);
  };

  // Update
  const updatePost = async (postId: string) => {
    await postActions.update({
      postId,
      content: "Updated content"
    });
  };

  // Delete
  const deletePost = async (postId: string) => {
    await postActions.delete(postId);
  };

  // List with pagination
  const listPosts = async () => {
    const posts = await postActions.listByLatest(0, 10);
  };
};

Relationship Management

Connect and manage relationships between collections:

import {createConnectionActions, CONNECTION_TYPES} from '@nlabs/metropolisjs';

const connectionActions = createConnectionActions(flux);

// Add user to group
await connectionActions.addConnection(
  'users',
  userId,
  'groups',
  groupId,
  CONNECTION_TYPES.MEMBER
);

// Get all connections
const connections = await connectionActions.getConnections('users', userId);

// Remove connection
await connectionActions.removeConnection('users', userId, 'groups', groupId);

Extensible Fields

All CRUD operations support custom fields:

const post = await postActions.add({
  content: "Hello",
  userId: "user123",
  // Custom fields
  customField1: "value",
  customField2: 42,
  metadata: {
    source: "mobile",
    version: "1.0"
  }
});

Documentation

For comprehensive guides and examples, see: