MERN stack patterns including React with Vite, Express middleware, MongoDB schemas, API Gateway architecture, session management, error handling, and testing strategies. Activate for MERN development, microservices architecture, and full-stack JavaScript applications.
/plugin marketplace add Lobbi-Docs/claude/plugin install lobbi-platform-manager@claude-orchestrationThis skill is limited to using the following tools:
Comprehensive MERN stack development patterns for the keycloak-alpha multi-tenant platform with 8 microservices.
Activate this skill when:
keycloak-alpha/
├── apps/
│ ├── web-app/ # Main React + Vite SPA
│ │ ├── src/
│ │ │ ├── components/
│ │ │ ├── pages/
│ │ │ ├── hooks/
│ │ │ ├── contexts/
│ │ │ ├── config/
│ │ │ ├── utils/
│ │ │ └── main.jsx
│ │ ├── vite.config.js
│ │ └── package.json
│ └── admin-portal/ # Admin dashboard (React + Vite)
│
├── services/
│ ├── api-gateway/ # Express API Gateway
│ ├── user-service/ # User management
│ ├── org-service/ # Organization management
│ ├── tenant-service/ # Multi-tenant provisioning
│ ├── notification-service/ # Email/SMS notifications
│ ├── billing-service/ # Stripe integration
│ ├── analytics-service/ # Usage analytics
│ └── keycloak-service/ # Keycloak integration
│
├── routes/
│ ├── api/
│ │ ├── users.js
│ │ ├── organizations.js
│ │ └── tenants.js
│ └── index.js
│
├── shared/
│ ├── types/
│ ├── utils/
│ └── constants/
│
└── package.json
// apps/web-app/vite.config.js
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
import path from 'path';
export default defineConfig({
plugins: [react()],
resolve: {
alias: {
'@': path.resolve(__dirname, './src'),
'@components': path.resolve(__dirname, './src/components'),
'@hooks': path.resolve(__dirname, './src/hooks'),
'@contexts': path.resolve(__dirname, './src/contexts'),
'@utils': path.resolve(__dirname, './src/utils'),
'@config': path.resolve(__dirname, './src/config'),
}
},
server: {
port: 3000,
proxy: {
'/api': {
target: 'http://localhost:4000',
changeOrigin: true,
}
}
},
build: {
outDir: 'dist',
sourcemap: true,
rollupOptions: {
output: {
manualChunks: {
'vendor': ['react', 'react-dom', 'react-router-dom'],
'keycloak': ['keycloak-js'],
'ui': ['@chakra-ui/react', '@emotion/react']
}
}
}
},
optimizeDeps: {
include: ['react', 'react-dom', 'keycloak-js']
}
});
// apps/web-app/src/components/features/UserProfile/index.jsx
import { useState, useEffect } from 'react';
import { useAuth } from '@hooks/useAuth';
import { useToast } from '@chakra-ui/react';
import { updateUserProfile } from '@/api/users';
export function UserProfile() {
const { user } = useAuth();
const toast = useToast();
const [profile, setProfile] = useState(null);
const [loading, setLoading] = useState(false);
useEffect(() => {
fetchProfile();
}, []);
async function fetchProfile() {
setLoading(true);
try {
const data = await getUserProfile(user.sub);
setProfile(data);
} catch (error) {
toast({
title: 'Error loading profile',
description: error.message,
status: 'error',
duration: 5000,
});
} finally {
setLoading(false);
}
}
async function handleSubmit(formData) {
try {
await updateUserProfile(user.sub, formData);
toast({
title: 'Profile updated',
status: 'success',
duration: 3000,
});
} catch (error) {
toast({
title: 'Update failed',
description: error.message,
status: 'error',
duration: 5000,
});
}
}
if (loading) return <Spinner />;
if (!profile) return <Alert>Profile not found</Alert>;
return <ProfileForm profile={profile} onSubmit={handleSubmit} />;
}
// apps/web-app/src/hooks/useAuth.js
import { createContext, useContext, useState, useEffect } from 'react';
import Keycloak from 'keycloak-js';
import { keycloakConfig } from '@config/keycloak.config';
const AuthContext = createContext(null);
export function AuthProvider({ children }) {
const [keycloak, setKeycloak] = useState(null);
const [authenticated, setAuthenticated] = useState(false);
const [user, setUser] = useState(null);
const [loading, setLoading] = useState(true);
useEffect(() => {
const kc = new Keycloak(keycloakConfig);
kc.init({
onLoad: 'check-sso',
checkLoginIframe: true,
pkceMethod: 'S256'
}).then(authenticated => {
setKeycloak(kc);
setAuthenticated(authenticated);
if (authenticated) {
setUser({
sub: kc.tokenParsed.sub,
email: kc.tokenParsed.email,
name: kc.tokenParsed.name,
orgId: kc.tokenParsed.org_id,
roles: kc.tokenParsed.realm_access?.roles || []
});
}
setLoading(false);
});
// Token refresh
kc.onTokenExpired = () => {
kc.updateToken(30).catch(() => {
kc.logout();
});
};
}, []);
const login = () => keycloak.login();
const logout = () => keycloak.logout();
const getToken = () => keycloak.token;
return (
<AuthContext.Provider value={{
authenticated,
user,
loading,
login,
logout,
getToken
}}>
{children}
</AuthContext.Provider>
);
}
export const useAuth = () => {
const context = useContext(AuthContext);
if (!context) {
throw new Error('useAuth must be used within AuthProvider');
}
return context;
};
// apps/web-app/src/utils/apiClient.js
import axios from 'axios';
import { useAuth } from '@hooks/useAuth';
const apiClient = axios.create({
baseURL: import.meta.env.VITE_API_URL || 'http://localhost:4000/api',
timeout: 10000,
headers: {
'Content-Type': 'application/json'
}
});
// Request interceptor to add auth token
apiClient.interceptors.request.use(
async (config) => {
const token = await getToken();
if (token) {
config.headers.Authorization = `Bearer ${token}`;
}
return config;
},
(error) => Promise.reject(error)
);
// Response interceptor for error handling
apiClient.interceptors.response.use(
(response) => response.data,
(error) => {
if (error.response?.status === 401) {
// Redirect to login
window.location.href = '/login';
}
const errorMessage = error.response?.data?.message || error.message;
return Promise.reject(new Error(errorMessage));
}
);
export default apiClient;
// Typed API methods
export const api = {
users: {
getProfile: (userId) => apiClient.get(`/users/${userId}`),
updateProfile: (userId, data) => apiClient.put(`/users/${userId}`, data),
listUsers: (orgId) => apiClient.get(`/users?org_id=${orgId}`)
},
organizations: {
get: (orgId) => apiClient.get(`/organizations/${orgId}`),
create: (data) => apiClient.post('/organizations', data),
update: (orgId, data) => apiClient.put(`/organizations/${orgId}`, data)
}
};
// services/user-service/src/index.js
import express from 'express';
import helmet from 'helmet';
import cors from 'cors';
import morgan from 'morgan';
import { connectDB } from './config/database.js';
import { errorHandler } from './middleware/errorHandler.js';
import { authMiddleware } from './middleware/auth.js';
import userRoutes from './routes/users.js';
const app = express();
// Security middleware
app.use(helmet());
app.use(cors({
origin: process.env.CORS_ORIGIN?.split(',') || 'http://localhost:3000',
credentials: true
}));
// Logging
app.use(morgan('combined'));
// Body parsing
app.use(express.json({ limit: '10mb' }));
app.use(express.urlencoded({ extended: true }));
// Connect to MongoDB
await connectDB();
// Health check
app.get('/health', (req, res) => {
res.json({ status: 'healthy', service: 'user-service' });
});
// API routes (protected)
app.use('/api/users', authMiddleware, userRoutes);
// Error handling
app.use(errorHandler);
const PORT = process.env.PORT || 5001;
app.listen(PORT, () => {
console.log(`User service running on port ${PORT}`);
});
// services/user-service/src/routes/users.js
import express from 'express';
import { body, param, query } from 'express-validator';
import { validate } from '../middleware/validate.js';
import { requireRole } from '../middleware/rbac.js';
import * as userController from '../controllers/user.controller.js';
const router = express.Router();
// GET /api/users - List users (org admin only)
router.get('/',
requireRole(['org_admin']),
query('org_id').optional().isString(),
query('page').optional().isInt({ min: 1 }),
query('limit').optional().isInt({ min: 1, max: 100 }),
validate,
userController.listUsers
);
// GET /api/users/:id - Get user by ID
router.get('/:id',
param('id').isUUID(),
validate,
userController.getUser
);
// POST /api/users - Create user (org admin only)
router.post('/',
requireRole(['org_admin']),
body('email').isEmail().normalizeEmail(),
body('firstName').trim().isLength({ min: 1, max: 50 }),
body('lastName').trim().isLength({ min: 1, max: 50 }),
body('orgId').isString(),
validate,
userController.createUser
);
// PUT /api/users/:id - Update user
router.put('/:id',
param('id').isUUID(),
body('firstName').optional().trim().isLength({ min: 1, max: 50 }),
body('lastName').optional().trim().isLength({ min: 1, max: 50 }),
validate,
userController.updateUser
);
// DELETE /api/users/:id - Delete user (org admin only)
router.delete('/:id',
requireRole(['org_admin']),
param('id').isUUID(),
validate,
userController.deleteUser
);
export default router;
// services/user-service/src/controllers/user.controller.js
import { UserModel } from '../models/User.js';
import { KeycloakService } from '../services/keycloak.service.js';
import { AppError } from '../utils/AppError.js';
export async function listUsers(req, res, next) {
try {
const { org_id, page = 1, limit = 20 } = req.query;
// Ensure user can only list users from their org (unless super admin)
const orgIdFilter = req.user.roles.includes('super_admin')
? org_id
: req.user.org_id;
if (!orgIdFilter) {
throw new AppError('Organization ID required', 400);
}
const users = await UserModel.find({ org_id: orgIdFilter })
.select('-password')
.limit(limit)
.skip((page - 1) * limit)
.sort({ createdAt: -1 });
const total = await UserModel.countDocuments({ org_id: orgIdFilter });
res.json({
users,
pagination: {
page: parseInt(page),
limit: parseInt(limit),
total,
pages: Math.ceil(total / limit)
}
});
} catch (error) {
next(error);
}
}
export async function getUser(req, res, next) {
try {
const { id } = req.params;
const user = await UserModel.findById(id).select('-password');
if (!user) {
throw new AppError('User not found', 404);
}
// Ensure user can only access users from their org
if (user.org_id !== req.user.org_id && !req.user.roles.includes('super_admin')) {
throw new AppError('Access denied', 403);
}
res.json(user);
} catch (error) {
next(error);
}
}
export async function createUser(req, res, next) {
try {
const { email, firstName, lastName, orgId } = req.body;
// Verify org_id matches user's org (unless super admin)
if (orgId !== req.user.org_id && !req.user.roles.includes('super_admin')) {
throw new AppError('Cannot create user for different organization', 403);
}
// Create user in Keycloak
const keycloakService = new KeycloakService();
const keycloakUserId = await keycloakService.createUser({
email,
firstName,
lastName,
orgId
});
// Create user in MongoDB
const user = new UserModel({
keycloakId: keycloakUserId,
email,
firstName,
lastName,
org_id: orgId,
createdBy: req.user.sub
});
await user.save();
res.status(201).json({
id: user._id,
keycloakId: keycloakUserId,
email: user.email
});
} catch (error) {
next(error);
}
}
// services/user-service/src/models/User.js
import mongoose from 'mongoose';
const userSchema = new mongoose.Schema({
keycloakId: {
type: String,
required: true,
unique: true,
index: true
},
email: {
type: String,
required: true,
lowercase: true,
trim: true,
index: true
},
firstName: {
type: String,
required: true,
trim: true
},
lastName: {
type: String,
required: true,
trim: true
},
org_id: {
type: String,
required: true,
index: true
},
roles: [{
type: String,
enum: ['org_admin', 'org_user', 'super_admin']
}],
metadata: {
type: Map,
of: String
},
createdBy: String,
updatedBy: String
}, {
timestamps: true,
toJSON: {
transform: (doc, ret) => {
ret.id = ret._id;
delete ret._id;
delete ret.__v;
return ret;
}
}
});
// Compound index for org queries
userSchema.index({ org_id: 1, email: 1 }, { unique: true });
// Virtual for full name
userSchema.virtual('fullName').get(function() {
return `${this.firstName} ${this.lastName}`;
});
// Pre-save hook
userSchema.pre('save', function(next) {
if (this.isModified('email')) {
this.email = this.email.toLowerCase();
}
next();
});
export const UserModel = mongoose.model('User', userSchema);
// services/org-service/src/models/Organization.js
import mongoose from 'mongoose';
const organizationSchema = new mongoose.Schema({
org_id: {
type: String,
required: true,
unique: true,
index: true
},
name: {
type: String,
required: true,
trim: true
},
domain: {
type: String,
required: true,
unique: true,
lowercase: true
},
settings: {
theme: {
type: String,
default: 'lobbi-base'
},
features: {
type: Map,
of: Boolean,
default: new Map()
},
branding: {
logoUrl: String,
primaryColor: String,
secondaryColor: String
}
},
subscription: {
plan: {
type: String,
enum: ['free', 'starter', 'professional', 'enterprise'],
default: 'free'
},
status: {
type: String,
enum: ['active', 'inactive', 'suspended'],
default: 'active'
},
billingCycle: {
type: String,
enum: ['monthly', 'annual']
},
stripeCustomerId: String,
stripeSubscriptionId: String
},
adminUsers: [{
userId: String,
email: String,
addedAt: Date
}],
status: {
type: String,
enum: ['active', 'inactive', 'suspended'],
default: 'active'
}
}, {
timestamps: true
});
export const OrganizationModel = mongoose.model('Organization', organizationSchema);
// services/api-gateway/src/index.js
import express from 'express';
import { createProxyMiddleware } from 'http-proxy-middleware';
import { authMiddleware } from './middleware/auth.js';
import { rateLimiter } from './middleware/rateLimit.js';
import { cacheMiddleware } from './middleware/cache.js';
const app = express();
// Rate limiting
app.use(rateLimiter);
// Authentication
app.use(authMiddleware);
// Service routing
const services = {
users: process.env.USER_SERVICE_URL || 'http://localhost:5001',
orgs: process.env.ORG_SERVICE_URL || 'http://localhost:5002',
tenants: process.env.TENANT_SERVICE_URL || 'http://localhost:5003',
notifications: process.env.NOTIFICATION_SERVICE_URL || 'http://localhost:5004',
billing: process.env.BILLING_SERVICE_URL || 'http://localhost:5005',
analytics: process.env.ANALYTICS_SERVICE_URL || 'http://localhost:5006'
};
// Proxy to microservices
Object.entries(services).forEach(([name, target]) => {
app.use(`/api/${name}`, createProxyMiddleware({
target,
changeOrigin: true,
pathRewrite: { [`^/api/${name}`]: '' },
onProxyReq: (proxyReq, req) => {
// Forward user context
if (req.user) {
proxyReq.setHeader('X-User-Id', req.user.sub);
proxyReq.setHeader('X-Org-Id', req.user.org_id);
proxyReq.setHeader('X-User-Roles', JSON.stringify(req.user.roles));
}
}
}));
});
app.listen(4000, () => {
console.log('API Gateway running on port 4000');
});
// services/api-gateway/src/config/session.js
import session from 'express-session';
import MongoStore from 'connect-mongo';
export const sessionConfig = session({
secret: process.env.SESSION_SECRET,
resave: false,
saveUninitialized: false,
store: MongoStore.create({
mongoUrl: process.env.MONGODB_URL,
ttl: 24 * 60 * 60, // 1 day
touchAfter: 24 * 3600 // lazy session update
}),
cookie: {
secure: process.env.NODE_ENV === 'production',
httpOnly: true,
maxAge: 1000 * 60 * 60 * 24, // 24 hours
sameSite: 'lax',
domain: process.env.COOKIE_DOMAIN
},
name: 'lobbi.sid'
});
// shared/utils/AppError.js
export class AppError extends Error {
constructor(message, statusCode = 500, isOperational = true) {
super(message);
this.statusCode = statusCode;
this.isOperational = isOperational;
Error.captureStackTrace(this, this.constructor);
}
}
export class ValidationError extends AppError {
constructor(message, errors = []) {
super(message, 400);
this.errors = errors;
}
}
export class NotFoundError extends AppError {
constructor(resource) {
super(`${resource} not found`, 404);
}
}
export class UnauthorizedError extends AppError {
constructor(message = 'Unauthorized') {
super(message, 401);
}
}
export class ForbiddenError extends AppError {
constructor(message = 'Forbidden') {
super(message, 403);
}
}
// services/user-service/src/middleware/errorHandler.js
import { AppError } from '../utils/AppError.js';
export function errorHandler(err, req, res, next) {
let { statusCode, message, isOperational } = err;
// Default to 500 server error
if (!statusCode) {
statusCode = 500;
isOperational = false;
}
// Log error
console.error('Error:', {
message,
statusCode,
isOperational,
stack: err.stack,
url: req.url,
method: req.method,
user: req.user?.sub
});
// Send error response
res.status(statusCode).json({
error: {
message: isOperational ? message : 'Internal server error',
statusCode,
...(process.env.NODE_ENV === 'development' && { stack: err.stack })
}
});
}
// Async error wrapper
export function asyncHandler(fn) {
return (req, res, next) => {
Promise.resolve(fn(req, res, next)).catch(next);
};
}
// services/user-service/tests/controllers/user.controller.test.js
import { listUsers, createUser } from '../../src/controllers/user.controller.js';
import { UserModel } from '../../src/models/User.js';
import { KeycloakService } from '../../src/services/keycloak.service.js';
jest.mock('../../src/models/User.js');
jest.mock('../../src/services/keycloak.service.js');
describe('UserController', () => {
describe('listUsers', () => {
it('should return paginated users for org', async () => {
const mockUsers = [
{ _id: '1', email: 'user1@test.com', org_id: 'org_1' },
{ _id: '2', email: 'user2@test.com', org_id: 'org_1' }
];
UserModel.find.mockReturnValue({
select: jest.fn().mockReturnThis(),
limit: jest.fn().mockReturnThis(),
skip: jest.fn().mockReturnThis(),
sort: jest.fn().mockResolvedValue(mockUsers)
});
UserModel.countDocuments.mockResolvedValue(2);
const req = {
query: { org_id: 'org_1', page: 1, limit: 20 },
user: { org_id: 'org_1', roles: ['org_admin'] }
};
const res = {
json: jest.fn()
};
const next = jest.fn();
await listUsers(req, res, next);
expect(res.json).toHaveBeenCalledWith({
users: mockUsers,
pagination: expect.objectContaining({
page: 1,
total: 2
})
});
});
});
});
// services/user-service/tests/integration/users.test.js
import request from 'supertest';
import { app } from '../../src/index.js';
import { connectDB, closeDB, clearDB } from '../setup.js';
beforeAll(async () => await connectDB());
afterEach(async () => await clearDB());
afterAll(async () => await closeDB());
describe('User API Integration', () => {
it('POST /api/users - should create user', async () => {
const userData = {
email: 'test@example.com',
firstName: 'Test',
lastName: 'User',
orgId: 'org_test'
};
const response = await request(app)
.post('/api/users')
.set('Authorization', `Bearer ${mockAdminToken}`)
.send(userData)
.expect(201);
expect(response.body).toMatchObject({
email: userData.email
});
});
});
| Path | Purpose |
|---|---|
apps/web-app/ | React + Vite main application |
services/api-gateway/ | API Gateway with routing |
services/user-service/ | User management microservice |
services/org-service/ | Organization management |
routes/api/ | Shared route definitions |
shared/utils/ | Shared utilities and helpers |
This skill should be used when the user asks to "create an agent", "add an agent", "write a subagent", "agent frontmatter", "when to use description", "agent examples", "agent tools", "agent colors", "autonomous agent", or needs guidance on agent structure, system prompts, triggering conditions, or agent development best practices for Claude Code plugins.
This skill should be used when the user asks to "create a slash command", "add a command", "write a custom command", "define command arguments", "use command frontmatter", "organize commands", "create command with file references", "interactive command", "use AskUserQuestion in command", or needs guidance on slash command structure, YAML frontmatter fields, dynamic arguments, bash execution in commands, user interaction patterns, or command development best practices for Claude Code.
This skill should be used when the user asks to "create a hook", "add a PreToolUse/PostToolUse/Stop hook", "validate tool use", "implement prompt-based hooks", "use ${CLAUDE_PLUGIN_ROOT}", "set up event-driven automation", "block dangerous commands", or mentions hook events (PreToolUse, PostToolUse, Stop, SubagentStop, SessionStart, SessionEnd, UserPromptSubmit, PreCompact, Notification). Provides comprehensive guidance for creating and implementing Claude Code plugin hooks with focus on advanced prompt-based hooks API.