Plugin Settings
Plugins need configuration—API keys, feature flags, display preferences. EmDash provides two mechanisms: a settings schema for admin-configurable options and a KV store for programmatic access.
Settings Schema
Section titled “Settings Schema”Declare a settings schema in admin.settingsSchema to auto-generate an admin UI:
import { definePlugin } from "emdash";
export default definePlugin({ id: "seo", version: "1.0.0",
admin: { settingsSchema: { siteTitle: { type: "string", label: "Site Title", description: "Used in title tags and meta", default: "", }, maxTitleLength: { type: "number", label: "Max Title Length", description: "Characters before truncation", default: 60, min: 30, max: 100, }, generateSitemap: { type: "boolean", label: "Generate Sitemap", description: "Automatically generate sitemap.xml", default: true, }, defaultRobots: { type: "select", label: "Default Robots", options: [ { value: "index,follow", label: "Index & Follow" }, { value: "noindex,follow", label: "No Index, Follow" }, { value: "noindex,nofollow", label: "No Index, No Follow" }, ], default: "index,follow", }, apiKey: { type: "secret", label: "API Key", description: "Encrypted at rest", }, }, },});EmDash generates a settings form in the plugin’s admin section. Users edit settings without touching code.
Field Types
Section titled “Field Types”String
Section titled “String”Text input for single-line or multiline strings.
siteTitle: { type: "string", label: "Site Title", description: "Optional help text", default: "My Site", multiline: false // Set true for textarea}Number
Section titled “Number”Numeric input with optional min/max constraints.
maxItems: { type: "number", label: "Maximum Items", default: 100, min: 1, max: 1000}Boolean
Section titled “Boolean”Toggle switch for true/false values.
enabled: { type: "boolean", label: "Enabled", description: "Turn this feature on or off", default: true}Select
Section titled “Select”Dropdown for predefined options.
theme: { type: "select", label: "Theme", options: [ { value: "light", label: "Light" }, { value: "dark", label: "Dark" }, { value: "auto", label: "System" } ], default: "auto"}Secret
Section titled “Secret”Encrypted field for sensitive values like API keys. Never sent to the client after saving.
apiKey: { type: "secret", label: "API Key", description: "Stored encrypted"}Accessing Settings
Section titled “Accessing Settings”Read settings in hooks and routes via ctx.kv:
"content:beforeSave": async (event, ctx) => { // Read a setting const maxLength = await ctx.kv.get<number>("settings:maxTitleLength"); const apiKey = await ctx.kv.get<string>("settings:apiKey");
// Use defaults if not set const limit = maxLength ?? 60;
ctx.log.info("Using max length", { limit }); return event.content;}Settings are stored with the settings: prefix by convention. This distinguishes user-configurable values from internal plugin state.
KV Store API
Section titled “KV Store API”The KV store (ctx.kv) is a general-purpose key-value store for plugin data:
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 }>>;}Reading Values
Section titled “Reading Values”// Get a single valueconst enabled = await ctx.kv.get<boolean>("settings:enabled");
// Get with typeconst config = await ctx.kv.get<{ url: string; timeout: number }>("state:config");Writing Values
Section titled “Writing Values”// Set a valueawait ctx.kv.set("settings:lastSync", new Date().toISOString());
// Set complex valuesawait ctx.kv.set("state:cache", { data: items, expiry: Date.now() + 3600000,});Listing Values
Section titled “Listing Values”// List all settingsconst settings = await ctx.kv.list("settings:");// Returns: [{ key: "settings:enabled", value: true }, ...]
// List all plugin keysconst all = await ctx.kv.list();Deleting Values
Section titled “Deleting Values”const deleted = await ctx.kv.delete("state:tempData");// Returns true if key existedKey Naming Conventions
Section titled “Key Naming Conventions”Use prefixes to organize KV data:
| Prefix | Purpose | Example |
|---|---|---|
settings: | User-configurable preferences | settings:apiKey |
state: | Internal plugin state | state:lastSync |
cache: | Cached data | cache:results |
// Good: clear prefixesawait ctx.kv.set("settings:webhookUrl", url);await ctx.kv.set("state:lastRun", timestamp);await ctx.kv.set("cache:feed", feedData);
// Avoid: no prefix, unclear purposeawait ctx.kv.set("url", url);Settings vs Storage vs KV
Section titled “Settings vs Storage vs KV”Choose the right storage mechanism:
| Use Case | Mechanism |
|---|---|
| Admin-editable preferences | admin.settingsSchema + ctx.kv with settings: |
| Internal plugin state | ctx.kv with state: |
| Collections of documents | ctx.storage |
Settings are for user-configurable values—things an admin might change. They get an auto-generated UI.
KV is for internal state like timestamps, sync cursors, or cached computations. No UI, just code.
Storage is for document collections with indexed queries—form submissions, audit logs, etc.
Loading Settings in Routes
Section titled “Loading Settings in Routes”API routes can expose settings to admin UI components:
routes: { settings: { handler: async (ctx) => { const settings = await ctx.kv.list("settings:"); const result: Record<string, unknown> = {};
for (const entry of settings) { const key = entry.key.replace("settings:", ""); result[key] = entry.value; }
return result; } },
"settings/save": { handler: async (ctx) => { const input = ctx.input as Record<string, unknown>;
for (const [key, value] of Object.entries(input)) { if (value !== undefined) { await ctx.kv.set(`settings:${key}`, value); } }
return { success: true }; } }}Default Values
Section titled “Default Values”Settings from settingsSchema are not automatically persisted. They’re defaults in the admin UI. Your code should handle missing values:
"content:afterSave": async (event, ctx) => { // Always provide a fallback const enabled = await ctx.kv.get<boolean>("settings:enabled") ?? true; const maxItems = await ctx.kv.get<number>("settings:maxItems") ?? 100;
if (!enabled) return; // ...}Alternatively, persist defaults in plugin:install:
hooks: { "plugin:install": async (_event, ctx) => { // Persist schema defaults await ctx.kv.set("settings:enabled", true); await ctx.kv.set("settings:maxItems", 100); }}Storage Implementation
Section titled “Storage Implementation”KV values are stored in the _options table with plugin-namespaced keys:
INSERT INTO _options (name, value) VALUES ('plugin:seo:settings:siteTitle', '"My Site"'), ('plugin:seo:settings:maxTitleLength', '60');The plugin:seo: prefix is added automatically. Your code uses settings:siteTitle, and EmDash stores it as plugin:seo:settings:siteTitle.
This ensures plugins can’t accidentally overwrite each other’s data.