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:

OptionTypePurpose
namestringPlugin name used for service discovery, Sentry tags, and queue prefixes. Must match the ENABLED_PLUGINS entry.
portnumberListen 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.
expressRouterexpress.RouterExtra routes mounted before middlewares (webhooks, file routes).
middlewaresany[]Additional Express middleware.
apiHandlers{ method, path, resolver }[]Simple REST handlers wrapped in the shared logHandler.
corsOptionscors optionsPassed to cors().
hasSubscriptionsbooleanRequires subscriptionPluginPath; serves the subscription bundle.
subscriptionPluginPathstringFile served at GET /subscriptionPlugin.js (rate-limited).
onServerInit(app) => Promise<void>Runs after registration; plugins start MQ workers here.
metaIMetaExtension-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

EndpointConditionNotes
GET /healthalwaysReturns ok; used for health checks.
GET /debug-sentryalwaysThrows a test error for Sentry.
POST /graphqlalwaysApollo Server v4 subgraph (buildSubgraphSchema).
* /trpctrpcAppRoutertRPC express adapter with the shared context factory.
GET /subscriptionPlugin.jshasSubscriptionsServes 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 keyStartup dispatch
automationsstartAutomations: tRPC producers on the app
segmentsinitSegmentProducers
beforeResolversstartBeforeResolvers
afterProcessstartAfterProcess
referencesinitRecordReferences
notificationsinitializePluginConfig(name, 'notifications', …)
importExportstartImportExportWorker: BullMQ workers
paymentsstartPayments: 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).

Was this helpful?