Using extensions
An extension is a package that teaches Prisma ORM a database feature it does not know out of the box. Installing one gives you new column types to declare and new query operations to call, along with the migrations that install the feature in the database itself. Vector search, geospatial data, full-text search, typed JSON, and provider-specific integrations all reach your project this way.
Reach for an extension when your application needs one of those database features and you still want everything Prisma ORM gives you for the rest of your data: typed schema declarations, generated TypeScript, migration support, and query helpers.
Your contract is what Prisma ORM 8 calls your schema: contract.prisma in place of schema.prisma. In a project made with npm create prisma@latest it is at src/prisma/contract.prisma.
Adding an extension means installing its package and then registering it in two places, your config file and your client. These steps use pgvector, the vector search extension, as the example, and they assume a PostgreSQL project from the quickstart, so @prisma/orm-postgres is already installed and the extension package is added next to it. Supabase is set up differently, so if that is the extension you are adding, read the note under the catalog first.
bun add @prisma/orm-extension-pgvectorpnpm add @prisma/orm-extension-pgvectoryarn add @prisma/orm-extension-pgvectornpm install @prisma/orm-extension-pgvectorA project made with npm create prisma@latest already has prisma.config.ts, so open it and add the extension to the extensions array.
import { definePrismaConfig } from 'prisma/config';
import pgvector from '@prisma/orm-extension-pgvector/control';
import { defineConfig as ormConfig } from '@prisma/orm-postgres/config';
export default definePrismaConfig({
orm: ormConfig({
contract: './src/prisma/contract.prisma',
extensions: [pgvector],
db: {
connection: process.env['DATABASE_URL']!,
},
}),
});@prisma/orm-postgres is the package you call directly: ormConfig(...) in prisma.config.ts and postgres(...) in db.ts. Other extensions are listed in the extensions array passed to those two calls. The example renames defineConfig to ormConfig so it is not confused with definePrismaConfig, and you can keep the original name.
Import the paths the extension's page in the extension directory gives you. For pgvector they are /control in prisma.config.ts and /runtime in db.ts.
Add the extension to the extensions array on the client as well, which is what gives the db object you write queries against the extension's query operations and value types.
import pgvector from '@prisma/orm-extension-pgvector/runtime';
import postgres from '@prisma/orm-postgres/runtime';
import type { Contract } from './contract.d';
import contractJson from './contract.json' with { type: 'json' };
export const db = postgres<Contract>({
contractJson,
url: process.env['DATABASE_URL']!,
extensions: [pgvector],
});The './contract.d' in that import is not a typo: it is how you import contract.d.ts, one of the two files npx prisma contract emit writes.
Now that pgvector is in prisma.config.ts, npx prisma contract emit accepts pgvector.Vector(1536) in your contract. A vector column needs an explicit number of dimensions, so declare that dimension once as a named type in a types block, and then every column that uses the name gets the same dimension. The trailing ? on Embedding1536? makes the column optional, exactly as ? did in Prisma ORM 7. The types block is part of PSL syntax.
The pgvector in pgvector.Vector(1536) is the name the extension registers its types under, and its page in the extension directory lists them. PostGIS, for example, gives you postgis.Geometry(4326).
The first line of the file, // use prisma-8, is required. npx prisma contract emit reads only .prisma files whose first line is // use prisma-8 and skips any other file without a warning, so keep that line at the top when you add the types block.
// use prisma-8
types {
Embedding1536 = pgvector.Vector(1536)
}
model Post {
id String @id @default(uuid())
title String
embedding Embedding1536?
}Getting the new column from your contract into your database takes these commands, in this order:
npx prisma contract emit, so thatcontract.jsonandcontract.d.tspick up the new column. Run it again after every later change tocontract.prisma.npx prisma migration plan, which writes the extension's own migration intomigrations/, so commit it along with your other migrations. Skip this and the next command stops with an error whosecodeisMIGRATION.CONTRACT_SPACE_LAYOUT_VIOLATION.npx prisma db initon a new database, ornpx prisma db updateon one that already has tables, which Applying a migration covers. This runs the extension's migration and makes the database match your contract: it creates thePosttable, or adds theembeddingcolumn when thePosttable already exists.
The extension's migration is the CREATE EXTENSION IF NOT EXISTS vector statement, so you never write that statement yourself, but the PostgreSQL server has to have pgvector available for it to succeed. Both databases the quickstart offers have it: the local Prisma Postgres database that Composer starts, and a Prisma Postgres database from npx create-db@latest. A stock PostgreSQL image may not, and then the migration fails on that statement, so install pgvector on the server first, following pgvector's own instructions.
With the column in your database, write an embedding into it. A vector value is a plain number[] with as many entries as the column's dimension, so a column typed Embedding1536 takes an array of 1536 numbers, which is what an embedding model gives you. Prisma ORM does not produce the numbers, and embed in this example stands for whichever model you call:
import { db } from './db';
const embedding: number[] = await embed('about cats');
await db.orm.public.Post.create({ title: 'about cats', embedding });In Prisma ORM 8 create takes the fields directly, with no data wrapper. Create a record shows the two side by side. public is the PostgreSQL schema your tables are in.
Searching for the rows closest to a query vector uses the operations the extension adds, and you write that search with the SQL query builder rather than with db.orm. In that block:
db.ormis the ORM API, and under it you address a model by the name in your contract, so the modelPostisdb.orm.public.Post.db.sqlis the SQL query builder, and under it you address a table by the name it has in the database, so the same data isdb.sql.public.Post. The two names are the same because the model sets no@@map.db.runtime()is what runs a built query.f, in the callback of each builder method, holds the row's columns.fnsholds the functions you can call on those columns, including the ones pgvector adds.queryVectoris anumber[]of the same length as the stored embeddings.
import { db } from './db';
export async function similaritySearch(queryVector: number[]) {
const plan = db.sql.public.Post
.select('id', 'title')
.select('distance', (f, fns) => fns.cosineDistance(f.embedding, queryVector))
.orderBy((f, fns) => fns.cosineDistance(f.embedding, queryVector), { direction: 'asc' })
.limit(10)
.build();
return db.runtime().query(plan);
}.select('distance', ...) names the computed column distance, and .build() returns a plain object that db.runtime().query(...) runs. Each row that comes back has id, title, and distance, a number that is null when the row has no embedding.
Alongside fns.cosineDistance, pgvector gives you fns.cosineSimilarity. Both take two vectors and return a PostgreSQL float8, which you get as a number. The difference is which way you sort: order ascending when you sort by distance, because the closest row has the smallest distance, and descending when you sort by similarity.
One installed package supplies both registrations, the one in prisma.config.ts and the one in db.ts:
One extension package, two registrations, one databaseStep 1 of 4
One package brings everything the extension needs.
When your contract uses an extension and db.ts does not list it in extensions, postgres(...) throws an error whose code is RUNTIME.MISSING_EXTENSION_PACK before any query runs. Add the extension's runtime import to db.ts to fix it. If the database cannot install the extension, db init or db update reports it.
Every extension, whether Prisma wrote it or someone in the community did, is listed in the extension directory with its install command and the registration snippets to copy.
| Name | What it adds | Package | Databases | By |
|---|---|---|---|---|
| arktype-json | JSON columns validated by an arktype schema and typed end to end. | @prisma/orm-extension-arktype-json |
PostgreSQL | Prisma |
| MongoDB | MongoDB support for Prisma ORM: config, runtime, contract authoring, BSON values, and migrations in one package. | @prisma/orm-mongo |
MongoDB | Prisma |
| ParadeDB (experimental) | BM25 full-text search indexes. | @prisma/orm-extension-paradedb |
PostgreSQL | Prisma |
| pgvector | Vector columns and similarity search for embeddings. | @prisma/orm-extension-pgvector |
PostgreSQL | Prisma |
| PostGIS | Geometry columns and geospatial queries such as distance and containment. | @prisma/orm-extension-postgis |
PostgreSQL | Prisma |
| PostgreSQL | PostgreSQL support for Prisma ORM: config, runtime, contract authoring, and migrations in one package. | @prisma/orm-postgres |
PostgreSQL | Prisma |
| SQLite (experimental) | SQLite support for Prisma ORM: config, runtime, contract authoring, and migrations in one package. | @prisma/orm-sqlite |
SQLite | Prisma |
| Supabase (experimental) | Supabase auth and storage tables plus role-bound clients for row-level security. | @prisma/orm-extension-supabase |
PostgreSQL | Prisma |
| IndexedDB (experimental) | Prisma 8 for IndexedDB: a browser database from your PSL schema, with typed accessors and explicit migrations. | @prisma-idb/client-idb |
IndexedDB | Yash Kolekar |
| typed-json | Typed JSON and text columns with no validator dependency. | prisma-orm-extension-typed-json |
PostgreSQL | Omar Dulaimi |
| zod-json | Typed JSON columns described and enforced by a zod schema. | prisma-orm-extension-zod-json |
PostgreSQL | Omar Dulaimi |
The database packages are in the table too.
The ones marked experimental work today, but their methods and options are still changing between releases. Each name in the table links to that extension's directory page, and from there to the package README. Middleware wraps queries rather than adding a database feature, so it is listed on How middleware works instead.
If you would rather read a whole working project than a set of snippets, there is a runnable example for each of these extensions: pgvector, PostGIS, ParadeDB, and Supabase.
If the extension you need does not exist yet, you can build it. An extension is an npm package with a documented layout, and the call for extension authors explains how to write and publish one. Once yours is on npm, submit it to the directory, where the form validates your entry and opens the pull request for you.
- Advanced queries: the SQL query builder, where extension operations like
cosineDistanceappear - How middleware works for wrapping queries rather than adding database features
- Quickstart with PostgreSQL to set up a project to add extensions to
- Extensions overview for the full catalog, including middleware
- Prisma ORM overview for how your contract, your migrations, and your client fit together