@prisma/dev
A local Prisma Postgres server for development and testing.
It runs a real PostgreSQL database in your own Node process — no Docker, no separate service to install — and exposes it over both a plain TCP connection string and the prisma+postgres:// HTTP protocol that deployed Prisma Postgres uses.
Not for production. This is a development and testing tool.
There are two ways to use it:
@prisma/dev/vite — a Vite plugin that runs a database for the lifetime of your dev server.
@prisma/dev — start and stop a server from your own code.
Install
npm install --save-dev @prisma/dev
Vite plugin
Add the plugin and your app gets a database whenever vite dev runs.
import { prismaDev } from "@prisma/dev/vite";
import { defineConfig } from "vite";
export default defineConfig({
plugins: [prismaDev({ name: "my-app" })],
});
That sets two environment variables before any of your code runs:
DATABASE_URL | TCP connection string for the main database |
SHADOW_DATABASE_URL | TCP connection string for the shadow database, which prisma migrate dev requires |
Set name per project. It decides where the database files are persisted, so two projects left on the default name would share data.
Production builds are untouched. See When the plugin does nothing.
Applying migrations and seeding
A fresh database has no schema. You could migrate from application code, but then migration logic ships in your production artifact for the sake of local development. onDatabaseReady keeps it in the dev-only config instead.
import { execFile } from "node:child_process";
import { promisify } from "node:util";
import { prismaDev } from "@prisma/dev/vite";
import { defineConfig } from "vite";
const execFileAsync = promisify(execFile);
export default defineConfig({
plugins: [
prismaDev({
name: "my-app",
async onDatabaseReady({ env }) {
await execFileAsync("npx", ["prisma", "migrate", "deploy"], { env: { ...process.env, ...env } });
},
}),
],
});
The hook runs before any application or framework code executes, so nothing can observe an unprepared database. vite dev waits for it, so a slow migration delays startup.
One hook is registered, but it can do as much as you need. Sequence with plain await:
async onDatabaseReady(database) {
await migrate(database);
await seed(database);
}
It receives:
databaseUrl | TCP connection string for the main database |
shadowDatabaseUrl | TCP connection string for the shadow database |
prismaPostgresUrl | The prisma+postgres:// connection string. Carries an API key |
env | The variables this plugin manages, mapped to the local database |
name | The server name |
owned | false when the plugin attached to a database another process started |
Pass database.env when spawning a subprocess
Spread env explicitly, as the example above does. Do not rely on the ambient process.env.
By default the plugin does not overwrite a variable your environment already defines. So if your .env points DATABASE_URL at a deployed database, the plugin leaves it alone — and a migration that inherited process.env would run against that deployed database.
database.env always describes the local database, whether or not those values were written to process.env. Spreading it keeps the migration local under either setting of overrideExistingEnv, so it is the safe habit regardless of how you configure that.
The hook runs on every start
That includes restarts, and the case where the plugin attached to a database that was already running. Make it idempotent — prisma migrate deploy already is.
If the hook fails
The plugin shuts the database down and Vite fails to start. It will not serve your app against a half-prepared database.
Throwaway databases
By default the database is persisted under name and survives dev server restarts. Set persistenceMode: "stateless" to get a clean, in-memory database on every start instead.
prismaDev({
enableInTest: true,
name: "my-tests",
persistenceMode: "stateless",
});
The clearest use is integration tests, paired with enableInTest, where every run should start from a known state.
For interactive vite dev it is worth understanding what you give up:
- Editing
vite.config.ts or an .env file restarts the dev server, which discards the data. Anything you seeded or entered by hand is gone.
onDatabaseReady gets an empty database on every start, so it has to do the full migrate and seed each time.
- The database is invisible to
prisma dev ls, stop, and rm, and cannot be shared with a running prisma dev.
- Ports are chosen from whatever is free rather than the defaults below, unless you set them explicitly. That matters if you want to
psql in at a predictable address.
Sharing a database with prisma dev
If a prisma dev server is already running under the same name, the plugin uses it instead of starting its own, and leaves it running when Vite exits. database.owned is false in that case.
This applies to the default stateful mode only. A stateless plugin always starts its own throwaway database, so asking for one never hands you a persistent database by surprise.
So this works, with both terminals on one database:
npx prisma dev --name my-app
npx vite dev
Connection strings never reach the browser
The plugin writes to process.env and nothing else. It deliberately does not use Vite's define or import.meta.env.
Those inline values into browser assets. Both the TCP connection string and the prisma+postgres:// URL carry credentials, so inlining either would hand database access to every visitor of your app.
Server-side code, SSR loaders, and Prisma Client all read process.env, which is the surface that actually needs these values.
The database's lifetime
The database runs inside the Vite process, so it cannot outlive it. There is no background daemon and nothing to clean up by hand.
Quitting Vite normally, or with Ctrl-C, shuts the database down cleanly. A kill -9 takes the database down with the process; the next start recovers on its own, though it may pause briefly to do so.
Editing vite.config.ts or an .env file restarts the database, because the edit may have changed the plugin's own options. Your data is persisted, so it survives the restart — unless you opted into a throwaway database. Editing application code does not touch the database.
When the plugin does nothing
No database is started when:
- the command is
vite build
- the command is
vite preview
- the config was loaded by Vitest, which reads the same
vite.config.ts
Pass enableInTest: true to allow a database under Vitest.
Conflicting environment variables
By default, if a variable the plugin wants to write already holds a value, the existing value wins and the plugin logs a warning. An intentional .env entry or shell export is not silently replaced.
Pass overrideExistingEnv: true to let the local database win instead.
Using the prisma+postgres:// URL
Off by default, because it embeds an API key. Opt in by naming a variable:
prismaDev({
env: { prismaPostgresUrl: "PRISMA_DATABASE_URL" },
name: "my-app",
});
Options
name | "default" | Server name, which decides where data is persisted |
onDatabaseReady | — | Async hook to migrate and seed before Vite starts |
persistenceMode | "stateful" | "stateless" for a clean in-memory database on every start |
env.databaseUrl | "DATABASE_URL" | Variable for the main TCP connection string. false to skip |
env.shadowDatabaseUrl | "SHADOW_DATABASE_URL" | Variable for the shadow TCP connection string. false to skip |
env.prismaPostgresUrl | false | Variable for the prisma+postgres:// URL. Name it to opt in |
overrideExistingEnv | false | Overwrite variables that already hold a value |
enableInTest | false | Start a database when the config is loaded by Vitest |
port | 51213 | HTTP server port |
databasePort | 51214 | Main database port |
shadowDatabasePort | 51215 | Shadow database port |
streamsPort | 51216 | Colocated Prisma Streams server port |
debug | false | Log the server's debug output |
Every port falls back to another free port when the requested one is taken.
Programmatic server
For test harnesses, scripts, and integrations outside Vite:
import { startPrismaDevServer } from "@prisma/dev";
const server = await startPrismaDevServer({ name: "my-tests" });
console.log(server.database.prismaORMConnectionString);
console.log(server.ppg.url);
await server.close();
The resolved server exposes:
database | connectionString, prismaORMConnectionString, and a psql terminalCommand |
shadowDatabase | The same shape, for prisma migrate |
ppg.url | The prisma+postgres:// connection string, API key included |
http.url | Base URL of the HTTP server |
name | The server name |
close() | Shuts everything down |
Persistence
startPrismaDevServer() defaults to persistenceMode: "stateless", which discards all data when the server closes. That suits tests.
Pass persistenceMode: "stateful" to persist data under the given name, matching prisma dev and the Vite plugin's default.
const server = await startPrismaDevServer({ name: "my-app", persistenceMode: "stateful" });
Ports
Defaults are 51213 for HTTP, 51214 for the database, 51215 for the shadow database, and 51216 for the Prisma Streams server. Override any of them with port, databasePort, shadowDatabasePort, and streamsPort.
Starting a second server under a name that is already running rejects with ServerAlreadyRunningError.
Experimental
server.experimental exposes change-data-capture events, query insights, and the colocated Prisma Streams endpoint. These are experimental and may change without notice.
Requirements
The Vite plugin supports Vite 5, 6, 7, and 8. vite is an optional peer dependency, so installing @prisma/dev without Vite is fine — the plugin module references Vite for types only.
License
ISC