Build a Storage Plugin
Implement IDbPlugin from @routier/core/plugins when Routier needs to target a backend that does not already have a storage plugin. Application users selecting a built-in backend do not need this SPI.
Requirements
A plugin must implement a small interface that Routier uses during reads/writes:
- initialize/destroy lifecycle
- add/update/remove batched entity operations
- query execution with parameterized filters, ordering, skip/take
- change tracking integration (apply computed/tracked fields after save)
- identity/index awareness (keys, distinct, composite indexes)
Minimal skeleton
import { BulkPersistResult } from "@routier/core/collections";
import type { IDbPlugin, DbPluginBulkPersistEvent, DbPluginQueryEvent, DbPluginEvent, ITranslatedValue } from "@routier/core/plugins";
import { PluginEventCallbackPartialResult, PluginEventCallbackResult, PluginEventResult } from "@routier/core/results";
import { JsonTranslator } from "@routier/core/plugins/translators";
export class MyPlugin implements IDbPlugin {
// Use the constructor to take in any plugin options
constructor() {
}
query<TShape>(event: DbPluginQueryEvent<any, TShape>, done: PluginEventCallbackResult<ITranslatedValue<TShape>>): void {
// Execute the event.operation against your backend and invoke the callback
// Use a translator to wrap results in ITranslatedValue (allows iteration for grouped queries
// and determines if change tracking should be enabled)
const translator = new JsonTranslator(event.operation);
const results: unknown[] = []; // Your query results here
// translate() automatically wraps results in ITranslatedValue
const translatedValue = translator.translate(results);
done(PluginEventResult.success(event.id, translatedValue));
}
bulkPersist(event: DbPluginBulkPersistEvent, done: PluginEventCallbackPartialResult<BulkPersistResult>): void {
// Persist adds/updates/removes from event.operation to your backend
// The operation is a key value pair of schema Id + changes
for (const [schemaId, changes] of event.operation) {
const {
adds,
updates,
hasItems,
removes,
tags,
total
} = changes;
}
done(PluginEventResult.error(event.id, new Error("Not implemented")));
}
destroy(event: DbPluginEvent, done: PluginEventCallbackResult<never>): void {
// Cleanup resources/connections
done(PluginEventResult.success(event.id));
}
}Key behaviors
- Respect schema metadata passed by collections (keys, indexes, nullable/optional/defaults).
- Compute fields marked as
computed()after persistence; persist whentracked(). - For
identity()columns, return generated values so entities can be updated in memory beforesaveChangesAsync()resolves. - For
distinct()/index()ensure unique or indexed storage if supported by the backend. - Push what each query executed into
event.executedQueriesafter it runs —{ text, parameters? }, once per backend read. This is how your plugin supports.explain(). A plugin that does not push still works; explanations then mark its step as not reported.
Separation for single-collection datastores
If your datastore persists all entities into one physical table/collection (e.g. PouchDB), add a tracked computed property to each schema that records its collection name. This guarantees clear separation between entity types and prevents cross‑collection collisions when fields share names (like name). See the tracked + computed example in the schema modifiers reference: Tracked computed. With SQL/SQLite backends, this is not an issue since data is already isolated per table.
Testing your plugin
- Start with the Memory plugin behavior as a reference.
- Use the CRUD how‑to pages to validate operations and saved changes.
- Wire into an example app and run live queries to ensure incremental updates behave as expected.
Examples to study
- Built‑in implementations:
Next steps
- Query Translation for Plugin Authors — push filters, joins, sorting, and aggregation into the backend.
- Result Translation for Plugin Authors — adapt backend rows and finish operations in memory.
- Expose configuration through the constructor and document the backend's durability, concurrency, migration, and failure guarantees.