PluginContext
Todo plugin exporta una función init que el servidor ejecuta al cargarlo. Su firma exacta es:
import type { PluginContext } from '@openfactu/plugin-sdk';
export const init = async (context: PluginContext) => { // ...};
// Firma del tipo:// type PluginInit = (context: PluginContext) => void | Promise<void>;Resumen
Sección titulada «Resumen»PluginContext tiene 8 campos:
| Campo | Qué es | Referencia |
|---|---|---|
app |
Servidor HTTP (Express) para registrar rutas propias | esta página |
migration |
Añadir campos a tablas core y crear tablas propias | Migraciones y tablas |
hooks |
Registro genérico de hooks por evento | Catálogo de hooks |
documents |
Atajos onBeforeCreate / onAfterCreate para documentos |
Catálogo de hooks |
factuApi |
API programática de negocio (documentos, maestros, contabilidad…) | FactuAPI |
db |
Acceso Drizzle directo (schema public y por tenant) |
esta página |
widgets |
Registro programático de widgets de dashboard | Manifest y UI |
aiTools |
Registro de tools para el chat de IA | esta página |
app — rutas HTTP
Sección titulada «app — rutas HTTP»Instancia del servidor HTTP (estilo Express, tipada como any). Permite al plugin exponer sus propios endpoints:
export const init = async ({ app }: PluginContext) => { app.get('/api/plugins/mi-plugin/estado', (req, res) => { res.json({ ok: true }); });};Por convención, prefija tus rutas con /api/plugins/<id-del-plugin>/ para evitar colisiones con el core y con otros plugins.
migration — campos y tablas
Sección titulada «migration — campos y tablas»Dos métodos, ambos idempotentes (se ejecutan en cada arranque sin duplicar nada):
migration: { addCustomField: (opts: { pluginId: string; tableName: CoreTableName | string; fieldName: string; type: 'TEXT' | 'INTEGER' | 'DECIMAL' | 'BOOLEAN' | 'JSONB'; label: string; }) => Promise<void>; createTable: (opts: { pluginId: string; tableName: string; columns: Array<{ name: string; type: 'TEXT' | 'INTEGER' | 'DECIMAL' | 'BOOLEAN' | 'JSONB' | 'UUID' | 'TIMESTAMP'; primaryKey?: boolean; nullable?: boolean; default?: string; }>; }) => Promise<void>;}Detalle completo, incluidas las 35 tablas core extensibles, en Migraciones y tablas.
hooks y documents — eventos
Sección titulada «hooks y documents — eventos»hooks: { register: (event: string, handler: HookHandler) => void;};documents: { onBeforeCreate: (tableName: string, handler: HookHandler) => void; onAfterCreate: (tableName: string, handler: HookHandler) => void;};hooks.registeres la vía genérica: acepta cualquier evento del catálogo ('salesInvoice.posted','payment.created','partners.list.afterFetch'…).documents.onBeforeCreate/onAfterCreateson atajos para documentos, y reciben el nombre de tabla en PascalCase ('SalesInvoice'), no el nombre de evento en camelCase.
El catálogo completo de 39 eventos, el contenido de HookContext y la explicación de los dos estilos están en el Catálogo de hooks.
factuApi — API de negocio
Sección titulada «factuApi — API de negocio»La API programática de Keirost, con transacciones atómicas, factories de documentos, consultas de maestros, contabilidad, RRHH y pagos. Es el mismo FactuApi que se usa en scripts:
export const init = async ({ factuApi, hooks }: PluginContext) => { hooks.register('salesInvoice.posted', async (ctx) => { await factuApi.transaction(ctx.tenantId, ctx.db, ctx.user, async (api) => { const partner = await api.getPartner(ctx.data.partnerId); // ... }); });};db — acceso directo a base de datos
Sección titulada «db — acceso directo a base de datos»db: { public: PluginDrizzleClient; // schema 'public' (Tenant, GlobalUser, PluginField...) forTenant: (tenantId: string) => Promise<PluginDrizzleClient>; schema: any; // módulo de schema del server}db.public— cliente Drizzle del schema globalpublic.db.forTenant(tenantId)— la única vía para datos de un tenant: resuelve el schema físico a través de la tablaTenant, nunca acepta un nombre de schema arbitrario (evita fugas entre tenants).db.schema— módulo de schema tipado del server.
import { eq } from 'drizzle-orm';
const tenantDb = await db.forTenant(tenantId);const items = await tenantDb.select().from(db.schema.items).limit(5);const [partner] = await tenantDb .select() .from(db.schema.businessPartners) .where(eq(db.schema.businessPartners.id, partnerId));Usa db.schema para construir queries Drizzle en vez de SQL crudo.
widgets — widgets de dashboard
Sección titulada «widgets — widgets de dashboard»Registro programático de widgets, alternativa a declararlos en manifest.json:
widgets: { registerDashboard: (widget: PluginDashboardWidgetInput) => void;}widgets.registerDashboard({ id: 'mi-widget', title: 'Mi widget', component: 'ui/MiWidget.tsx', // ruta relativa al componente ESM del plugin size: 'md', // grid de 4 columnas: sm=1, md=2, lg=3, full=4 order: 100,});Si registras dos veces el mismo id, el widget se actualiza (útil con hot reload). La variante declarativa (ui.dashboardWidgets del manifest) tiene el mismo shape — ver Manifest y UI.
aiTools — tools para el chat de IA
Sección titulada «aiTools — tools para el chat de IA»Los plugins pueden aportar herramientas al chat de IA (Keiro), igual que aportan hooks o widgets:
aiTools: { register: (name: string, factory: (ctx: AiChatToolContext) => any) => void;}
interface AiChatToolContext { tenantClient: PluginDrizzleClient; // cliente Drizzle del tenant de la conversación tenantId: string; tenantSchema: string; user: any; apiBase?: string;}La factory se invoca por petición con el contexto del tenant y usuario de la conversación, y debe devolver el resultado de tool({...}) del paquete ai (Vercel AI SDK):
import { tool } from 'ai';import { z } from 'zod';import { eq } from 'drizzle-orm';
aiTools.register('mi_plugin_consulta', (ctx) => tool({ description: 'Consulta los puntos de fidelidad de un interlocutor.', inputSchema: z.object({ partnerId: z.string() }), execute: async ({ partnerId }) => { const [row] = await ctx.tenantClient .select() .from(db.schema.businessPartners) .where(eq(db.schema.businessPartners.id, partnerId)); return { points: row?.loyalty_points ?? 0 }; }, }),);- Si el
nameya lo usa otra tool (del core o de otro plugin), el registro se ignora — nunca sobreescribe una tool existente. - Si tu tool crea o modifica datos, pasa
needsApproval: trueatool({...}): el chat mostrará la misma tarjeta de confirmación que usan las acciones del core.