Persistencia, eventos y auth flow en el backend

Objetivos del capítulo
  • Persistir datos con Knex y migraciones versionadas.

  • Configurar el plugin-database para datos de aplicación.

  • Publicar y consumir eventos del EventsService.

  • Proteger endpoints con HttpAuthService y verificar identidad.

Tres bases de datos, tres capas

Backstage cocina con tres ollas separadas: la del catálogo (Postgres + tabla de entidades), la de plugins (la misma DB, esquema separado por plugin), y la de tu aplicación (tu DB si quieres aislar). Cada olla tiene su propósito.

Diagram
Figure 10. Tres capas de DB en Backstage

Migraciones con Knex

Las migraciones son archivos versionados que aplican cambios a la DB. Viven en plugins/<plugin>/migrations/<timestamp>_<name>.ts:

import type { Knex } from 'knex';

export async function up(knex: Knex): Promise {
  await knex.schema.createTable('status_changes', table => {
    table.increments('id').primary();
    table.string('service_name').notNullable().index();
    table.string('from_status').notNullable();
    table.string('to_status').notNullable();
    table.timestamp('changed_at').defaultTo(knex.fn.now());
    table.string('changed_by').notNullable();
  });
}

export async function down(knex: Knex): Promise {
  await knex.schema.dropTable('status_changes');
}
Convención de nombres
  • Timestamps en formato YYYYMMDDHHMMSS.

  • up() aplica; down() revierte.

  • Las migraciones son inmutables: una vez en producción, no las editas; creas una nueva.

plugin-database: el servicio de DB

Para que tu plugin use una DB, no escribes new Knex(…​). Pides DatabaseService al framework:

import {
  createBackendPlugin,
  coreServices,
} from '@backstage/backend-plugin-api';
import { Knex } from 'knex';

export const databasePlugin = createBackendPlugin({
  id: 'sazon-database',
  register(env) {
    env.registerInit({
      deps: {
        database: coreServices.database,
        logger: coreServices.logger,
      },
      async init({ database, logger }) {
        const client: Knex = await database.getClient();
        logger.info('Applying sazon migrations');
        await client.migrate.latest({
          directory: __dirname + '/../migrations',
        });
      },
    });
  },
});
¿Por qué no instanciar Knex?

Porque el framework ya configuró el cliente con la conexión, el pool y las migraciones. Si lo instanciaras a mano, te saltas las migraciones del framework y rompes el ciclo de vida.

Dónde corre

plugin-database crea un esquema por plugin dentro de la misma DB Postgres. Los datos quedan aislados por esquema, no por DB física. Para aislar físicamente, configura una conexión distinta en app-config.yaml.

Eventos: el bus interno

El EventsService es un bus pub/sub interno. Útil para reaccionar a cambios sin acoplar plugins.

import {
  createBackendPlugin,
  coreServices,
} from '@backstage/backend-plugin-api';
import { EventParams, EventsService } from '@backstage/plugin-events-backend';

export const eventsPlugin = createBackendPlugin({
  id: 'sazon-events',
  register(env) {
    env.registerInit({
      deps: {
        events: coreServices.events,
        logger: coreServices.logger,
      },
      async init({ events, logger }) {
        // Subscribe to events from any source
        await events.subscribe({
          id: 'sazon-status-listener',
          topics: ['sazon.status'],
          async onEvent(params: EventParams) {
            logger.info(`status event: ${params.topic} ${params.eventPayload}`);
            // handle event
          },
        });
      },
    });
  },
});

// Helper used elsewhere to publish
export async function publishStatusChange(
  events: EventsService,
  payload: { serviceName: string; from: string; to: string; changedBy: string },
) {
  await events.publish({
    topic: 'sazon.status',
    eventPayload: payload,
  });
}
Eventos vs webhooks
  • Eventos viven dentro del proceso de Backstage. Son baratos y síncronos.

  • Webhooks salen al exterior. Son útiles para integraciones con terceros (GitHub, GitLab).

No confundas: el EventsService no es para notificar a sistemas externos.

Diagram
Figure 11. Pub/sub del EventsService

Auth flow: proteger endpoints

Proteger un endpoint es extraer el Principal del request:

import {
  createBackendPlugin,
  coreServices,
} from '@backstage/backend-plugin-api';
import { InputError } from '@backstage/errors';
import { Router } from 'express';
import { z } from 'zod';

const ChangeStatusBody = z.object({
  serviceName: z.string().min(1),
  toStatus: z.enum(['healthy', 'degraded', 'down']),
});

export const httpAuthPlugin = createBackendPlugin({
  id: 'sazon-http-auth-example',
  register(env) {
    env.registerInit({
      deps: {
        httpRouter: coreServices.httpRouter,
        httpAuth: coreServices.httpAuth,
        logger: coreServices.logger,
      },
      async init({ httpRouter, httpAuth, logger }) {
        const router = Router();

        router.post('/status', async (req, res) => {
          const credentials = await httpAuth.credentials(req);
          const principal = await httpAuth.getPrincipal(credentials);
          if (!principal) {
            res.status(401).json({ title: 'Unauthorized', status: 401 });
            return;
          }

          const parsed = ChangeStatusBody.safeParse(req.body);
          if (!parsed.success) {
            throw new InputError(parsed.error.message);
          }

          logger.info(
            `${principal.subject} changed ${parsed.data.serviceName} -> ${parsed.data.toStatus}`,
          );
          res.status(202).json({ accepted: true });
        });

        httpRouter.use(router);
      },
    });
  },
});
No confundir autenticación con autorización
  • Autenticación = quién es (HttpAuthService.credentials).

  • Autorización = qué puede hacer (PermissionsService en :cap-14).

El http-auth-protect resuelve solo la primera. Para la segunda, usa el PermissionPolicy del RBAC plugin.

Example 7. Receta del capítulo
  1. Crea una migración versionada con up() y down().

  2. Pide DatabaseService en registerInit({ deps }) para aplicar migraciones.

  3. Publica eventos con EventsService.publish({ topic, eventPayload }).

  4. Protege endpoints con HttpAuthService.credentials() + getPrincipal().

  5. Si necesitas permisos, delega a PermissionsService.

Resumen

  • Las migraciones viven en migrations/ y son versionadas por timestamp.

  • DatabaseService da un cliente Knex configurado por el framework.

  • EventsService es el bus pub/sub interno, no un sistema de webhooks.

  • Proteger endpoints = HttpAuthService.credentials() + getPrincipal().

Glosario del capítulo

migración

Archivo versionado que aplica un cambio de esquema a la DB.

Knex

SQL query builder para Node, usado por Backstage como capa de DB.

EventsService

Bus pub/sub interno, síncrono y en proceso.

PermissionsService

Servicio que aplica políticas de RBAC del plugin @backstage/plugin-permission-backend.

esquema

Agrupación lógica de tablas dentro de una DB Postgres.

Próximo capítulo

Tu primer plugin frontend —cambiamos de piso: del backend al frontend, React + Material UI + createPlugin.

Parte III: Parte 3 — Frontend: la sala y la carta

React, Material UI, plugins frontend, componentes y el menú del portal. Lo que el developer ve, toca y firma.