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).