Plugin Metadata & Extensions

Register permissions, automations, segments, and other cross-module hooks so core and the gateway can discover plugin behavior.

The meta object passed to startPlugin does two things: joinErxesGateway serializes it into the Redis key erxesservice:config:<name> (read by core-api, the gateway, and other services through getPlugin), and startPlugin mounts or starts the subsystems that need live producers. See Backend Plugins for the startPlugin options.

Extension-point keys

The IMeta keys from start-plugin.ts, with who consumes them:

meta keyConfig shapeConsumer
permissionsIPermissionConfigcore-api permission resolvers, group-action map, OAuth scopes (via Redis config)
automationsAutomationConfigsstartAutomations mounts tRPC producers; automations-service calls them
segmentsSegmentConfigsinitSegmentProducers; core segments engine and logs service call them
documents{ types: { label, contentType }[] }core documents (serialized)
notifications{ plugin, modules[{ name, icon, events[] }] }initializePluginConfig; core notifications (serialized)
importExportImportExportConfigsstartImportExportWorker: BullMQ workers on <plugin>-import-processor / -export-processor
payments{ <jobName>: (ctx, data) => … }startPayments: BullMQ worker on <plugin>-payments
beforeResolversBeforeResolversConfigstartBeforeResolvers; every GraphQL resolver runs through it
afterProcessAfterProcessConfigsstartAfterProcess; fires after mutations/documents/API calls/auth
referencesTRecordReferencesConfiginitRecordReferences; core record-reference lookups
tags{ types: [{ type, description }] }core tags (serialized)
propertiesIPropertyMetacore properties: systemFields per content type (serialized)
relations{ subscribedTypes: string[] }core relations activity log (serialized)
logsLogsConfigs ({ contentTypes: [{ moduleName, collectionName }] })logs service (serialized)

Sales registers nearly all of these in sales_api/src/main.ts and src/meta/; use it as the structural reference without importing its source.

Permissions: src/meta/permissions.ts

Declare an IPermissionConfig (permission.ts): { plugin, modules[], defaultGroups[] }. Each module has name, scopes (own/group/all), ownerFields, scopeField, and actions ({ title, name, description, always?, disabled?, oauthScope(s)?, type? }). Resolvers enforce actions via context.checkPermission('<action>'); OAuth clients map to the same action names.

defaultGroups provides ready-made groups keyed <plugin>:<group> (the colon marks them as default-group ids):

// sales_api/src/meta/permissions.ts (excerpt)
export const permissions: IPermissionConfig = {
  plugin: 'sales',
  modules: [
    {
      name: 'deal',
      description: 'Deals management',
      ownerFields: ['userId', 'assignedUserIds'],
      scopes: [
        { name: 'own', description: 'Deals created by or assigned to user' },
        { name: 'all', description: 'All deals' },
      ],
      actions: [
        { title: 'View deals', name: 'showDeals', description: 'View deals', always: true },
        { title: 'Add deals', name: 'dealsAdd', description: 'Create deals' },
      ],
    },
  ],
  defaultGroups: [
    {
      id: 'sales:admin',
      name: 'Sales Admin',
      permissions: [{ plugin: 'sales', module: 'deal', actions: ['showDeals', 'dealsAdd'], scope: 'all' }],
    },
  ],
};

beforeResolvers and afterProcess

beforeResolvers (beforeResolvers.ts) intercepts GraphQL resolvers by name before they run: { resolvers: { <rootField>: [<resolverName>…] }, handler, check?, blocker? }. The handler returns { status: 'ok' } (optionally rewriting args), { status: 'blocked', code, message }, or { status: 'resolved', data }. Sales uses it to rewrite productIds on productsRemove/productsMerge to only the ids not referenced by deals.

afterProcess fires after work completes: { rules: [{ type: 'afterMutation', mutationNames }], afterMutation, afterAuth, afterApiRequest, afterDocumentCreated, afterDocumentUpdated }. Sales uses afterMutation to sync POS product groups and publish subscription events.

Migrations and boundaries

Keep migrations inside the plugin (for example backend/plugins/operation_api/src/migrations/). Never read or mutate another plugin's collections; communicate through published GraphQL, tRPC, HTTP, event, or federation interfaces.

Was this helpful?