Skip to content

Storage

turbomem persists memories and vector embeddings through a pluggable StorageAdapter. Pick a built-in backend with a string preset, or pass your own adapter instance.

See Configuration for the full config shape and how storage fits alongside embeddings and extraction.

Quick comparison

PGlite (default)PGlite (browser)sqlite-vecUpstash Vector (edge)Pinecone (edge)
Preset"pglite""pglite" + idb://"sqlite-vec""upstash-vector""pinecone"
EngineWASM Postgres + pgvectorSame (IndexedDB VFS)SQLite + sqlite-vecUpstash Vector (HTTP)Pinecone (HTTP)
Native compileNoNoYes (better-sqlite3)NoNo
Extra installNone (bundled)None (import turbomem/browser)npm install better-sqlite3 sqlite-vecnpm install @upstash/vectornpm install @pinecone-database/pinecone@^8
Default path.turbomem/ (directory)idb://turbomem.turbomem.sqlite (file)Remote Upstash indexRemote Pinecone index
Vector searchpgvector HNSW, cosine (<=>)Samevec0 KNN, cosine distanceUpstash KNN, cosine (scores 0–1)Pinecone KNN, cosine (scores 0–1)
Best forDefault, zero native deps, broad Node compatibilityReact/SPA apps, offline-first, client-side persistenceTeams already on SQLite, familiar .db filesEdge Workers, Vercel Edge, stateless serverlessEdge Workers, Vercel Edge, stateless serverless

Both backends share the same API surface: scoped insert/search/delete, dimension guards at init(), and cosine similarity scores in the 0–1 range.

Selecting a backend

ts
new TurboMemory({
  storage: "pglite", // or "sqlite-vec" | "upstash-vector" | "pinecone" | StorageAdapter
  // ...
});

Omit storage to use PGlite. Backend-specific options live under pglite, sqliteVec, upstashVector, or pinecone in the config object; they are ignored when another backend is selected.

Dimensions are fixed per store

The vector column dimension is set from your embedding adapter at init(). A turbomem_meta table records the dimension so switching to a model with different dimensions against an existing store throws DimensionMismatchError. Use a fresh data directory or database file when you change embedding models.

PGlite (default)

PGlite runs a WASM Postgres instance inside your Node/Bun process. The pgvector extension handles vector storage and similarity search, no native compilation, no separate database server.

Setup

PGlite ships as a dependency of turbomem. No extra install step.

Configuration

ts
new TurboMemory({
  storage: "pglite",
  pglite: {
    dataDir: "./my-memory", // default: .turbomem in process.cwd(); use idb:// in browser
    inMemory: false,
    relaxedDurability: undefined, // defaults to true for idb:// paths
  },
  // ...
});

Data is written to a directory on disk (Postgres data files). Point dataDir at any path your process can write to.

For browser apps, prefix with idb:// and import from turbomem/browser. See the Browser guide.

For tests or ephemeral usage, pass a custom adapter with in-memory mode:

ts
import { PGliteStorageAdapter } from "turbomem";

new TurboMemory({
  storage: new PGliteStorageAdapter({ inMemory: true }),
  // ...
});

How it works

On init(), turbomem creates a memories table with a vector(N) column sized to your embedding adapter, an HNSW index for cosine search, and scope indexes on user_id / agent_id. Search uses pgvector's <=> operator; scores are mapped to cosine similarity as 1 - distance.

sqlite-vec (optional)

sqlite-vec adds vector search to SQLite via a vec0 virtual table. turbomem loads it through better-sqlite3 a native Node addon that must be compiled for your platform and Node version.

Setup

Install the optional peer dependencies alongside turbomem:

bash
npm install better-sqlite3 sqlite-vec

If you see a Node ABI mismatch after upgrading Node, rebuild the native module:

bash
npm rebuild better-sqlite3

If the packages are missing at runtime, turbomem throws a ConfigError with install instructions.

Configuration

