El ecosistema de plugins: la carta del restaurante

Tiempo de lectura: 22 min · Itinerarios: cocinero jefe, consultor, CTO. Lee el apéndice :cap-F antes si nunca has tocado un plugin de Backstage.

Objetivos del capítulo

Al terminar este capítulo vas a poder:

  • Distinguir entre plugin, módulo, scaffolder action y service.

  • Conocer el catálogo oficial de plugins del marketplace de Backstage.

  • Entender la diferencia entre el App Router clásico y el New Frontend System.

  • Decidir si conviene migrar a plugins dinámicos o quedarse con plugins estáticos.

  • Escribir un plugin que otro equipo pueda instalar sin tocar tu packages/.

El menú ya no es solo el Software Catalog

En el capítulo :cap-04 vimos el corazón de Backstage: el Software Catalog. Pero un restaurante con un único plato no sobrevive. Backstage vive o muere por su ecosistema de plugins — la capacidad de añadir Kubernetes, Jenkins, ArgoCD, Snyk, Tech Radar, Cost Insights, Datadog, GitHub Actions y cien integraciones más sin reescribir el core.

Spotify descubrió esto en 2020: su instancia interna tiene más de 200 plugins. La comunidad open source mantiene otros tantos. Backstage es, técnicamente, un agregador de UIs sobre datos que ya tienes en otros sistemas. Su valor está en la densidad del ecosistema, no en lo que hace el core.

Backstage sin plugins es como un restaurante que solo sirve agua. Sí, técnicamente cumple con "dar de beber", pero nadie vuelve.

Anatomía de un plugin

Un plugin de Backstage no es "un componente React". Es un paquete npm con un manifiesto concreto. Hay cuatro tipos:

Tipo Qué hace Dónde corre

Frontend plugin

Añade rutas, páginas, tarjetas al portal

En el bundle del frontend (Vite/Webpack)

Backend plugin

Expone APIs REST/GraphQL a otros plugins

En el proceso Node del backend

Módulo (backend module)

Extiende un backend plugin sin reemplazarlo

En el mismo proceso backend

Scaffolder action

Acción reutilizable dentro de un template

En el Scaffolder backend

Cada tipo tiene un factory concreto:

// Frontend plugin (legacy App Router)
import { createPlugin, createRouteRef } from '@backstage/core-plugin-api';

export const miPlugin = createPlugin({
  id: 'mi-plugin',
  routes: {
    root: createRouteRef({ id: 'mi-plugin/root' }),
  },
});

export const MiPluginPage = miPlugin.provide(
  createRoutableExtension({
    name: 'MiPluginPage',
    component: () => import('./components/Page').then(m => m.Page),
    mountPoint: miPlugin.routes.root,
  }),
);
// Backend plugin (New Backend System)
import { createBackendPlugin, coreServices } from '@backstage/backend-plugin-api';

export const miBackendPlugin = createBackendPlugin({
  pluginId: 'mi-plugin',
  register(env) {
    env.registerInit({
      deps: {
        logger: coreServices.logger,
        httpRouter: coreServices.httpRouter,
      },
      async init({ logger, httpRouter }) {
        httpRouter.use(await createRouter({ logger }));
      },
    });
  },
});
// Módulo (extiende catalog sin reemplazarlo)
import { createBackendModule, coreServices } from '@backstage/backend-plugin-api';

export const miModulo = createBackendModule({
  pluginId: 'catalog',
  moduleId: 'mi-modulo',
  register(env) {
    env.registerInit({
      deps: { catalog: catalogServiceRef },
      async init({ catalog }) {
        catalog.addProcessor(new MiProcessor());
      },
    });
  },
});

Si vienes de un plugin de la versión 1.x, el patrón "old school" era crear un plugin.ts con un createPlugin({ id: 'x' }) y exportar todo desde ahí. El New Frontend System cambia esto: ahora tu plugin declara extensiones con createFrontendPlugin y el integrador las activa o no según la configuración YAML.

Esto te permite crear plugins que el equipo A no sabe que existen, instalarlos sin tocar el App.tsx del equipo B, y desactivarlos si fallan. El viejo App Router era un main() único; el NFS es un registro de extensiones.

El catálogo oficial de plugins

Backstage no tiene una "App Store" cerrada como Slack. Tiene un marketplace abierto en el monorepo oficial, con ~250 plugins mantenidos por la comunidad. Los más populares, que verás en cualquier instalación real:

Plugin Categoría Para qué sirve

@backstage/plugin-kubernetes

DevOps

Ver pods, deployments, services del cluster desde Backstage

@backstage/plugin-jenkins

CI

Estado de jobs, última build, links a artefactos

@backstage/plugin-github-actions

CI

Estado de workflows de GitHub Actions por repo

@backstage/plugin-argocd

GitOps

Estado de aplicaciones ArgoCD, sync status, manifests

@backstage/plugin-tech-radar

Arquitectura

Visualizar adopciones, trials, holds de tecnologías

@backstage/plugin-cost-insights

FinOps

Coste por equipo, alertas de overspend

@backstage/plugin-snyk

Seguridad

