Adapters Overview
Storage backends — persistence, lifecycle, and how to choose
Adapters are the persistence layer of ctrodb. They handle all read and write operations to the underlying storage. The rest of the library (schema validation, query engine, reactivity, plugins) is adapter-agnostic — swap the adapter and everything still works.
Built-in adapters
| Adapter | Environment | Persistence |
|---|---|---|
IndexedDBAdapter | Browser (all modern) | Persists in IndexedDB — survives reloads and tab closures |
MemoryAdapter | Node.js, browser, Deno | In-memory Map — lost on process exit or page reload |
Auto-detection
When no adapter is specified, ctrodb selects one automatically:
const db = new Database({ name: "my-app" })
// Browser with indexedDB → IndexedDBAdapter
// Node.js, Deno, or no IDB → MemoryAdapter
The detection checks for window.indexedDB at runtime:
// src/adapter/create.ts (simplified)
if (typeof window !== "undefined" && window.indexedDB) {
return new IndexedDBAdapter()
}
return new MemoryAdapter()
Explicit selection
You can pick an adapter explicitly using the adapter config option:
import { Database, createAdapter, MemoryAdapter } from "ctrodb"
// Short string form:
const db1 = new Database({ adapter: "memory" })
const db2 = new Database({ adapter: "indexeddb" })
// Adapter instance:
const db3 = new Database({ adapter: new MemoryAdapter() })
// Via createAdapter factory:
const db4 = new Database({ adapter: createAdapter("memory") })
Lifecycle
Every adapter follows the same lifecycle:
connect() → isConnected() → operations... → disconnect()
const adapter = new MemoryAdapter()
await adapter.connect("my-db", schemaConfig)
adapter.isConnected() // true
// ... create, find, update, delete, query ...
await adapter.disconnect()
adapter.isConnected() // false
The Database class manages this lifecycle automatically — you don't call
connect() or disconnect() on the adapter directly.
StorageAdapter interface
Every adapter implements the StorageAdapter interface:
interface StorageAdapter {
readonly name: string
// Lifecycle
connect(name: string, schema: SchemaConfig | null): Promise<void>
disconnect(): Promise<void>
isConnected(): boolean
// Schema version (persisted in _ctrodb_meta store)
getSchemaVersion(): Promise<number>
setSchemaVersion(version: number): Promise<void>
// CRUD
create(collection: string, data: unknown): Promise<unknown>
findById(collection: string, id: ID): Promise<unknown>
findAll(collection: string): Promise<unknown[]>
update(collection: string, id: ID, changes: unknown): Promise<unknown>
delete(collection: string, id: ID): Promise<void>
deleteMany(collection: string, ids: ID[]): Promise<void>
// Indexed queries
scanIndex(
collection: string,
indexName: string,
range: IDBKeyRange | undefined,
postFilters: QueryCondition[],
): Promise<unknown[]>
// Transactions
transaction<T>(fn: (ctx: TransactionContext) => Promise<T>): Promise<T>
// Metadata key-value store (schema version, sync state, etc.)
getMetadata(key: string): Promise<unknown>
setMetadata(key: string, value: unknown): Promise<void>
}
All return types are unknown — the Collection layer casts them to the
correct generic type.
ID handling
IDs are UUID strings generated by the Collection layer:
// src/collection.ts (simplified)
#generateId(): ID {
if (typeof crypto.randomUUID === "function") {
return crypto.randomUUID()
}
return `${Date.now()}-${Math.random().toString(36).slice(2, 10)}`
}
async create(data) {
const raw = { ...data }
if (raw.id === undefined) {
raw.id = this.#generateId() // UUID
}
// ... schema validation, hooks, adapter.create()
}
Adapters themselves do not generate IDs — they receive the full record with
id already set. If you call an adapter's create() directly without an id,
some adapters (like MemoryAdapter) will throw.
You can supply your own IDs:
await collection.create({ id: "my-custom-id", title: "Hello" })
// The Collection layer will use your id instead of generating one
SchemaConfig
Adapters receive the full schema configuration during connect(), which
includes:
interface SchemaConfig {
version: number
collections: Record<string, CollectionSchema>
pluginStoreNames?: (string | PluginStoreConfig)[] // stores created by plugins
}
The IndexedDBAdapter uses this to create object stores and indexes during
migration. The MemoryAdapter pre-creates collection maps.
Transaction API
All adapters support transactions. If the transaction callback throws, all changes are rolled back.
Transaction context methods vary by adapter:
Warning: The ctx.collection(name) return type differs between adapters.
See each adapter's page for the exact API.
IndexedDBAdapter
Returns a bound collection proxy — method calls don't need the collection name:
await db.transaction(async (ctx) => {
const todos = ctx.collection("todos")
await todos.create({ id: "1", title: "A" })
const record = await todos.findById("1")
await todos.update("1", { title: "B" })
await todos.delete("1")
})
MemoryAdapter
Returns the raw adapter — you must pass the collection name to every method:
await db.transaction(async (ctx) => {
const adapter = ctx.collection("todos") as MemoryAdapter
await adapter.create("todos", { id: "1", title: "A" })
const all = await adapter.findAll("todos")
})
scanIndex
Both adapters implement scanIndex() for indexed lookups by field value.
This powers the query planner's index_scan strategy and the sync engine's
status-based queries.
IndexedDBAdapter: Uses native IDB indexes — fast, only returns matching records.
MemoryAdapter: Does a full collection scan with in-memory range and post-filter matching.
const results = await adapter.scanIndex(
"users",
"email",
IDBKeyRange.only("alice@test.com"),
[],
)
Metadata store
Both adapters implement a key-value metadata store for internal state:
await adapter.setMetadata("schemaVersion", 2)
const version = await adapter.getMetadata("schemaVersion") // 2
The IndexedDBAdapter stores metadata in a special _ctrodb_meta object store.
The MemoryAdapter stores it in a private Map<string, unknown>.
Custom adapters
Any object that satisfies the StorageAdapter interface can serve as a ctrodb
backend. See the Custom Adapters guide.
How is this guide?
Last updated on Jul 2, 2026