ts
new TurboMemory({
  storage: "sqlite-vec",
  sqliteVec: {
    dbPath: "./my-memory.db", // default: .turbomem.sqlite in process.cwd()
    inMemory: false, // set true for ephemeral storage (tests)
  },
  // ...
});

PGlite and sqlite-vec use separate default paths (.turbomem/ vs .turbomem.sqlite) so switching backends does not mix Postgres data files with a SQLite file.

For tests:

ts
new TurboMemory({
  storage: "sqlite-vec",
  sqliteVec: { inMemory: true },
  // ...
});

Or instantiate the adapter directly:

ts
import { SqliteVecStorageAdapter } from "turbomem";

new TurboMemory({
  storage: new SqliteVecStorageAdapter({ inMemory: true }),
  // ...
});

How it works

sqlite-vec uses a two-table layout:

  1. memories - scalar fields (content, scope, metadata, timestamps) with a UUID primary key.
  2. memory_embeddings - a vec0 virtual table holding vectors and scope columns for filtered KNN search, linked to memories by SQLite rowid.

Search runs cosine KNN inside SQLite (distance_metric=cosine); results are joined back to memories and scored as 1 - distance to match PGlite semantics.

Upstash Vector (edge, optional)

Upstash Vector stores vectors in a remote index over HTTP. Use it on edge runtimes where local disk is unavailable.

Setup

Install the optional peer dependency:

bash
npm install @upstash/vector

Create an Upstash Vector index in the Upstash Console with dimensions matching your embedding model and cosine similarity. See the Edge guide for the full step-by-step walkthrough.

Configuration

ts
new TurboMemory({
  storage: "upstash-vector",
  upstashVector: {
    url: process.env.UPSTASH_VECTOR_REST_URL,
    token: process.env.UPSTASH_VECTOR_REST_TOKEN,
    namespace: "my-app", // optional
  },
  // ...
});

Credentials fall back to UPSTASH_VECTOR_REST_URL and UPSTASH_VECTOR_REST_TOKEN when omitted. If @upstash/vector is not installed, turbomem throws a ConfigError with install instructions.

How it works

On init(), turbomem verifies the Upstash index dimension count matches your embedding adapter. Each memory is upserted as a vector with metadata fields for content, scope, timestamps, and user metadata. Search uses Upstash metadata filters for scoping and returns cosine similarity scores in the 0–1 range.

Operational notes

getAll() paginates the full index and filters client-side. deleteAll() uses Upstash metadata filter deletes, which perform a full index scan. See Edge limitations for details.

Pinecone (edge, optional)

Pinecone stores vectors in a remote serverless index over HTTP. Use it on edge runtimes where local disk is unavailable.

Setup

Install the optional peer dependency (requires v8+ for metadata filter APIs used by getAll()):

bash
npm install @pinecone-database/pinecone@^8

Create a serverless Pinecone index in the Pinecone Console with dimensions matching your embedding model and cosine similarity. See the Edge guide for the full step-by-step walkthrough.

Integration patterns

Choose based on your runtime and bundler — not on whether you deploy to edge.

Simple (Node, Bun, Next.js Node runtime)

Use the built-in preset. turbomem dynamically loads @pinecone-database/pinecone at runtime. This is the default for scripts, Express, and Next.js App Router routes on the Node.js runtime.

ts
import { TurboMemory } from "turbomem";

new TurboMemory({
  storage: "pinecone",
  pinecone: {
    apiKey: process.env.PINECONE_API_KEY,
    index: process.env.PINECONE_INDEX,
    host: process.env.PINECONE_INDEX_HOST, // optional — skips describeIndex
    namespace: "my-app", // optional
  },
  // ...
});

Explicit client (bundled SSR — Vite, TanStack Start)

When your server bundle inlines turbomem source (common with Vite SSR), the dynamic import inside storage: "pinecone" can fail even though the package is installed. Statically import Pinecone in your app and pass a pre-built index client via indexClient:

ts
import { Pinecone } from "@pinecone-database/pinecone";
import { TurboMemory, PineconeStorageAdapter } from "turbomem";