Vulnerabilidades por repo, severidad, fixes disponibles

@backstage/plugin-lighthouse

Calidad

Auditorías de performance y accesibilidad por URL

Spotify mantiene los del core (catalog, scaffolder, techdocs, kubernetes, etc.). La comunidad mantiene los demás, cada uno con su cadencia de releases.

¿Puedes usar plugins de fuera del monorepo oficial? Sí, de hecho es lo recomendado. Para plugins internos de tu empresa (mi-equipo-dashboard, mi-cost-tracker, mi-deploy-bot) lo correcto es publicarlos en tu propio registro npm privado o en GitHub Packages. Backstage los descubre si configuras pluginPackages en el app-config.yaml o usas plugin-paths.

Plugin-paths: cómo descubre Backstage plugins externos

Por defecto, Backstage solo mira dentro de packages/ y plugins/ de su propio monorepo. Si quieres instalar plugins de fuera (un paquete publicado en npm), tienes dos opciones:

Opción 1: pluginPackages en app-config.yaml

# app-config.yaml
dynamicPlugins:
  frontend:
    backstage-plugin-kubernetes:
      mountPoints:
        - mountPoint: entity.catalog/overview/item
          module: KubernetesContent
          importName: EntityKubernetesContent

Esto le dice a Backstage: "el plugin Kubernetes monta su contenido en la pestaña de overview del catalog". El NFS (New Frontend System) lo lee al arrancar y enlaza las extensiones.

Opción 2: pluginPaths (legacy App Router)

// packages/app/src/index.ts (legacy)
import { registerPluginImportSideEffects } from '@backstage/frontend-plugin-api';

registerPluginImportSideEffects([
  {
    importName: 'KubernetesPlugin',
    module: () => import('@backstage/plugin-kubernetes'),
  },
]);

pluginPaths carga dinámicamente plugins externos. Es la antesala de los dynamic plugins que llegaron con Backstage 1.30.

Dynamic plugins (Backstage 1.30+)

Antes de 1.30, instalar un plugin en producción requería recompilar el bundle del frontend y hacer un redeploy completo. Backstage 1.30 introduce dynamic plugins: plugins empaquetados como bundles pre-compilados que se montan en caliente sin recompilar nada.

Dynamic plugins es lo que pasa cuando el equipo de plataforma decide que recompilar el frontend cada vez que alguien quiere un plugin es tan 2019 como usar npm install sin package-lock.json.

Diagram
Figure 19. Arquitectura clásica vs New Frontend System

El NFS es el camino a seguir para todo proyecto nuevo. Spotify recomienda que adoptes el NFS desde el día uno si empiezas en 1.30+. Si vienes del legacy App Router, la migración es un trabajo de uno o dos sprints: sustituir EntityPage.tsx por app.ts con createApp, mover las rutas a app.routes, y los widgets a blueprints.

El "metadata" de un plugin publicable

Para que un plugin sea descubrible, instalable y mantenible, su package.json debe declarar un bloque backstage:

{
  "name": "@mi-empresa/plugin-deploy-bot",
  "version": "1.2.3",
  "description": "Inicia deploys a Kubernetes desde Backstage",
  "backstage": {
    "role": "frontend-plugin",
    "pluginId": "deploy-bot",
    "pluginPackages": [
      "@mi-empresa/plugin-deploy-bot",
      "@mi-empresa/plugin-deploy-bot-backend"
    ]
  },
  "keywords": [
    "backstage",
    "backstage-plugin",
    "kubernetes",
    "deploy"
  ]
}

Este manifiesto permite:

  • Que el backstage-cli reconozca el plugin sin configuración adicional.

  • Que la búsqueda de plugins en el marketplace filtre por compatibilidad.

  • Que las herramientas de auditoría (backstage-plugin-audit, dependency-cruiser) detecten versiones incompatibles.

Regla de oro: si tu plugin no tiene bloque backstage en su package.json, no es un plugin de Backstage — es una librería de React que alguien metió en el bundle. Esto se manifiesta en errores crípticos al arrancar. Mejor declararlo desde el primer commit.

Escribir un plugin que el equipo vecino pueda instalar

El test definitivo de un plugin es: ¿puede otro equipo instalarlo sin tocar tu packages/app/src/index.ts? Si la respuesta es sí, has ganado. Vamos a verlo con un ejemplo:

Estructura del repo

mi-plugin-deploy/
├── packages/
│   ├── deploy-bot/            # frontend plugin
│   │   ├── package.json       # bloque "backstage"
│   │   ├── src/
│   │   │   ├── plugin.ts      # createFrontendPlugin
│   │   │   └── components/
│   │   └── README.md
│   └── deploy-bot-backend/    # backend plugin
│       ├── package.json
│       ├── src/
│       │   ├── module.ts      # createBackendPlugin
│       │   └── router.ts
│       └── README.md
└── README.md

Frontend plugin (NFS)

// packages/deploy-bot/src/plugin.ts
import { createFrontendPlugin, createEntityCardExtension } from '@backstage/frontend-plugin-api';

