# Storage, KV & Settings Plugins have three data mechanisms: | Mechanism | Purpose | Access | | ------------------- | ----------------------------------------- | ---------------------- | | **Storage** | Document collections with indexed queries | `ctx.storage` | | **KV** | Key-value pairs for state and settings | `ctx.kv` | | **Settings Schema** | Auto-generated admin UI for configuration | `admin.settingsSchema` | ## Storage Collections Declare in `definePlugin({ storage })`. EmDash creates the schema automatically — no migrations. ```typescript definePlugin({ id: "forms", version: "1.0.0", storage: { submissions: { indexes: [ "formId", // Single-field index "status", "createdAt", ["formId", "createdAt"], // Composite index ], }, forms: { indexes: ["slug"], }, }, }); ``` Storage is scoped to the plugin — `submissions` in plugin `forms` is separate from `submissions` in another plugin. ### CRUD ```typescript const { submissions } = ctx.storage; await submissions.put("sub_123", { formId: "contact", email: "user@example.com" }); const item = await submissions.get("sub_123"); const exists = await submissions.exists("sub_123"); await submissions.delete("sub_123"); ``` ### Batch Operations ```typescript const items = await submissions.getMany(["sub_1", "sub_2"]); // Map await submissions.putMany([ { id: "sub_1", data: { formId: "contact", status: "new" } }, { id: "sub_2", data: { formId: "contact", status: "new" } }, ]); const deletedCount = await submissions.deleteMany(["sub_1", "sub_2"]); ``` ### Querying Only indexed fields can be queried. Non-indexed queries throw. ```typescript const result = await ctx.storage.submissions.query({ where: { formId: "contact", status: "pending", }, orderBy: { createdAt: "desc" }, limit: 20, }); // result.items - Array of { id, data } // result.cursor - Pagination cursor // result.hasMore - Boolean ``` ### Where Operators ```typescript // Exact match where: { status: "pending" } // Range where: { createdAt: { gte: "2024-01-01" } } where: { score: { gt: 50, lte: 100 } } // In where: { status: { in: ["pending", "approved"] } } // Starts with where: { slug: { startsWith: "blog-" } } ``` ### Pagination ```typescript let cursor: string | undefined; do { const result = await ctx.storage.submissions!.query({ orderBy: { createdAt: "desc" }, limit: 100, cursor, }); // process result.items cursor = result.cursor; } while (cursor); ``` ### Counting ```typescript const total = await ctx.storage.submissions!.count(); const pending = await ctx.storage.submissions!.count({ status: "pending" }); ``` ### Index Design | Query Pattern | Index Needed | | ---------------------------------------- | ------------------------- | | Filter by `formId` | `"formId"` | | Filter by `formId`, order by `createdAt` | `["formId", "createdAt"]` | | Order by `createdAt` only | `"createdAt"` | Composite indexes support filtering on the first field + ordering by the second. ### Type Safety ```typescript interface Submission { formId: string; status: "pending" | "approved" | "spam"; createdAt: string; } // Cast in hook/route handlers const submissions = ctx.storage.submissions as StorageCollection; ``` ### Full API ```typescript interface StorageCollection { get(id: string): Promise; put(id: string, data: T): Promise; delete(id: string): Promise; exists(id: string): Promise; getMany(ids: string[]): Promise>; putMany(items: Array<{ id: string; data: T }>): Promise; deleteMany(ids: string[]): Promise; query(options?: QueryOptions): Promise>; count(where?: WhereClause): Promise; } ``` ## KV Store General-purpose key-value store. Use for internal state, cached computations, or programmatic access to settings. ```typescript interface KVAccess { get(key: string): Promise; set(key: string, value: unknown): Promise; delete(key: string): Promise; list(prefix?: string): Promise>; } ``` ### Key Naming Conventions | Prefix | Purpose | Example | | ----------- | ----------------------------- | ----------------- | | `settings:` | User-configurable preferences | `settings:apiKey` | | `state:` | Internal plugin state | `state:lastSync` | | `cache:` | Cached data | `cache:results` | ```typescript await ctx.kv.set("settings:webhookUrl", url); await ctx.kv.set("state:lastRun", new Date().toISOString()); const allSettings = await ctx.kv.list("settings:"); ``` ## Settings Schema Declare `admin.settingsSchema` to auto-generate a settings form in the admin UI: ```typescript admin: { settingsSchema: { siteTitle: { type: "string", label: "Site Title", description: "Used in title tags", default: "", }, maxItems: { type: "number", label: "Max Items", default: 100, min: 1, max: 1000, }, enabled: { type: "boolean", label: "Enabled", default: true, }, theme: { type: "select", label: "Theme", options: [ { value: "light", label: "Light" }, { value: "dark", label: "Dark" }, ], default: "light", }, apiKey: { type: "secret", label: "API Key", description: "Encrypted at rest", }, }, } ``` ### Setting Types | Type | UI | Notes | | --------- | ------------ | ----------------------------------------- | | `string` | Text input | Optional `multiline: true` for textarea | | `number` | Number input | Optional `min`, `max` | | `boolean` | Toggle | | | `select` | Dropdown | Requires `options: [{ value, label }]` | | `secret` | Masked input | Encrypted at rest, never shown after save | ### Reading Settings Settings are accessed via KV with `settings:` prefix: ```typescript const enabled = (await ctx.kv.get("settings:enabled")) ?? true; const apiKey = await ctx.kv.get("settings:apiKey"); ``` Schema defaults are UI defaults only — not auto-persisted. Handle missing values with `??` or persist defaults in `plugin:install`: ```typescript "plugin:install": async (_event, ctx) => { await ctx.kv.set("settings:enabled", true); await ctx.kv.set("settings:maxItems", 100); } ``` ## When to Use What | Use Case | Mechanism | | -------------------------------------------- | --------------------------------- | | Admin-editable preferences | `settingsSchema` + KV `settings:` | | Internal state (timestamps, cursors) | KV `state:` | | Collections of documents (logs, submissions) | Storage | | Cached computations | KV `cache:` |