Persistencia, eventos y auth flow en el backend
|
Objetivos del capítulo
|
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.
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
|
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
|
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
No confundas: el |
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
El |
-
Crea una migración versionada con
up()ydown(). -
Pide
DatabaseServiceenregisterInit({ deps })para aplicar migraciones. -
Publica eventos con
EventsService.publish({ topic, eventPayload }). -
Protege endpoints con
HttpAuthService.credentials()+getPrincipal(). -
Si necesitas permisos, delega a
PermissionsService.
Resumen
-
Las migraciones viven en
migrations/y son versionadas por timestamp. -
DatabaseServiceda un cliente Knex configurado por el framework. -
EventsServicees 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.