Move between providers
How to move a running application from one adapter to another without rewriting queries.
The verbs, filters, and result types stay the same when you move. What changes is the client you construct, how each provider stores ids and namespaces, and two filter edge cases.
Swap the adapter
Replace the client and the create*Store import. Nothing else in your code refers to the provider.
import { Pinecone } from "@pinecone-database/pinecone";
import { createPineconeStore } from "vecstore-sdk/pinecone";
const store = createPineconeStore({ client: new Pinecone({ apiKey }) });import { QdrantClient } from "@qdrant/js-client-rest";
import { createQdrantStore } from "vecstore-sdk/qdrant";
const store = createQdrantStore({ client: new QdrantClient({ url }) });import { Pool } from "pg";
import { createPgvectorStore } from "vecstore-sdk/pgvector";
const store = createPgvectorStore({ client: new Pool({ connectionString }) });import { createClient } from "@supabase/supabase-js";
import { createSupabaseStore } from "vecstore-sdk/supabase";
const store = createSupabaseStore({ client: createClient(url, key) });import { Index } from "@upstash/vector";
import { createUpstashStore } from "vecstore-sdk/upstash";
const store = createUpstashStore({ client: new Index({ url, token }) });import Cloudflare from "cloudflare";
import { createVectorizeStore } from "vecstore-sdk/vectorize";
const store = createVectorizeStore({
client: new Cloudflare({ apiToken }),
accountId,
});import { createClient } from "redis";
import { createRedisStore } from "vecstore-sdk/redis";
const client = createClient({ url });
await client.connect();
const store = createRedisStore({
client,
metadataFields: [{ field: "genre", type: "tag" }],
});Moving to Supabase needs a setup step the others do not: install sql/supabase.sql in the project before the first call. The Supabase page covers it.
Copy the data
The SDK does not move records for you. The SDK has no verb that lists records, so export ids with the provider's own scroll or list call through store.raw (Qdrant scroll, Pinecone listPaginated, Upstash range, a keyset SELECT on pgvector or Supabase, a paged FT.SEARCH on Redis, listVectors on Vectorize), then fetch them in pages of 1,000 with { includeVector: true } and upsert into the new index. fetch leaves vectors out unless you ask for them, and an upsert of a record without one fails (on pgvector and Supabase with invalid_argument). The adapter batches upserts, so pass as many records per call as fit in memory.
const page = await source.fetch(ids, { includeVector: true });
if (!page.ok) {
throw new Error(page.error.message);
}
const copied = await target.upsert(page.value);
if (!copied.ok) {
throw new Error(copied.error.message);
}Record ids and metadata round-trip unchanged. Vectors can come back with less precision than you wrote, because vector databases generally store 32-bit floats: on pgvector and Supabase 0.123456789 returns as 0.12345679. A Qdrant collection with the cosine metric also stores each vector normalized to unit length. Every provider accepts only string, number, boolean, and string[] metadata values. Each adapter adds a few rules of its own. Qdrant and Upstash in metadata mode reject metadata under _id or _namespace, and Vectorize rejects metadata under _id. All three reject a namespace that holds /, and Upstash in metadata mode also rejects one that holds a backslash or both quote characters. Redis rejects a value whose type contradicts the declared field type, and an index name that holds :.
Check what each provider stores
| Concern | Qdrant | pgvector and Supabase | Pinecone | Upstash | Vectorize | Redis |
|---|---|---|---|---|---|---|
| Index | Collection | Table | Index | Namespace | Index | Index over a key prefix |
| Namespace | _namespace payload key | namespace column | Native | _namespace metadata key, or a real namespace under namespaceMode: "native" | Native | The namespace tag, and a segment in the key |
| Id | UUID, hashed from your id when needed. The original id is kept in _id. | id text | Native | Prefixed with the namespace. The original id is kept in _id. Unchanged under namespaceMode: "native". | UUID, hashed from your id when needed. The original id is kept in _id. | The last segment of the key |
| Metadata | Payload | metadata jsonb | Metadata | Metadata | Metadata | The metadata object in the JSON document |
| Metric | Set on the collection | Read from the HNSW index operator class | Set on the index | Set on the Upstash index | Set on the index | Set on the vector field |
The pgvector and Supabase adapters share a table layout, so moving between those two copies no data. Point the other adapter at the same table.
If you read a Qdrant collection, a pgvector table, an Upstash namespace, or a Vectorize index with another tool, expect those extra keys and columns.
Moving to Upstash needs one step the others do not. Create the vector index in the Upstash console first with the dimension and similarity function you want, because @upstash/vector cannot create one. createIndex then checks your spec against it.
Moving to Vectorize needs one too. Name the properties you filter on in metadataIndexes before the first upsert, because a Vectorize filter matches nothing on a property that has no metadata index, and an index added later does not cover data already written. Vectorize also has no OR operator, no exists, and no delete beyond delete-by-id. See Cloudflare Vectorize.
Moving to Redis needs the same kind of step. Name the fields you filter on in metadataFields, because the Redis index schema decides what a query can match on. Unlike Vectorize, you can add a field later without writing the records again, but not with deleteIndex, which deletes the documents along with the index. Drop the index alone through the native client and create it again, as the Redis page shows. The server also has to carry the query engine and JSON. See Redis.
Re-check score thresholds
Qdrant, pgvector, Pinecone, and Vectorize return the provider's native score for the index metric, Upstash normalizes every metric to the range 0 to 1, and Redis returns a distance where lower is closer. A threshold tuned on one provider does not carry over. See the score table in Stores and indexes and check the threshold against real data.
Check the filter edge cases
Negation on missing fields and eq on array-valued fields behave differently per provider. See Provider differences. If your queries depend on either case, add an exists clause or store a scalar field before you move.
Verify the move
Before you cut over, run the live conformance suite against the new backend:
VECSTORE_LIVE=1 QDRANT_URL=http://localhost:6333 bun run test:liveThe suite creates a temporary index, runs the store verbs, the record verbs, and the eq, gt, isIn, notIn, and not builders, and deletes the index. The pgvector and Supabase adapters also run on PGlite in the unit suite.