
Research
/Security News
OpenAPI React Query Codegen Compromised in Mini Shai-Hulud npm Supply Chain Attack
Ten malicious OpenAPI React Query Codegen versions were published to npm in the Mini Shai-Hulud attack, all with valid provenance.
@prisma/dev
Advanced tools
@prisma/devA 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.npm install --save-dev @prisma/dev
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:
| Variable | Value |
|---|---|
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.
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.
The plugin does this work in Vite's configureServer hook. That is the earliest hook which only runs for a dev server that is actually going to serve. Anything earlier also runs for config resolutions that serve nothing — a bare resolveConfig call, or the child compiler a framework builds from your config file — and starting a database for those is wrong. See Frameworks that build a child compiler.
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:
| Property | Description |
|---|---|
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 |
database.env when spawning a subprocessSpread env explicitly, as the example above does.
It names the local database directly, so a migration cannot pick up a variable this plugin does not manage. That matters if you disabled one with env: { databaseUrl: false }: the ambient process.env still holds your own value, while database.env holds the local one.
Not once per dev server. Two dev servers sharing one database in one process — see Frameworks that build a child compiler — run it once. A config restart that does not change the plugin's own options keeps the running database, so it does not re-run the hook either.
It does run when the plugin attached to a database that prisma dev was already running. Make it idempotent — prisma migrate deploy already is.
The plugin shuts the database down and Vite fails to start. It will not serve your app against a half-prepared database.
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({
name: "my-app",
test: { name: "my-tests", persistenceMode: "stateless" },
});
The clearest use is integration tests, where every run should start from a known state. See Databases for Vitest runs.
For interactive vite dev it is worth understanding what you give up:
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.prisma dev ls, stop, and rm, and cannot be shared with a running prisma dev.psql in at a predictable address.prisma devIf 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:
# terminal 1
npx prisma dev --name my-app
# terminal 2 — vite.config.ts uses prismaDev({ name: "my-app" })
npx vite dev
Some frameworks compile part of your app with a second, internal Vite server built from the same config file. React Router does this, and so the plugin has to cope with being instantiated twice in one process.
The plugin does. Two acquisitions of one name converge on one database: the second joins the first rather than restarting it, the database stays up until the last of them lets go, and onDatabaseReady runs once. Nothing needs configuring for this.
Two consequences are worth knowing:
DATABASE_URL to be set.react-router typegen and react-router build load vite.config.ts without serving anything, so no database starts and no migration runs. A typecheck is a typecheck.process.env reaches the Vite process and its children. It does not reach a psql session, a migration you run by hand, or anything else started from a second terminal.
Look the server up by name instead:
import { getPrismaDevServerConnection } from "@prisma/dev";
const connection = await getPrismaDevServerConnection({ name: "my-app" });
console.log(connection?.databaseUrl); // postgres://...
It resolves to null when no server of that name is running. Only stateful servers are visible — a stateless one writes no state and can only be addressed through the process that started it.
This is why hardcoding a connection string into an .env file is not necessary, and why pinning ports is not either. See Ports and stable addresses.
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 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 Vite dev server. The database is only restarted along with it if the edit changed this plugin's own options, in which case the new options have to win. Otherwise the running database is carried over untouched. Either way your data is persisted, unless you opted into a throwaway database. Editing application code does not touch the database.
No database is started when:
vite buildvite previewvite.config.tsSee Databases for Vitest runs to allow one under Vitest.
Vitest reads the same vite.config.ts, so by default the plugin starts nothing there. A plain vitest run should not boot a database as a config side effect.
Opt in with the test option:
prismaDev({
name: "my-app",
test: { name: "my-app-test", persistenceMode: "stateless" },
});
Each entry under test replaces the corresponding option above it. Everything you do not name is inherited. Here the test run gets its own throwaway database and inherits the env mapping, while vite dev keeps its persistent my-app one.
The port options are inherited too, but this database is stateless, so it takes whatever ports are free rather than the ones it inherited — see Throwaway databases. Set the ports under test if a run needs a predictable address. Note that pinning them collides with a vite dev on the same ports if both run at once.
test: {} opts in with the surrounding options unchanged, so vite dev and vitest run share one database.
Setting test has no effect outside Vitest, so there is one config file and no branching in it.
The plugin owns every variable it writes. If your environment already points one somewhere else, the local database wins and the plugin logs a warning naming what it displaced.
This is deliberate. Many frameworks load .env into process.env before the plugin runs, so deferring to the existing value would leave a project whose .env already names DATABASE_URL with a running database that nothing connects to.
To keep your own value, disable that variable:
prismaDev({
env: { databaseUrl: false },
name: "my-app",
});
The plugin then never writes DATABASE_URL, and reports no conflict. database.env in onDatabaseReady still carries the local connection string.
A variable that already holds exactly the value the plugin was going to write is not warned about. There is no conflict to report.
prisma+postgres:// URLOff by default, because it embeds an API key. Opt in by naming a variable:
prismaDev({
env: { prismaPostgresUrl: "PRISMA_DATABASE_URL" },
name: "my-app",
});
| Option | Default | Description |
|---|---|---|
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 |
test | false | Options for Vitest runs. An object opts in, false stays off |
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 |
A named stateful server records the ports it settled on and reuses them the next time it starts. So name alone is enough for a stable address.
To find out which ports a server actually landed on, look it up by name. That is also the recommended way to reach the database from outside the Vite process.
Passing a port explicitly changes the failure mode, deliberately:
A stateless database records nothing, so it takes any free port on every start unless you name one.
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); // postgres://...
console.log(server.ppg.url); // prisma+postgres://...
await server.close();
The resolved server exposes:
| Property | Description |
|---|---|
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 |
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" });
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.
A default that is taken falls back to another free port. A port you pass explicitly is bound or the call rejects — with PortNotAvailableError when nothing can bind it, and PortBelongsToAnotherServerError when another Prisma Dev server already holds it.
Starting a second server under a name that is already running rejects with ServerAlreadyRunningError.
import { getPrismaDevServerConnection } from "@prisma/dev";
const connection = await getPrismaDevServerConnection({ name: "my-app" });
Resolves the databaseUrl, shadowDatabaseUrl, and prismaPostgresUrl of a running stateful server by name, or null when none is running. Use it from a process that did not start the database and therefore cannot read its process.env.
server.experimental exposes change-data-capture events, query insights, and the colocated Prisma Streams endpoint. These are experimental and may change without notice.
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.
ISC
FAQs
A local Prisma Postgres server for development and testing
The npm package @prisma/dev receives a total of 5,617,719 weekly downloads. As such, @prisma/dev popularity was classified as popular.
We found that @prisma/dev demonstrated a healthy version release cadence and project activity because the last version was released less than a year ago. It has 4 open source maintainers collaborating on the project.

Research
/Security News
Ten malicious OpenAPI React Query Codegen versions were published to npm in the Mini Shai-Hulud attack, all with valid provenance.

Security News
Socket joins more than 100 technology, cybersecurity, and financial organizations calling for a global surge in cyber defense.

Product
Enterprise security teams can now detect malware, credential theft, suspicious network activity, and risky updates across Microsoft Edge extensions.