# Backend Plugins

Tenant-aware API services with gateway registration


Build a tenant-aware API service that registers with the gateway through Redis service discovery.

Start with [Create a Plugin](https://erxes.io/docs/create-plugin). For schema and service-call conventions see [GraphQL & tRPC](https://erxes.io/docs/plugin-graphql-trpc); for the `meta` extension points see [Plugin Metadata & Extensions](https://erxes.io/docs/plugin-metadata-extensions).

## `startPlugin` options

Every backend plugin's `src/main.ts` calls [`startPlugin(configs)`](https://github.com/erxes/erxes/blob/f91e8c6f4714c910a32c6b6e14fe085f6435c89e/backend/erxes-api-shared/src/utils/start-plugin.ts) 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](https://github.com/erxes/erxes/blob/f91e8c6f4714c910a32c6b6e14fe085f6435c89e/backend/plugins/sales_api/src/main.ts) uses every option):

```ts
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](https://github.com/erxes/erxes/blob/f91e8c6f4714c910a32c6b6e14fe085f6435c89e/backend/erxes-api-shared/src/utils/mongo/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](https://erxes.io/docs/plugin-graphql-trpc) 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](https://erxes.io/docs/plugin-metadata-extensions).

## Service discovery and ports

On startup `joinErxesGateway` ([service-discovery.ts](https://github.com/erxes/erxes/blob/f91e8c6f4714c910a32c6b6e14fe085f6435c89e/backend/erxes-api-shared/src/utils/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`).

