@nlabs/metropolisjs
v1.1.1
Published
Type-safe frontend integration for authentication, data, messaging, realtime events, billing, and Reaktor APIs.
Maintainers
Readme
MetropolisJS: Seamless Frontend-Backend Integration Framework
The Ultimate Frontend Integration Library for Modern Web Applications
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-reactBasic 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.sendBeaconis unavailable, throws, or declines the payload, MetropolisJS immediately falls back to its normal asynchronous RUM request. - A batch accepted by
sendBeaconis not sent again through the normal request path. - Scheduled and explicit
flush()calls continue to use the normal request path unlessuseBeaconis requested. - Privacy signals and the
enabledoption 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 environmentapp: Application-specific configurationapi: API endpoint configurationendpoints: Named external REST endpoints forrestActionsurl: Main API endpointpublic: Public API endpointuploadImage: Image upload endpoint
urls: Additional URL configurationswebsocket: WebSocket server URL
session: Session management settingsmaxMinutes: Maximum session duration in minutesminMinutes: Minimum session duration in minutes
name: Application nameversion: Application version
isAuth: Function that returns a boolean indicating if the user is authenticatedadapters: 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:
- The
environmentproperty in your config process.env.stage(if set)process.env.NODE_ENV(fallback)- 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 deliveryuseUserActions()- Authentication, personas, user managementuseMessageActions()- Real-time messaging and conversationsusePermissionActions()- Permission and role management (RBAC)usePostActions()- Social media posts and contentuseReactionActions()- Likes, reactions, and interactionsuseTagActions()- Content categorization and discoveryuseEventActions()- Event management and schedulinguseImageActions()- Media upload and managementuseLocationActions()- Geolocation and location-based featuresuseWebsocketActions()- Real-time communication setupuseContentActions()- Content managementusePersonaActions()- Persona managementuseTranslationActions()- 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
- Functional Programming: Pure functions instead of classes with side effects
- Better Testability: Easier to mock and test individual functions
- Composability: Actions can be easily combined and extended
- 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 managementcreatePostActions(flux, options?)- Post managementcreateEventActions(flux, options?)- Event managementcreateMessageActions(flux, options?)- MessagingcreateImageActions(flux, options?)- Image handlingcreateLocationActions(flux, options?)- Location servicescreateReactionActions(flux, options?)- ReactionscreateTagActions(flux, options?)- Tag managementcreateWebsocketActions(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
- Use Specialized Hooks: Prefer
useUserActions()overuseMetropolis(['user'])when you only need one action type - Selective Creation: Use
useMetropolis(['user', 'post'])when you need multiple specific actions - Context-Based Config: Use
useMetropolisConfig()for accessing configuration - Leverage Adapter Injection: Pass custom adapters through the
Metropoliscomponent - Type Safety: Always use TypeScript interfaces for better type checking
- Error Handling: Custom adapters should throw meaningful errors
- Wrap Components: Always wrap components using hooks with
<Metropolis>provider
Removed APIs
The following APIs have been removed:
Config.get()- UseuseMetropolisConfig()in React components orgetConfigFromFlux(flux)in non-React codeConfig.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 permissioncheck(userId, resource, requiredLevel)- Check if a user has accessitemById(permissionId)- Retrieve a specific permissionlist(from?, to?)- List all permissions with paginationlistByUser(userId)- Get all permissions for a specific userremove(permissionId)- Revoke a permissionupdate(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
- Use Permission Guards for UI: Wrap sensitive UI elements with
<PermissionGuard>components - Check Permissions in Actions: Always verify permissions before executing sensitive operations
- Resource-Specific Permissions: Use resource-based permissions for fine-grained control
- Leverage Boolean Helpers: Use
isAdmin,isModerator, etc. for cleaner code - Test Permission Logic: Write tests for permission checks to ensure security
- 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/nodeEnvironment 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 buildnpm run typecheck checks the production source, unit and integration tests, lint inputs, and every file under examples/.
To contribute:
- Fork the repository
- Create a feature branch (
git checkout -b feature/amazing-feature) - Commit your changes (
git commit -m 'Add amazing feature') - Push to the branch (
git push origin feature/amazing-feature) - Open a Pull Request
License
This project is licensed under the MIT License - see the LICENSE file for details.
Additional Documentation
- Actions Reference - Action families, hooks, factories, and RUM beacon delivery
- Factory Pattern Guide - Detailed guide on using the factory pattern
- Architecture Analysis - Deep dive into the architecture
- Changelog - Complete list of changes and improvements
Support
- Documentation: See above for detailed guides
- Issues: GitHub Issues
- Discussions: GitHub Discussions
- Email: [email protected]
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:
- CRUD Integration Guide - Complete guide to CRUD operations
- Actions Reference - Action families, hooks, creators, and method map
- Collections Reference - Detailed information about each collection
- Connections Guide - Managing relationships between collections
- CRUD Examples - Practical code examples
- Connection Examples - Relationship management examples
- Extensibility Examples - Custom field examples