export const deployBotPlugin = createFrontendPlugin({
  pluginId: 'deploy-bot',
  extensions: [
    createEntityCardExtension({
      name: 'DeployBotCard',
      // El equipo vecino lo monta desde su app-config.yaml
      // sin tocar este código.
    }),
  ],
});

export default deployBotPlugin;

Backend plugin

// packages/deploy-bot-backend/src/router.ts
import { httpRouterServiceRef } from '@backstage/backend-plugin-api';
import express from 'express';

export async function createRouter() {
  const router = express.Router();
  router.get('/health', (_req, res) => res.json({ status: 'ok' }));
  return router;
}
// packages/deploy-bot-backend/src/module.ts
import { createBackendPlugin, coreServices } from '@backstage/backend-plugin-api';
import { createRouter } from './router';

export const deployBotBackend = createBackendPlugin({
  pluginId: 'deploy-bot',
  register(env) {
    env.registerInit({
      deps: {
        logger: coreServices.logger,
        httpRouter: coreServices.httpRouter,
      },
      async init({ logger, httpRouter }) {
        logger.info('Deploy bot backend starting');
        httpRouter.use(await createRouter());
      },
    });
  },
});

Cómo lo instala el equipo vecino

Solo añaden dos líneas a su packages/backend/src/index.ts:

// En el backend del equipo vecino
backend.add(import('@mi-empresa/plugin-deploy-bot-backend'));

Y en su app-config.yaml:

dynamicPlugins:
  frontend:
    '@mi-empresa/plugin-deploy-bot':
      mountPoints:
        - mountPoint: entity.catalog/overview/item
          module: DeployBotPlugin
          importName: DeployBotCard

Listo. Cero cambios en su App.tsx. El plugin declara sus propias extensiones y el NFS las monta en la pestaña de overview del catalog. Esa es la diferencia entre un plugin moderno y un plugin legacy.

Errores comunes

Errores típicos al empezar con plugins
  1. Olvidar el bloque backstage en package.json. El plugin se compila pero backstage-cli no lo reconoce.

  2. Hardcodear rutas de la app del vecino. Tu plugin nunca debe asumir que existe /catalog o /settings. Usa el app.routes API.

  3. Depender de un plugin core concreto (ej: @backstage/plugin-catalog sin peerDependencies). El equipo vecino puede no tener catalog instalado.

  4. Publicar como @backstage/plugin- sin ser parte del monorepo oficial. Genera confusión. Usa el scope de tu empresa: @mi-empresa/plugin-.

  5. Asumir que el legacy App Router y el NFS son compatibles. La migración rompe plugins que no tengan createFrontendPlugin. Revisa siempre las release notes.

Cuándo construir un plugin vs cuándo comprar SaaS

Si alguien te dice "construyamos un panel de Kubernetes en Backstage", la respuesta correcta es casi siempre "ya existe y se llama `@backstage/plugin-kubernetes`". Solo constrúyelo si el oficial no cubre el 80% de tu caso. La regla del restaurante: no inventes un plato si ya hay uno en la carta.

Las señales de que deberías construir tu propio plugin:

  • El dato que necesitas no existe en ningún sistema que Backstage pueda consultar.

  • El flujo de trabajo es único de tu empresa y tiene 0% de uso fuera de ella.

  • Ningún plugin oficial tiene más del 30% de overlap con tu necesidad.

Las señales de que NO deberías:

  • Spotify/Roadie/Red Hat ya lo construyeron.

  • Estarías reimplementando un dashboard que Kibana/Grafana/Datadog ya hace mejor.

  • El coste de mantenerlo supera el coste de comprar un SaaS.

Resumen

  • Backstage es un agregador de UIs. Su valor está en la densidad de plugins, no en el core.

  • Hay cuatro tipos: frontend plugin, backend plugin, módulo, scaffolder action.

  • El New Frontend System (≥ 1.30) reemplaza el App Router clásico: plugins declaran extensiones y el integrador las activa desde YAML.

  • Dynamic plugins te permiten instalar sin recompilar.

  • Un plugin publicable necesita un bloque backstage en package.json, no asumir rutas hardcoded, y respetar peerDependencies.

Glosario del capítulo

Plugin

Paquete npm con un bloque backstage en package.json que añade funcionalidad a Backstage.

Backend plugin

Plugin que expone APIs REST/GraphQL desde el proceso backend.

Frontend plugin

Plugin que añade rutas, páginas, tarjetas al bundle del frontend.

Módulo

Extensión de un plugin existente sin reemplazarlo (ej: añadir un processor al catalog).

Scaffolder action

Acción reutilizable dentro de un template.

New Frontend System (NFS)

Sistema de extensiones que reemplaza el App Router clásico desde 1.30.

Dynamic plugin

Plugin empaquetado como bundle pre-compilado que se monta sin recompilar.

App Router clásico

Sistema de routing legacy donde el integrador importa cada plugin manualmente.

Próximo capítulo

Capítulo 18 — Seguridad: cuando el portal tiene las llaves de la cocina — cerraduras, modelado de amenazas, secretos del Scaffolder y cómo defender tu IDP de los CVEs reales (CVE-2025-32791, CVE-2025-55285, etc.) que ya han aparecido en producción.