Storage
Pluggable storage backends, custom implementations, and browser support.
Table of contents
Overview
Vectra separates index logic from file I/O through the FileStorage interface. Every index operation — reading vectors, writing metadata, listing files — goes through this abstraction. You can swap storage backends without changing any index code.
Vectra ships with three built-in implementations:
| Implementation | Environment | Persistence |
|---|---|---|
LocalFileStorage | Node.js | Disk (filesystem) |
IndexedDBStorage | Browser, Electron | IndexedDB |
VirtualFileStorage | Any | In-memory (ephemeral) |
FileStorage interface
All storage backends implement this interface:
import { FileStorage } from 'vectra';
interface FileStorage {
createFile(filePath: string, content: Buffer | string): Promise<void>;
createFolder(folderPath: string): Promise<void>;
deleteFile(filePath: string): Promise<void>;
deleteFolder(folderPath: string): Promise<void>;
getDetails(fileOrFolderPath: string): Promise<FileDetails>;
listFiles(folderPath: string, filter?: 'files' | 'folders' | 'all'): Promise<FileDetails[]>;
pathExists(fileOrFolderPath: string): Promise<boolean>;
readFile(filePath: string): Promise<Buffer>;
upsertFile(filePath: string, content: Buffer | string): Promise<void>;
}
interface FileDetails {
name: string;
path: string;
isFolder: boolean;
fileType?: string;
}
Key behaviors:
createFilethrows if the file already exists;upsertFilecreates or overwrites.createFoldercreates parent directories recursively.deleteFolderremoves the folder and all its contents.listFilesaccepts an optional filter:'files','folders', or'all'(default).
Built-in implementations
LocalFileStorage
The default for Node.js. Wraps fs/promises for direct filesystem access.
import { LocalIndex, LocalFileStorage } from 'vectra';
// Default — uses the index folder path directly
const index = new LocalIndex('./my-index');
// Explicit — pass a storage instance with a custom root
const storage = new LocalFileStorage('/data/vectra');
const index = new LocalIndex('./my-index', { storage });
IndexedDBStorage
Browser-compatible persistent storage backed by IndexedDB. Works in any modern browser and Electron renderer processes.
import { LocalIndex, IndexedDBStorage } from 'vectra';
const storage = new IndexedDBStorage('my-app-db'); // database name (default: 'vectra-db')
const index = new LocalIndex('my-index', { storage });
IndexedDBStorage stores files and folders in two IndexedDB object stores, with parent-path indexes for efficient directory listing. Paths are normalized to forward slashes.
Additional methods:
close()— close the database connectiondestroy()— delete the entire database and all stored data
VirtualFileStorage
Fully in-memory storage. Works in Node.js and browsers. Useful for testing, CI, and ephemeral workflows where you don’t need persistence.
import { LocalIndex, VirtualFileStorage } from 'vectra';
const storage = new VirtualFileStorage();
const index = new LocalIndex('test-index', { storage });
All data is lost when the process exits. Use this for tests and short-lived demos, not production data.
Custom implementations
You can implement the FileStorage interface to store index data anywhere — S3, SQLite, a remote API, etc.
import { FileStorage, FileDetails, LocalIndex } from 'vectra';
class S3Storage implements FileStorage {
constructor(private bucket: string, private prefix: string) {}
async createFile(filePath: string, content: Buffer | string): Promise<void> {
// Upload to S3...
}
async createFolder(folderPath: string): Promise<void> {
// S3 doesn't have real folders — create a marker object or no-op
}
async deleteFile(filePath: string): Promise<void> {
// Delete from S3...
}
async deleteFolder(folderPath: string): Promise<void> {
// List and delete all objects with the folder prefix...
}
async getDetails(fileOrFolderPath: string): Promise<FileDetails> {
// HEAD the object and return metadata...
}
async listFiles(folderPath: string, filter?: 'files' | 'folders' | 'all'): Promise<FileDetails[]> {
// List objects under the prefix...
}
async pathExists(fileOrFolderPath: string): Promise<boolean> {
// Check if the S3 key exists...
}
async readFile(filePath: string): Promise<Buffer> {
// Download from S3...
}
async upsertFile(filePath: string, content: Buffer | string): Promise<void> {
// PUT to S3 (create or overwrite)...
}
}
// Use it like any other storage backend
const storage = new S3Storage('my-bucket', 'vectra/');
const index = new LocalIndex('my-index', { storage });
Your implementation must match the behavioral contract: createFile must throw if the file exists, createFolder must be recursive, and deleteFolder must remove contents. See the built-in implementations for reference.
Running in the browser
Vectra can run entirely in the browser or Electron — no Node.js required. Use IndexedDBStorage for persistence or VirtualFileStorage for ephemeral use.
Setup
Import from vectra/browser (or just vectra — bundlers that support conditional exports will resolve automatically). This entry point excludes Node-specific modules (LocalFileStorage, FileFetcher, WebFetcher, FolderWatcher) and includes browser alternatives like BrowserWebFetcher.
If you accidentally import LocalFileStorage in a browser bundle, the stub throws a helpful error directing you to use IndexedDBStorage or VirtualFileStorage instead.
Browser example with IndexedDB
import { LocalDocumentIndex, TransformersEmbeddings, IndexedDBStorage } from 'vectra/browser';
// Persistent browser storage via IndexedDB
const storage = new IndexedDBStorage('my-app-vectors');
// Local embeddings run entirely in the browser — no API key needed
const embeddings = await TransformersEmbeddings.create();
const index = new LocalDocumentIndex({
folderPath: 'my-index',
embeddings,
storage,
});
if (!(await index.isIndexCreated())) {
await index.createIndex({ version: 1 });
}
await index.upsertDocument('doc://notes', 'Your document text here...', 'txt');
const results = await index.queryDocuments('search query', {
maxDocuments: 5,
maxChunks: 10,
});
What works in the browser
| Feature | Browser support |
|---|---|
| LocalIndex | Yes (with IndexedDBStorage or VirtualFileStorage) |
| LocalDocumentIndex | Yes |
| LocalEmbeddings | Yes (uses @huggingface/transformers) |
| TransformersEmbeddings | Yes (async factory, GPU/WASM device options, quantization) |
| OpenAIEmbeddings | Yes (makes fetch requests to API) |
| Metadata filtering | Yes |
| VirtualFileStorage | Yes |
| IndexedDBStorage | Yes |
| BrowserWebFetcher | Yes (uses Fetch API + DOMParser) |
| LocalFileStorage | No (Node.js only) |
| FileFetcher | No (Node.js only) |
| WebFetcher | No (Node.js only — uses cheerio) |
| FolderWatcher | No (Node.js only — uses fs.watch) |
| CLI | No (Node.js only) |
Cross-platform paths
Vectra includes a pathUtils module that normalizes paths across Node.js and browsers. All storage implementations use forward-slash separators internally, so paths like my-index/items/abc.json work consistently everywhere.
import { pathUtils } from 'vectra';
pathUtils.join('my-index', 'items', 'abc.json'); // 'my-index/items/abc.json'
pathUtils.basename('my-index/items/abc.json'); // 'abc.json'
pathUtils.dirname('my-index/items/abc.json'); // 'my-index/items'
Storage formats
Vectra supports two serialization formats for index data. The storage format is independent of the storage backend — you can use protobuf with LocalFileStorage, IndexedDBStorage, or any custom implementation.
JSON (default)
Human-readable, zero dependencies. Files use .json extension.
Protocol Buffers
Compact binary format providing 40-50% smaller index files. Requires the optional protobufjs package. Files use .pb extension.
npm install protobufjs
import { LocalIndex, ProtobufCodec } from 'vectra';
const index = new LocalIndex('./my-index', { codec: new ProtobufCodec() });
Migrating between formats
npx vectra migrate ./my-index --to protobuf
npx vectra migrate ./my-index --to json
import { migrateIndex } from 'vectra';
await migrateIndex('./my-index', { to: 'protobuf' });
See the CLI Reference for details.