This document provides comprehensive information about the erxes codebase structure, development workflows, and key conventions for AI assistants working on this project.
- Project Overview
- Architecture & Technology Stack
- Repository Structure
- Development Workflows
- Plugin System
- Code Conventions
- Testing
- CI/CD
- Common Tasks
- Important Patterns
erxes (pronounced 'erk-sis') is a secure, self-hosted, and scalable source-available Experience Operating System (XOS) that enables businesses to manage marketing, sales, operations, and support in one unified platform.
- Architecture: Nx-powered pnpm monorepo with microservices architecture
- License: AGPLv3 (core) with Enterprise Edition plugins
- Package Manager: pnpm (v9.12.3) - REQUIRED
- Build System: Nx (v20.0.8) with intelligent caching and task orchestration
- Version: TypeScript 5.7.3, Node.js 18+
- 100% customizable through plugin architecture
- Self-hosted for data privacy
- Microservices with GraphQL Federation
- Micro-frontends with Module Federation
┌─────────────────────────────────────────┐
│ API Gateway (Port 4000) │
│ Apollo Router + Service Discovery │
└─────────────────────────────────────────┘
│
┌─────────────┼─────────────┐
▼ ▼ ▼
┌──────────┐ ┌──────────┐ ┌──────────┐
│ Core API │ │ Plugin │ │ Plugin │
│ (3300) │ │ APIs │ │ APIs │
└──────────┘ └──────────┘ └──────────┘
│ │ │
└─────────────┴─────────────┘
│
┌─────────────┼─────────────┐
▼ ▼ ▼
┌──────────┐ ┌──────────┐ ┌──────────┐
│ MongoDB │ │ Redis │ │Elasticsea│
│ │ │ +BullMQ │ │ rch │
└──────────┘ └──────────┘ └──────────┘
Technologies:
- Runtime: Node.js with TypeScript 5.7.3
- Framework: Express.js
- GraphQL: Apollo Server v4, Apollo Federation (@apollo/subgraph)
- API: tRPC v11 for type-safe endpoints
- Database: MongoDB with Mongoose (v8.13.2)
- Cache/Queue: Redis (ioredis) + BullMQ v5.40.0
- Search: Elasticsearch 7
- Real-time: GraphQL Subscriptions (graphql-redis-subscriptions)
- Authentication: JWT (jsonwebtoken), WorkOS for SSO
┌─────────────────────────────────────────┐
│ Core UI (Host - Port 3001) │
│ Module Federation Host Application │
└─────────────────────────────────────────┘
│
┌─────────────┼─────────────┐
▼ ▼ ▼
┌──────────┐ ┌──────────┐ ┌──────────┐
│ Plugin │ │ Plugin │ │ Plugin │
│ UI (3005)│ │ UI (3006)│ │ UI (3007)│
└──────────┘ └──────────┘ └──────────┘
Technologies:
- Framework: React 18.3.1
- Bundler: Rspack v1.0.5 (Rust-based, faster than Webpack)
- Module Federation: @module-federation/enhanced v0.6.6
- Styling: TailwindCSS v4.1.17 + PostCSS
- UI Components: Radix UI primitives + custom design system (erxes-ui)
- State Management: Jotai (atomic state) + Apollo Client
- Routing: React Router v7
- Forms: React Hook Form + Zod validation
- i18n: react-i18next
- Rich Text: Blocknote editor
- Icons: @tabler/icons-react
- Data Visualization: Recharts
Standalone Applications:
- client-portal-template: Next.js 16 customer portal
- posclient-front: Next.js 14 POS with PWA support
- frontline-widgets: Customer-facing widgets (chat, forms)
erxes/
├── backend/ # Backend microservices
│ ├── gateway/ # API Gateway (Port 4000)
│ │ └── src/main.ts # Gateway entry point
│ ├── core-api/ # Core business logic (Port 3300)
│ │ ├── src/
│ │ │ ├── main.ts # Core API entry point
│ │ │ ├── apollo/ # GraphQL schema & resolvers
│ │ │ ├── trpc/ # tRPC router
│ │ │ ├── modules/ # Business logic modules
│ │ │ │ ├── contacts/
│ │ │ │ ├── products/
│ │ │ │ ├── segments/
│ │ │ │ ├── automations/
│ │ │ │ └── documents/
│ │ │ ├── meta/ # Automation, segment configs
│ │ │ └── routes.ts # Express routes
│ │ ├── Dockerfile
│ │ ├── project.json # Nx configuration
│ │ └── tsconfig.json
│ ├── erxes-api-shared/ # Shared library for all services
│ │ └── src/
│ │ ├── utils/ # Service discovery, Redis, MQ
│ │ ├── core-types/ # TypeScript type definitions
│ │ └── core-modules/ # Reusable business logic
│ ├── plugins/ # Plugin microservices
│ │ ├── sales_api/ # Sales plugin (Port 3305)
│ │ ├── operation_api/ # Operations plugin
│ │ ├── frontline_api/ # Customer service plugin
│ │ ├── accounting_api/ # Accounting plugin (EE)
│ │ ├── content_api/ # Content management (EE)
│ │ └── ...
│ └── services/ # Background services
│ ├── automations/ # Automation execution engine
│ └── logs/ # Logging service
├── frontend/ # Frontend applications
│ ├── core-ui/ # Module federation host (Port 3001)
│ │ ├── src/
│ │ │ ├── main.ts # Entry point
│ │ │ └── bootstrap.tsx # App bootstrap
│ │ └── module-federation.config.ts
│ ├── libs/ # Shared UI libraries
│ │ ├── erxes-ui/ # Core UI components & state
│ │ └── ui-modules/ # Reusable UI modules
│ └── plugins/ # Frontend plugin remotes
│ ├── sales_ui/ # Sales UI plugin (Port 3005)
│ │ ├── src/
│ │ │ ├── config.tsx # Plugin configuration
│ │ │ ├── modules/ # Module components
│ │ │ ├── pages/ # Page components
│ │ │ └── widgets/ # Widget components
│ │ ├── module-federation.config.ts
│ │ └── rspack.config.ts
│ └── ...
├── apps/ # Standalone applications
│ ├── client-portal-template/ # Next.js 16 customer portal
│ ├── posclient-front/ # Next.js 14 POS client
│ └── frontline-widgets/ # Customer-facing widgets
├── scripts/ # Development scripts
│ ├── create-plugin.js # Plugin generator
│ ├── start-api-dev.js # Start all API services
│ └── start-ui-dev.js # Start all UI plugins
├── .github/workflows/ # CI/CD pipelines (26+ workflows)
├── nx.json # Nx configuration
├── pnpm-workspace.yaml # pnpm workspace config
├── package.json # Root package.json
├── tsconfig.base.json # Base TypeScript config
└── CLAUDE.md # This file
All backend services use consistent path aliases:
"paths": {
"~/*": ["./src/*"], // Service root
"@/*": ["./src/modules/*"], // Modules directory
"erxes-api-shared/*": ["../erxes-api-shared/src/*"] // Shared lib
}- pnpm ≥ 8 (enforced in package.json)
- Node.js 18.16.9+ (see
.nvmrcif exists) - MongoDB 27017
- Redis (default port)
- Elasticsearch 7 (optional, for search)
# Clone repository
git clone https://github.com/erxes/erxes.git
cd erxes
# Install dependencies (MUST use pnpm)
pnpm install
# Setup environment variables
cp .env.example .env
# Edit .env with your configurationOption 1: Run Core Only
# Runs Gateway + Core API
pnpm dev:core-apiOption 2: Run All APIs
# Starts all backend services defined in ENABLED_PLUGINS
pnpm dev:apisOption 3: Run All UIs
# Starts all frontend plugins
pnpm dev:uisOption 4: Run Specific Service (Nx)
# Backend service
pnpm nx serve core-api
pnpm nx serve sales_api
# Frontend plugin
pnpm nx serve sales_ui
# Build specific project
pnpm nx build sales_api
# Run tests
pnpm nx test sales_api
# Run affected commands (only changed projects)
pnpm nx affected:build
pnpm nx affected:test# Required
MONGO_URL=mongodb://localhost:27017/erxes
REDIS_HOST=localhost
REDIS_PORT=6379
# Plugin Management
ENABLED_PLUGINS=operation,sales,frontline,accounting
# API Configuration
DOMAIN=http://localhost:3000
REACT_APP_API_URL=http://localhost:4000
# Feature Flags
DISABLE_CHANGE_STREAM=true # Disable MongoDB change streams in dev
# SAAS Mode (optional)
SAAS_MODE=trueGateway: 4000
Core API: 3300
Core UI: 3001
Plugin APIs: 3305+ (sales=3305, operation=3306, etc.)
Plugin UIs: 3005+ (sales=3005, operation=3006, etc.)
BullMQ Board: 4000/bullmq-board
erxes uses a plugin-based architecture for both backend and frontend:
- Backend Plugins: Microservices registered with the gateway via Redis
- Frontend Plugins: Module Federation remotes dynamically loaded at runtime
Standard Plugin Entry Point (src/main.ts):
import { startPlugin } from 'erxes-api-shared/utils';
import { appRouter } from './trpc/init-trpc';
import resolvers from './apollo/resolvers';
import { typeDefs } from './apollo/typeDefs';
import { generateModels } from './connectionResolvers';
import { router } from './routes';
import automations from './meta/automations';
import segments from './meta/segments';
startPlugin({
name: 'sales',
port: 3305,
graphql: async () => ({
typeDefs: await typeDefs(),
resolvers,
}),
expressRouter: router,
hasSubscriptions: true,
subscriptionPluginPath: require('path').resolve(
__dirname,
'apollo',
process.env.NODE_ENV === 'production'
? 'subscription.js'
: 'subscription.ts',
),
apolloServerContext: async (subdomain, context) => {
const models = await generateModels(subdomain, context);
context.models = models;
return context;
},
trpcAppRouter: {
router: appRouter,
createContext: async (subdomain, context) => {
const models = await generateModels(subdomain);
context.models = models;
return context;
},
},
onServerInit: async () => {
// Initialize workers, cron jobs, etc.
},
meta: {
automations,
segments,
notificationModules: [/* ... */],
},
});Key Files in Backend Plugin:
main.ts- Entry point usingstartPlugin()connectionResolvers.ts- Database modelsapollo/- GraphQL schema, resolvers, subscriptionstrpc/- tRPC router and proceduresmodules/- Business logic organized by featuremeta/- Automations, segments, exports configurationroutes.ts- Express routesDockerfile- Container configurationproject.json- Nx build configuration
Plugin Configuration (src/config.tsx):
import { IconBriefcase } from '@tabler/icons-react';
import { IUIConfig } from 'erxes-ui';
import { lazy, Suspense } from 'react';
const MainNavigation = lazy(() =>
import('./modules/MainNavigation').then((module) => ({
default: module.MainNavigation,
})),
);
export const CONFIG: IUIConfig = {
name: 'sales',
icon: IconBriefcase,
navigationGroup: {
name: 'sales',
icon: IconBriefcase,
content: () => (
<Suspense fallback={<div />}>
<MainNavigation />
</Suspense>
),
},
modules: [
{
name: 'sales',
icon: IconBriefcase,
path: 'sales',
hasSettings: false,
hasRelationWidget: true,
hasFloatingWidget: false,
},
],
widgets: {
relationWidgets: [
{
name: 'deals',
icon: IconBriefcase,
},
],
},
};Module Federation Configuration (module-federation.config.ts):
import { ModuleFederationConfig } from '@nx/rspack/module-federation';
const coreLibraries = new Set([
'react',
'react-dom',
'react-router',
'react-router-dom',
'erxes-ui',
'@apollo/client',
'jotai',
'ui-modules',
'react-i18next',
]);
const config: ModuleFederationConfig = {
name: 'sales_ui',
exposes: {
'./config': './src/config.tsx',
'./sales': './src/modules/Main.tsx',
'./dealsSettings': './src/pages/SettingsPage.tsx',
'./Widgets': './src/widgets/Widgets.tsx',
'./relationWidget': './src/widgets/relation/RelationWidgets.tsx',
},
shared: (libraryName, defaultConfig) => {
if (coreLibraries.has(libraryName)) {
return defaultConfig;
}
return false;
},
};
export default config;Using the Plugin Generator:
pnpm create-pluginThis will prompt for:
- Plugin name: e.g., "inventory"
- Module name: e.g., "products"
The script creates:
- Backend:
backend/plugins/inventory_api/ - Frontend:
frontend/plugins/inventory_ui/
Both with complete boilerplate including:
- GraphQL/tRPC setup
- Module Federation configuration
- Example components and routes
- Nx project configuration
Plugin Activation:
Add to .env:
ENABLED_PLUGINS=operation,sales,frontline,inventoryPlugins register with the gateway using Redis:
// From erxes-api-shared/utils
await joinErxesGateway({
name: 'sales',
address: 'http://localhost:3305',
config: {
typeDefs,
hasSubscriptions: true,
meta: { automations, segments },
},
});Gateway dynamically routes requests to plugins:
- GraphQL: Federated via Apollo Router
- REST: Proxy via
/pl:{serviceName}/* - tRPC: Proxy via
/trpc/
Configuration:
- Strict null checks: enabled
- No implicit any: disabled (legacy code compatibility)
- Target: ES2017
- Module: CommonJS (backend), ESNext (frontend)
Naming Conventions:
// Interfaces & Types
interface IUser { ... }
type UserRole = 'admin' | 'user';
// Classes (PascalCase)
class UserService { ... }
// Functions & Variables (camelCase)
const getUserById = (id: string) => { ... };
// Constants (UPPER_SNAKE_CASE for globals)
const MAX_RETRY_COUNT = 3;
// Files
// - Components: PascalCase (UserProfile.tsx)
// - Utils/Services: camelCase (authService.ts)
// - Config: kebab-case (module-federation.config.ts){
"singleQuote": true,
"trailingComma": "all",
"endOfLine": "auto"
}Key Rules:
- Single quotes for strings
- Trailing commas in arrays/objects
- 2-space indentation (inferred)
- No semicolons (inferred)
Component Structure:
// Prefer functional components with hooks
export const UserList: React.FC<Props> = ({ users, onSelect }) => {
// State
const [selectedId, setSelectedId] = useState<string | null>(null);
// Queries (Apollo)
const { data, loading, error } = useQuery(GET_USERS);
// Mutations
const [updateUser] = useMutation(UPDATE_USER);
// Effects
useEffect(() => {
// Side effects
}, [dependency]);
// Handlers
const handleSelect = (id: string) => {
setSelectedId(id);
onSelect(id);
};
// Render
if (loading) return <Loading />;
if (error) return <Error message={error.message} />;
return (
<div>
{users.map(user => (
<UserCard key={user.id} user={user} onClick={handleSelect} />
))}
</div>
);
};State Management:
- Local State:
useStatefor component-local state - Global State: Jotai atoms for app-wide state
- Server State: Apollo Client for GraphQL data
- Form State: React Hook Form with Zod validation
Lazy Loading (Module Federation):
// Always lazy load federation modules
const RemoteModule = lazy(() => import('remote/Module'));
// Always wrap in Suspense
<Suspense fallback={<Loading />}>
<RemoteModule />
</Suspense>Schema Naming:
# Types: PascalCase
type User {
_id: String!
email: String
details: UserDetails
}
# Queries: camelCase with descriptive names
type Query {
users(page: Int, perPage: Int): [User]
userDetail(_id: String!): User
usersTotalCount: Int
}
# Mutations: camelCase verb + noun
type Mutation {
usersAdd(email: String!, details: UserDetailsInput): User
usersEdit(_id: String!, doc: UserDetailsInput): User
usersRemove(_id: String!): JSON
}
# Subscriptions: noun + past tense verb
type Subscription {
userChanged(_id: String!): User
}Resolver Structure:
const resolvers = {
Query: {
users: async (_, { page, perPage }, { models, subdomain }) => {
return models.Users.find({})
.skip((page - 1) * perPage)
.limit(perPage);
},
},
Mutation: {
usersAdd: async (_, doc, { models, subdomain, user }) => {
// Permission check
if (!user) throw new Error('Unauthorized');
// Business logic
return models.Users.createUser(doc);
},
},
User: {
// Field resolver for computed fields
fullName: (user) => `${user.firstName} ${user.lastName}`,
},
};Service Layer Pattern:
// modules/users/services.ts
export const userService = {
async createUser(models, doc) {
// Validation
if (!doc.email) throw new Error('Email required');
// Business logic
const user = await models.Users.create(doc);
// Side effects
await sendWelcomeEmail(user.email);
return user;
},
};Model Layer (Mongoose):
// connectionResolvers.ts
export const generateModels = (subdomain: string) => {
const Users = loadUsersClass(subdomain);
return {
Users,
};
};
// models/definitions/users.ts
export const userSchema = new Schema({
email: { type: String, unique: true, required: true },
details: {
firstName: String,
lastName: String,
},
createdAt: { type: Date, default: Date.now },
});
// models/Users.ts
export class UserModel {
static async createUser(doc) {
// Business logic
return this.create(doc);
}
}Error Handling:
// Always throw descriptive errors
throw new Error('User with this email already exists');
// Use custom error classes for API responses
export class ValidationError extends Error {
constructor(message: string) {
super(message);
this.name = 'ValidationError';
}
}Every request includes a subdomain for tenant isolation:
// Context includes subdomain
const resolver = async (_, args, { subdomain, models, user }) => {
// Models are scoped to subdomain automatically
const users = await models.Users.find({ /* tenant-specific */ });
};
// MongoDB collections are prefixed with subdomain
// Example: subdomain_users, subdomain_productssrc/
├── modules/
│ └── users/
│ ├── __tests__/
│ │ ├── users.test.ts # Unit tests
│ │ └── queries.test.ts # GraphQL query tests
│ ├── services.ts
│ └── models.ts
# Run all tests
pnpm nx test <project-name>
# Run tests in watch mode
pnpm nx test <project-name> --watch
# Run tests with coverage
pnpm nx test <project-name> --coverage
# Run affected tests (only changed projects)
pnpm nx affected:test// jest.config.ts
export default {
displayName: 'sales-api',
preset: '../../jest.preset.js',
testEnvironment: 'node',
transform: {
'^.+\\.[tj]s$': ['ts-jest', { tsconfig: '<rootDir>/tsconfig.spec.json' }],
},
moduleFileExtensions: ['ts', 'js', 'html'],
coverageDirectory: '../../coverage/backend/plugins/sales_api',
};Backend Service Test:
import { generateModels } from '../connectionResolvers';
describe('User Service', () => {
let models;
beforeEach(async () => {
models = await generateModels('test');
});
afterEach(async () => {
await models.Users.deleteMany({});
});
it('should create a user', async () => {
const user = await models.Users.createUser({
email: '[email protected]',
});
expect(user.email).toBe('[email protected]');
expect(user._id).toBeDefined();
});
});Frontend Component Test:
import { render, screen } from '@testing-library/react';
import { MockedProvider } from '@apollo/client/testing';
import { UserList } from './UserList';
describe('UserList', () => {
it('renders user list', async () => {
const mocks = [
{
request: {
query: GET_USERS,
},
result: {
data: {
users: [{ _id: '1', email: '[email protected]' }],
},
},
},
];
render(
<MockedProvider mocks={mocks}>
<UserList />
</MockedProvider>
);
expect(await screen.findByText('[email protected]')).toBeInTheDocument();
});
});Located in .github/workflows/ with 26+ workflow files:
Naming Convention:
ci-api-core.yml- Core API CIci-plugin-sales.yml- Sales plugin CIci-ui-sales.yml- Sales UI CI
Workflow Pattern:
name: CI plugin--sales-api
on:
push:
branches: [main, develop]
paths:
- 'backend/plugins/sales_api/**'
- 'backend/erxes-api-shared/**'
- '.github/workflows/ci-plugin-sales.yml'
pull_request:
paths:
- 'backend/plugins/sales_api/**'
- 'backend/erxes-api-shared/**'
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: pnpm/action-setup@v2
with:
version: 8
- uses: actions/setup-node@v4
- run: pnpm install
- run: pnpm nx build erxes-api-shared # Build shared lib first
- run: pnpm nx build sales_api
# Docker multi-platform build
- uses: docker/setup-buildx-action@v3
- uses: docker/build-push-action@v5
with:
platforms: linux/amd64,linux/arm64
tags: |
erxes/erxes-next-sales-api:latest
erxes/erxes-next-sales-api:${{ env.DATE }}-${{ env.SHORT_SHA }}Key Features:
- Path-based triggers: Only builds affected services
- Nx caching: Leverages Nx build cache
- Multi-platform: Builds for AMD64 and ARM64
- Tagging:
latest+YYYYMMDD-{sha} - Shared lib: Always builds
erxes-api-sharedfirst for backend
Each service has its own Dockerfile:
# Example: backend/plugins/sales_api/Dockerfile
FROM node:18-alpine
WORKDIR /app
# Copy shared dependencies
COPY backend/erxes-api-shared/dist ./erxes-api-shared/dist
COPY backend/plugins/sales_api/dist ./sales_api/dist
COPY backend/plugins/sales_api/package.json ./sales_api/
RUN cd sales_api && npm install --production
WORKDIR /app/sales_api
CMD ["node", "dist/main.js"]Docker Images:
- Registry: Docker Hub
- Org:
erxes - Naming:
erxes-next-{service-name} - Example:
erxes/erxes-next-sales-api:latest
Services are typically deployed as:
- Docker Compose (development/self-hosted)
- Kubernetes (production/scaled)
- Cloud Platforms (AWS, GCP, Azure)
- Identify the service (core-api or plugin)
- Define GraphQL schema in
apollo/typeDefs/ - Create resolvers in
apollo/resolvers/ - Add service logic in
modules/{feature}/ - Create models if needed in
models/ - Add tRPC endpoints (optional) in
trpc/ - Write tests in
__tests__/ - Update meta if automation/segment related
- Identify the plugin (e.g., sales_ui)
- Create component in
modules/{feature}/ - Define routes if needed
- Add GraphQL queries using Apollo Client
- Update config.tsx to expose in navigation
- Update module-federation.config.ts to expose module
- Add translations in locales
- Write tests in
__tests__/
Backend Shared (erxes-api-shared):
- Make changes in
backend/erxes-api-shared/src/ - Build:
pnpm nx build erxes-api-shared - Rebuild dependent services (they reference dist/)
Frontend Shared (erxes-ui):
- Make changes in
frontend/libs/erxes-ui/src/ - No build needed (imported directly)
- Hot reload works across plugins
Mongoose migrations pattern:
// scripts/migration-{feature}.ts
import { connect } from '../db/connection';
const migrate = async () => {
const db = await connect();
// Migration logic
await db.collection('users').updateMany(
{ role: { $exists: false } },
{ $set: { role: 'user' } }
);
console.log('Migration complete');
process.exit(0);
};
migrate();Run via: tsx scripts/migration-{feature}.ts
Backend:
# Enable debug logs
DEBUG=* pnpm nx serve sales_api
# Node inspector
node --inspect dist/main.jsFrontend:
# React DevTools
# Apollo DevTools (browser extension)
# Redux DevTools for Jotai (jotai-devtools)
# Rspack dev server provides source maps
pnpm nx serve sales_uiCommon Issues:
- Port conflicts: Check if services are already running
- Module Federation errors: Clear cache, restart dev servers
- GraphQL errors: Check gateway logs, verify service registration
- Shared lib not found: Rebuild
erxes-api-shared
// Always use subdomain for data access
const { subdomain, models } = context;
// Models are automatically scoped
const users = await models.Users.find({}); // Only tenant's users
// Manual subdomain in collection names
const collectionName = `${subdomain}_users`;Via GraphQL Federation:
// Reference other service types
type Deal @key(fields: "_id") {
_id: ID!
customer: Contact @provides(fields: "email") # From contacts service
}Via tRPC:
// backend/plugins/sales_api/src/trpc/routers/deals.ts
export const dealsRouter = t.router({
list: t.procedure
.input(z.object({ customerId: z.string() }))
.query(async ({ input, ctx }) => {
return ctx.models.Deals.find({ customerId: input.customerId });
}),
});
// From another service
import { trpc } from '@/lib/trpc';
const deals = await trpc.deals.list.query({ customerId: '123' });Caching:
import { redis } from 'erxes-api-shared/utils';
// Set with expiration
await redis.set(`user:${id}`, JSON.stringify(user), 'EX', 3600);
// Get
const cached = await redis.get(`user:${id}`);
const user = cached ? JSON.parse(cached) : null;
// Delete
await redis.del(`user:${id}`);PubSub (Real-time):
import { RedisPubSub } from 'graphql-redis-subscriptions';
const pubsub = new RedisPubSub({ /* redis config */ });
// Publish
await pubsub.publish('USER_CHANGED', { userChanged: user });
// Subscribe (GraphQL)
const subscriptions = {
userChanged: {
subscribe: () => pubsub.asyncIterator(['USER_CHANGED']),
},
};import { Queue, Worker } from 'bullmq';
// Create queue
const emailQueue = new Queue('emails', {
connection: { host: 'localhost', port: 6379 },
});
// Add job
await emailQueue.add('send', {
to: '[email protected]',
subject: 'Welcome',
});
// Process jobs
const worker = new Worker('emails', async (job) => {
const { to, subject } = job.data;
await sendEmail(to, subject);
}, {
connection: { host: 'localhost', port: 6379 },
});
// View dashboard at http://localhost:4000/bullmq-boardPlugins can register automation actions/triggers:
// meta/automations.ts
export default {
constants: {
actions: [
{
type: 'sales:createDeal',
icon: 'file-plus',
label: 'Create deal',
description: 'Create a new deal',
},
],
triggers: [
{
type: 'sales:dealCreated',
icon: 'file-check',
label: 'Deal created',
description: 'Triggered when deal is created',
},
],
},
actions: async ({ subdomain, data }) => {
const { action, execution } = data;
if (action.type === 'sales:createDeal') {
// Execute action
const models = await generateModels(subdomain);
return models.Deals.createDeal(execution.target);
}
},
triggers: async ({ subdomain, data }) => {
// Emit trigger events
await emitTrigger('sales:dealCreated', deal);
},
};Dynamic user/customer segmentation:
// meta/segments.ts
export default {
contentTypes: [
{
type: 'sales:deal',
description: 'Deals',
fields: [
{
key: 'name',
label: 'Name',
type: 'string',
},
{
key: 'amount',
label: 'Amount',
type: 'number',
},
],
},
],
esTypes: ['deal'],
associationTypes: [
{
name: 'deal',
label: 'Deal',
},
],
};// meta/import-export.ts
export default {
importTypes: [
{
text: 'Deals',
contentType: 'deal',
icon: 'file-plus',
},
],
exporter: async ({ subdomain, data }) => {
const models = await generateModels(subdomain);
const deals = await models.Deals.find(data.filter);
return {
data: deals.map(deal => ({
Name: deal.name,
Amount: deal.amount,
})),
};
},
};- Main Docs: https://erxes.io/docs
- Local Setup: https://erxes.io/docs/local-setup
- Contributing: See CONTRIBUTING.md
- Roadmap: https://erxes.io/roadmap
- Changelog: https://erxes.io/changelog
- Discord: https://discord.com/invite/aaGzy3gQK5
- GitHub Issues: https://github.com/erxes/erxes/issues
- Transifex (i18n): https://explore.transifex.com/erxes-inc/erxesxos/
Finding Features:
# Find GraphQL type definition
pnpm nx run-many -t grep -p 'type Deal'
# Find component usage
pnpm nx run-many -t grep -p 'UserList'
# Find API endpoint
pnpm nx run-many -t grep -p '/api/deals'Understanding Plugin Flow:
- Start at
main.ts- entry point - Check
apollo/typeDefs.ts- GraphQL schema - Look at
apollo/resolvers/- query/mutation logic - Explore
modules/- business logic - Review
models/- data layer
Understanding Frontend Plugin:
- Start at
config.tsx- plugin configuration - Check
module-federation.config.ts- exposed modules - Look at
modules/- main components - Check
pages/- route components - Review
widgets/- reusable widgets
- Always read existing code before making changes
- Understand the plugin architecture before modifications
- Check both GraphQL and tRPC endpoints when working with APIs
- Review module-federation.config.ts for exposed modules
- Backend: Rebuild
erxes-api-sharedif shared code changed - Frontend: Check if changes affect module federation exports
- Always maintain TypeScript types
- Follow existing patterns in the same service/plugin
- Test multi-tenancy (subdomain) implications
- Don't bypass plugin system - use proper extension points
- Don't break module federation shared dependencies
- Don't modify core without considering plugin impacts
- Always consider subdomain context for data access
- Remember port allocation when adding new services
- Run Nx affected commands to see what's impacted
- Test in development mode first
- Verify GraphQL schema still federates correctly
- Check module federation loads properly
- Test with different subdomains if multi-tenant
- Branch naming:
feat/,fix/,docs/ - Reference issues in commits
- Keep commits focused and atomic
- Run affected tests before pushing
- See CONTRIBUTING.md for full guidelines
Last Updated: 2026-01-15 Version: 1.0.0 Maintainer: erxes Team
For questions or clarifications, please open an issue or join our Discord community.