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 shared mongoose.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:

  1. withBeforeResolvers: runs registered meta.beforeResolvers handlers, which can rewrite args or resolve the call early.
  2. wrapPermission: rejects unauthenticated calls (checkLogin), unless the resolver carries wrapperConfig (skipPermission, forClientPortal, cpUserRequired).
  3. withLogging: mutations only; sends the call to the logs pipeline.
  4. withSentryCapture: reports non-EXPECTED errors 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).

Operate on one system.

Talk to our team about consolidating your stack onto erxes. 30-minute walkthrough, custom to your org.