Backend Plugins
Build a tenant-aware API service that registers with the gateway through Redis service discovery.
Start with Create a Plugin. For schema and service-call conventions see GraphQL & tRPC; for the meta extension points see Plugin Metadata & Extensions.
startPlugin options
Every backend plugin's src/main.ts calls startPlugin(configs) from erxes-api-shared/utils. The options type (ConfigTypes) accepts:
| Option | Type | Purpose |
|---|---|---|
name |
string |
Plugin name used for service discovery, Sentry tags, and queue prefixes. Must match the ENABLED_PLUGINS entry. |
port |
number |
Listen port; process.env.PORT overrides it at runtime. |
graphql |
() => Promise<{ typeDefs, resolvers }> |
Builds the federated subgraph served at /graphql. |
apolloServerContext |
(subdomain, context, req, res) => Promise<IMainContext> |
Extends the per-request context; attach models here. |
trpcAppRouter |
{ router, createContext } |
Mounts /trpc for service-to-service calls. |
expressRouter |
express.Router |
Extra routes mounted before middlewares (webhooks, file routes). |
middlewares |
any[] |
Additional Express middleware. |
apiHandlers |
{ method, path, resolver }[] |
Simple REST handlers wrapped in the shared logHandler. |
corsOptions |
cors options |
Passed to cors(). |
hasSubscriptions |
boolean |
Requires subscriptionPluginPath; serves the subscription bundle. |
subscriptionPluginPath |
string |
File served at GET /subscriptionPlugin.js (rate-limited). |
onServerInit |
(app) => Promise<void> |
Runs after registration; plugins start MQ workers here. |
meta |
IMeta |
Extension-point config; see below. |
A minimal main.ts (sales uses every option):
startPlugin({
name: 'inventory',
port: 3314,
graphql: async () => ({ typeDefs: await typeDefs(), resolvers }),
apolloServerContext: async (subdomain, context) => {
context.models = await generateModels(subdomain);
return context;
},
trpcAppRouter: {
router: appRouter,
createContext: async (subdomain, context) => {
context.models = await generateModels(subdomain);
return context;
},
},
});
Endpoints mounted for you
| Endpoint | Condition | Notes |
|---|---|---|
GET /health |
always | Returns ok; used for health checks. |
GET /debug-sentry |
always | Throws a test error for Sentry. |
POST /graphql |
always | Apollo Server v4 subgraph (buildSubgraphSchema). |
* /trpc |
trpcAppRouter |
tRPC express adapter with the shared context factory. |
GET /subscriptionPlugin.js |
hasSubscriptions |
Serves the file at subscriptionPluginPath, rate-limited to 1000 req/15 min/IP. |
The server sets keepAliveTimeout to 120 s and headersTimeout to 121 s, parses JSON bodies up to 15 MB, and registers a SIGINT/SIGTERM handler that closes HTTP, closes Mongoose, and leaves service discovery.
Tenant isolation with generateModels
Every request carries a subdomain, the tenant the request belongs to, derived from the request host by getSubdomain. connectionResolvers.ts defines loadClasses(db), which registers your Mongoose models on a connection, and exports generateModels = createGenerateModels<IModels>(loadClasses) (generate-models.ts).
- Self-hosted (
VERSION !== 'saas'): models bind to the single sharedmongoose.connection. - SaaS: the subdomain maps to an organization and models bind to
useDb('erxes_<organizationId>', { useCache: true }).
Because apolloServerContext and trpcAppRouter.createContext run per request, models are always scoped to the requesting tenant. Never cache models across requests and never query another plugin's collections; use tRPC calls for cross-service reads.
GraphQL composition and the resolver pipeline
The generated plugin composes typeDefs from apolloCommonTypes plus per-module types/queries/mutations strings under extend type Query / extend type Mutation. Resolvers merge per-module query/mutation objects, apolloCustomScalars, and custom type resolvers.
startPlugin wraps every Query and Mutation resolver with wrapApolloResolvers, which applies, in order:
withBeforeResolvers: runs registeredmeta.beforeResolvershandlers, which can rewrite args or resolve the call early.wrapPermission: rejects unauthenticated calls (checkLogin), unless the resolver carrieswrapperConfig(skipPermission,forClientPortal,cpUserRequired).withLogging: mutations only; sends the call to the logs pipeline.withSentryCapture: reports non-EXPECTEDerrors to Sentry with operation tags.
The context your resolvers receive (IMainContext plus whatever apolloServerContext adds) includes user, cpUser, clientPortal, subdomain, processId, eventHandlers, __(doc) (stamps the process id), and checkPermission(action) for per-action permission checks.
The meta object
meta does two things: joinErxesGateway serializes it into the Redis key erxesservice:config:<name> for core and the gateway to read, and startPlugin dispatches selected keys to local subsystems:
meta key |
Startup dispatch |
|---|---|
automations |
startAutomations: tRPC producers on the app |
segments |
initSegmentProducers |
beforeResolvers |
startBeforeResolvers |
afterProcess |
startAfterProcess |
references |
initRecordReferences |
notifications |
initializePluginConfig(name, 'notifications', …) |
importExport |
startImportExportWorker: BullMQ workers |
payments |
startPayments: BullMQ worker |
permissions, tags, documents, properties, relations, and logs are serialized only; core-api reads them from the service-discovery config. The full IMeta type and every key are in Plugin Metadata & Extensions.
Service discovery and ports
On startup joinErxesGateway (service-discovery.ts) writes two Redis keys. erxes-service-<name> holds the address core and the gateway call; erxesservice:config:<name> holds JSON config (dbConnectionString, hasSubscriptions, meta, releaseVersion). The address is LOAD_BALANCER_ADDRESS if set, else http://localhost:<port> in development and http://plugin-<name>-api:<port> in production. In production the plugin also enqueues a delayed gateway/update-apollo-router BullMQ job so the gateway recomposes the supergraph.
The gateway reads ENABLED_PLUGINS (plus ENABLED_PLUGINS_ONLY_API for API-only plugins) to build the active plugin list, always core first. Choose an unused port: 3303–3305, 3307–3313, and 33010 are taken (core 3300, gateway 4000).