const pc = new Pinecone({ apiKey: process.env.PINECONE_API_KEY! });
let index = pc.index({
  name: process.env.PINECONE_INDEX!,
  host: process.env.PINECONE_INDEX_HOST, // optional
});
if (process.env.PINECONE_NAMESPACE) {
  index = index.namespace(process.env.PINECONE_NAMESPACE);
}

new TurboMemory({
  storage: new PineconeStorageAdapter({ indexClient: index }),
  // ...
});

For Vite-based apps, also externalize Pinecone in your SSR config:

ts
// vite.config.ts
export default defineConfig({
  ssr: {
    external: ["@pinecone-database/pinecone"],
  },
});

See the TanStack Start + Pinecone starter for a full working example (src/lib/pinecone.ts).

Adapter with credentials (either runtime)

You can also pass PineconeStorageAdapter with apiKey and index instead of using the storage: "pinecone" preset. turbomem still loads the SDK dynamically — use the explicit client pattern above when bundlers break that import.

ts
import { TurboMemory, PineconeStorageAdapter } from "turbomem";

new TurboMemory({
  storage: new PineconeStorageAdapter({
    apiKey: process.env.PINECONE_API_KEY,
    index: "my-index",
  }),
  // ...
});

Credentials fall back to PINECONE_API_KEY, PINECONE_INDEX, and PINECONE_INDEX_HOST when omitted. If @pinecone-database/pinecone is not installed, turbomem throws a ConfigError with install instructions.

How it works

On init(), turbomem verifies the Pinecone index dimension count matches your embedding adapter. Each memory is upserted as a vector with flat metadata fields for content, scope, timestamps, and a JSON-serialised metadataJson field for user metadata (Pinecone does not support nested metadata objects). Search uses Pinecone metadata filters for scoping and returns cosine similarity scores in the 0–1 range.

Operational notes

getAll() uses metadata filter fetches or paginated list + fetch. deleteAll() uses Pinecone metadata filter deletes. Both can be costly on large indexes. Requires a serverless Pinecone index. See Edge limitations for details.

Custom adapter

Implement the StorageAdapter interface to plug in any vector store - Qdrant, an hosted Postgres instance, or an in-memory mock for tests:

ts
interface StorageAdapter {
  init(dimensions: number): Promise<void>;
  insert(memory: Omit<Memory, "id" | "createdAt" | "updatedAt">): Promise<Memory>;
  update(
    id: string,
    patch: { content: string; embedding: number[]; metadata?: Record<string, unknown> },
  ): Promise<Memory>;
  search(embedding: number[], scope: MemoryScope, limit: number): Promise<MemorySearchResult[]>;
  getAll(scope: MemoryScope): Promise<Memory[]>;
  delete(id: string): Promise<void>;
  deleteAll(scope: MemoryScope): Promise<void>;
  close?(): Promise<void>;
}
ts
new TurboMemory({
  storage: myCustomStorage,
  // ...
});

Custom adapters receive the embedding dimension in init() so the underlying vector column or index can be created with a matching size.

Choosing a backend

Use PGlite when:

  • You want the simplest install (npm install turbomem and go).
  • Native compilation is a concern (CI, restricted environments, Electron without rebuild tooling).
  • WASM Postgres fits your deployment target.

Use sqlite-vec when:

  • You already standardize on SQLite or want a single .db file to ship or back up.
  • Your team is comfortable managing better-sqlite3 native builds.
  • You prefer the sqlite-vec extension over embedded Postgres.

Use Upstash Vector when:

  • You deploy to edge runtimes (Cloudflare Workers, Vercel Edge, Deno Deploy).
  • You need shared remote storage across stateless serverless instances.
  • See the Edge guide for setup.

Use Pinecone when:

  • You deploy to edge runtimes and already use Pinecone for vector search.
  • You need shared remote storage across stateless serverless instances.
  • See the Edge guide for setup.

Use a custom adapter when:

  • You need a hosted or cloud vector database.
  • You want to share storage with an existing system of record.

Next steps