# Pluggable Core Storage Today plugins (OpenAPI, MCP, GraphQL, WorkOS Vault) each expose a typed `*Store` interface with named methods (`upsertSource`, `removeSource`, etc.) that users can override wholesale or decoratively. The core tables (`source`, `tool`, `secret`, `definition`) are the odd one out: the executor reaches into them via `core.create({ model: "tool", data })` on a raw `DBAdapter`. Two problems that fall out of that: 1. **Sidecar integrations need string-matching.** A user who wants to mirror tool writes to Algolia has to write `if (model === "tool")` inside an adapter wrapper. Brittle; no type help. 2. **Inconsistency.** Two patterns for the same job — plugins use typed stores, core does not. Contributors have to learn both. ## The refactor Introduce `CoreStore`, modelled on the existing plugin stores. ```ts export interface CoreStore { // sources readonly putSource: (source: StoredSource) => Effect.Effect; readonly getSource: (id: string) => Effect.Effect; readonly listSources: () => Effect.Effect; readonly removeSource: (id: string) => Effect.Effect; // tools readonly putTools: (tools: readonly StoredTool[]) => Effect.Effect; readonly getTool: (id: string) => Effect.Effect; readonly listTools: () => Effect.Effect; readonly removeToolsBySource: (sourceId: string) => Effect.Effect; // definitions readonly putDefinitions: (defs: readonly StoredDefinition[]) => Effect.Effect; readonly listDefinitionsBySource: ( sourceId: string, ) => Effect.Effect; readonly removeDefinitionsBySource: (sourceId: string) => Effect.Effect; // secrets (routing rows; provider-specific data lives in provider) readonly putSecret: (secret: StoredSecret) => Effect.Effect; readonly getSecret: (id: string) => Effect.Effect; readonly listSecrets: () => Effect.Effect; readonly removeSecret: (id: string) => Effect.Effect; } export const makeDefaultCoreStore = (deps: { adapter: DBAdapter; scope: Scope }): CoreStore => { /* typedAdapter wiring, one place */ }; ``` ## Wiring — defaults passed in deps, force the spread Same ergonomics as the future `search` hook and the existing plugin `storage:` options (which should be renamed to match, see below): ```ts export interface CoreStorageDeps { readonly adapter: DBAdapter; readonly scope: Scope; readonly defaults: CoreStore; } export interface ExecutorConfig { // ... readonly coreStorage?: (deps: CoreStorageDeps) => CoreStore; } // Inside createExecutor: const defaults = makeDefaultCoreStore({ adapter, scope }); const core = config.coreStorage ? config.coreStorage({ adapter, scope, defaults }) : defaults; // Executor now calls core.putTools(...), core.removeSource(...), etc. // No more adapter.create({ model: "tool", ... }) scattered through executor.ts. ``` **Why defaults-in-deps, not `Partial`:** - Partial overrides silently fall back on typos (`putTool` vs `putTools`) with no type help. - Anyone implementing from scratch (test doubles, a non-drizzle backend) gets no squigglies when they miss a method. - Defaults-in-deps gets "only write what you override" ergonomics _and_ keeps the full `CoreStore` return type enforced. ## What it unlocks **Algolia sidecar — no string-match, just method overrides:** ```ts const executor = yield * createExecutor({ scope, adapter, blobs, plugins, coreStorage: ({ defaults }) => ({ ...defaults, putTools: (tools) => Effect.gen(function* () { yield* defaults.putTools(tools); yield* Effect.promise(() => algolia.saveObjects({ indexName: `tools_${scope.id}`, objects: tools.map((t) => ({ objectID: t.id, ...t })), }), ); }), removeToolsBySource: (sourceId) => Effect.gen(function* () { const affected = yield* defaults.listTools(); yield* defaults.removeToolsBySource(sourceId); yield* Effect.promise(() => algolia.deleteObjects({ indexName: `tools_${scope.id}`, objectIDs: affected.filter((t) => t.sourceId === sourceId).map((t) => t.id), }), ); }), }), }); ``` Test doubles become trivial — either hand-roll the whole `CoreStore` against a `Map`, or override just the couple of methods a test cares about. ## Aligning plugin `storage:` options Today each plugin exposes `storage: (deps) => makeDefaultOpenapiStore(deps)`. The factory name leaks. Rename to the defaults-in-deps shape: ```ts // before storage: (deps) => makeDefaultOpenapiStore(deps); // after storage: ({ defaults }) => defaults; // no-op, identical to leaving off `storage:` // override one method storage: ({ defaults }) => ({ ...defaults, upsertSource: myUpsert }); ``` Mechanical sweep across openapi / mcp / graphql / workos-vault plugins. Worth doing in the same PR so there's one pattern everywhere. ## Scope of the refactor Not intellectually hard, but a broad diff: - Every `core.create / findOne / findMany / upsert({ model: "X" })` call in `packages/core/sdk/src/executor.ts` becomes a named method on `CoreStore`. That's the bulk of the work. - `makeDefaultCoreStore` is the one place the model-string wiring lives. - Plugin `storage:` signature update + every plugin's default store factory renamed to follow the `defaults` contract. - Docs/readme updates showing the new override pattern. Ideally lands _before_ pluggable search — the search sidecar story depends on this refactor to avoid the `if (model === "tool")` antipattern I'd otherwise be stuck with.