Los plugins sandboxed almacenan su configuración en el almacén KV por plugin y renderizan la UI de edición como una página de Block Kit: describes el formulario en JSON y lo sirves desde una ruta.
Todo sucede a través de la misma maquinaria que el plugin ya usa para hooks y rutas – no hay nada extra que aprender.
El almacén KV
Cada plugin obtiene un almacén clave-valor privado accesible como ctx.kv en cualquier hook o ruta. Es el lugar canónico para la configuración y cualquier otro pequeño estado persistente:
interface KVAccess {
get<T>(key: string): Promise<T | null>;
set(key: string, value: unknown): Promise<void>;
delete(key: string): Promise<boolean>;
list(prefix?: string): Promise<Array<{ key: string; value: unknown }>>;
}
KV es por plugin – las claves que escribes se almacenan bajo tu plugin id y no son visibles para otros plugins.
Lectura y escritura
// Read
const enabled = await ctx.kv.get<boolean>("settings:enabled");
const config = await ctx.kv.get<{ url: string; timeout: number }>("state:config");
// Write
await ctx.kv.set("settings:lastSync", new Date().toISOString());
await ctx.kv.set("state:cache", { data: items, expiry: Date.now() + 3600000 });
// Delete
const deleted = await ctx.kv.delete("state:tempData");
// List by prefix
const allSettings = await ctx.kv.list("settings:");
// → [{ key: "settings:enabled", value: true }, ...]
Convenciones de nomenclatura de claves
Usa prefijos para mantener separados diferentes tipos de valores. La convención en los plugins de EmDash:
| Prefijo | Propósito | Ejemplo |
|---|---|---|
settings: | Preferencias configurables por usuario | settings:apiKey |
state: | Estado interno del plugin | state:lastSync |
cache: | Datos en caché | cache:results |
// Clear prefixes
await ctx.kv.set("settings:webhookUrl", url);
await ctx.kv.set("state:lastRun", timestamp);
await ctx.kv.set("cache:feed", feedData);
// Avoid bare keys
await ctx.kv.set("url", url);
UI de configuración en Block Kit
Los plugins sandboxed describen su página de configuración como Block Kit. El administrador envía una interacción page_load a una ruta en tu plugin (convencionalmente routes.admin), y el plugin devuelve una descripción JSON del formulario. Cuando el usuario hace clic en Guardar, el administrador envía una interacción block_action o form_submit de vuelta; el plugin escribe en KV y devuelve bloques actualizados.
import type { PluginContext, SandboxedPlugin } from "emdash/plugin";
interface BlockInteraction {
type: "page_load" | "block_action" | "form_submit";
page?: string;
action_id?: string;
values?: Record<string, unknown>;
}
export default {
routes: {
admin: {
handler: async (routeCtx, ctx) => {
const interaction = routeCtx.input as BlockInteraction;
if (interaction.type === "page_load" && interaction.page === "/settings") {
return renderSettings(ctx);
}
if (interaction.type === "form_submit" && interaction.action_id === "save") {
await saveSettings(ctx, interaction.values ?? {});
return {
...(await renderSettings(ctx)),
toast: { message: "Settings saved", type: "success" },
};
}
return { blocks: [] };
},
},
},
} satisfies SandboxedPlugin;
async function renderSettings(ctx: PluginContext) {
const apiKey = (await ctx.kv.get<string>("settings:apiKey")) ?? "";
const enabled = (await ctx.kv.get<boolean>("settings:enabled")) ?? true;
const maxItems = (await ctx.kv.get<number>("settings:maxItems")) ?? 100;
return {
blocks: [
{ type: "header", text: "Plugin settings" },
{
type: "form",
block_id: "settings",
fields: [
{
type: "secret_input",
action_id: "apiKey",
label: "API key",
initial_value: apiKey,
},
{
type: "toggle",
action_id: "enabled",
label: "Enabled",
initial_value: enabled,
},
{
type: "number_input",
action_id: "maxItems",
label: "Max items",
min: 1,
max: 1000,
initial_value: maxItems,
},
],
submit: { label: "Save", action_id: "save" },
},
],
};
}
async function saveSettings(ctx: PluginContext, values: Record<string, unknown>) {
for (const [key, value] of Object.entries(values)) {
if (value !== undefined) {
await ctx.kv.set(`settings:${key}`, value);
}
}
}
Para conectar la página de configuración en la barra lateral del administrador, declárala en el manifiesto:
"admin": {
"pages": [{ "path": "/settings", "label": "Settings", "icon": "settings" }]
}
EmDash enruta automáticamente las interacciones page_load para esa ruta a tu ruta admin.
Consulta Block Kit para el conjunto completo de tipos de bloques, campos de formulario, campos condicionales y los helpers de constructor @emdash-cms/blocks.
Valores secretos
El campo secret_input de Block Kit se renderiza como una entrada enmascarada. Trata cualquier valor que el usuario ingrese allí con cuidado:
{
type: "secret_input",
action_id: "apiKey",
label: "API key",
// Don't seed initial_value with the real secret — pass an empty string or a sentinel,
// and only overwrite when the user enters a non-empty value.
initial_value: "",
}
Al guardar, omite las cadenas vacías para evitar borrar el secreto existente en cada guardado:
async function saveSettings(ctx: PluginContext, values: Record<string, unknown>) {
if (typeof values.apiKey === "string" && values.apiKey.length > 0) {
await ctx.kv.set("settings:apiKey", values.apiKey);
}
// ... other fields
}
Valores predeterminados
Las lecturas de KV devuelven null para claves que no se han escrito. Pasa valores predeterminados en el sitio de lectura:
const enabled = (await ctx.kv.get<boolean>("settings:enabled")) ?? true;
const maxItems = (await ctx.kv.get<number>("settings:maxItems")) ?? 100;
O persiste valores predeterminados durante la instalación:
hooks: {
"plugin:install": async (_event, ctx) => {
await ctx.kv.set("settings:enabled", true);
await ctx.kv.set("settings:maxItems", 100);
},
},
El compromiso es que plugin:install se ejecuta una vez por instalación. Si envías una nueva configuración en una versión posterior, solo las instalaciones nuevas ven el valor predeterminado – las instalaciones existentes necesitan una migración en plugin:activate (idempotente: solo escribir si falta) o continuar usando el fallback en tiempo de lectura.
Configuración vs. storage vs. KV
| Caso de uso | Mecanismo |
|---|---|
| Preferencias editables por admin | ctx.kv con prefijo settings: + página Block Kit |
| Estado interno del plugin | ctx.kv con prefijo state: |
| Colecciones de documentos (consultas) | ctx.storage |
KV es para valores pequeños identificados por una cadena – configuración, cursores de sincronización, cálculos en caché. Sin consultas, sin índices.
Storage es para colecciones de documentos con consultas indexadas – envíos de formularios, registros de auditoría, cualquier cosa donde quieras filtrar, paginar o contar.
Diseño de almacenamiento
Los valores KV se encuentran en la tabla _options con claves con namespace del plugin. Tu código usa settings:apiKey; EmDash lo almacena como plugin:<your-plugin-id>:settings:apiKey. El prefijo se agrega automáticamente y evita que un plugin lea o sobrescriba los datos KV de otro.
Plugins nativos: settingsSchema
Si estás escribiendo un plugin nativo (porque necesitas páginas de administración de React o componentes PT), puedes declarar un esquema de configuración directamente dentro de definePlugin() y dejar que EmDash genere automáticamente el formulario. Consulta Plugins nativos para esa ruta.