Skip to content

Instantly share code, notes, and snippets.

@alexanderson1993
Last active December 19, 2025 00:36
Show Gist options
  • Select an option

  • Save alexanderson1993/0852a8162ebac591b62a79883a81e1a8 to your computer and use it in GitHub Desktop.

Select an option

Save alexanderson1993/0852a8162ebac591b62a79883a81e1a8 to your computer and use it in GitHub Desktop.
Prisma D1 Migration CLI
migrate.mov

A handy CLI for working with the new Cloudflare D1/Prisma integration. You can read about that here: https://blog.cloudflare.com/prisma-orm-and-d1

Getting Started

  • Install wrangler, Prisma, and the other dependencies
npm install prisma@latest @prisma/client@latest @prisma/adapter-d1
npm install --save-dev wrangler toml tiny-parse-argv @clack/prompts
  • Create your D1 Database

npx wrangler d1 create prod-prisma-d1-app

  • Create a wrangler.toml file
// wrangler.toml
name="my-d1-prisma-app"
main = "src/index.ts"
compatibility_date = "2024-03-20"
compatibility_flags = ["nodejs_compat"]

[[d1_databases]]
binding = "DB" # i.e. available in your Worker on env.DB
database_name = "prod-prisma-d1-app"
database_id = "<unique-ID-for-your-database>"
  • Initialize Prisma
npx prisma init --datasource-provider sqlite
  • Turn on the adapters feature of Prisma
// schema.prisma
// This is your Prisma schema file,
// learn more about it in the docs: https://pris.ly/d/prisma-schema

generator client {
  provider = "prisma-client-js"
+ previewFeatures = ["driverAdapters"]
}

datasource db {
  provider = "sqlite"
  url      = env("DATABASE_URL")
}

Usage

npx tsx prisma/migrate.ts

┌  D1 Prisma Migrate CLI
│
│  migrate <command>
│    Commands:
│      create - Create a new migration
│      apply - Apply pending migrations
│    Options:
│      -h, --help - Show this help message

Each command includes help with the --help option.

import fs from "node:fs/promises";
import path from "node:path";
import { exec } from "node:child_process";
import {
intro,
outro,
log,
select,
text,
spinner,
isCancel,
confirm,
} from "@clack/prompts";
import toml from "toml";
import parseArgv from "tiny-parse-argv";
const args = parseArgv(process.argv.slice(2));
const command = args._[0];
const projectRoot = path.resolve();
const asyncExec = (command: string) =>
new Promise<string>((resolve, reject) => {
exec(command, (error, stdout, stderr) => {
if (error) {
reject(stderr);
} else {
resolve(stdout);
}
});
});
intro("D1 Prisma Migrate CLI");
if (args.help || !command) {
switch (command) {
case "create":
log.message(`migrate create
Create a new migration
Options:
-n, --name - The name of the migration
-d, --database - The name of the D1 database
--create-only - Only create the migration file, do not apply it
--schema - Custom path to the Prisma schema
-h, --help - Show this help message`);
break;
case "apply":
log.message(`migrate apply
Apply pending migrations
Options:
-d, --database - The name of the D1 database
--remote - Apply migrations to your remote database
--schema - Custom path to the Prisma schema
-h, --help - Show this help message`);
break;
default:
log.message(`migrate <command>
Commands:
create - Create a new migration
apply - Apply pending migrations
Options:
-h, --help - Show this help message`);
break;
}
process.exit(0);
}
// biome-ignore lint/suspicious/noExplicitAny: <explanation>
let wranglerConfig: any;
// Check wrangler.toml to see what D1 namespaces are used
try {
const wranglerToml = await fs.readFile("wrangler.toml", "utf-8");
wranglerConfig = toml.parse(wranglerToml);
} catch {
log.error("Could not read wrangler.toml");
process.exit(1);
}
const databases: { value: string; label: string }[] =
wranglerConfig.d1_databases?.map((db: { database_name: string }) => ({
value: db.database_name,
label: db.database_name,
})) || [];
let database = args.d || args.database || databases[0]?.value;
if (command === "create") {
const database = await getDatabase();
const migrationName =
args.name ||
args.n ||
(await text({
message: "What is the name of the migration?",
validate: (input) => {
if (input.length === 0) {
return "Migration name cannot be empty";
}
},
}));
if (isCancel(migrationName)) {
process.exit(1);
}
const s = spinner();
s.start("Creating migration");
const result = await asyncExec(
`npx wrangler d1 migrations create ${database} ${migrationName}`
);
s.stop("Creating migration");
const migrationPath = result
.trim()
.split("\n")
.find((line) => line.endsWith(".sql"));
if (!migrationPath) {
log.error("Could not find migration path");
process.exit(1);
}
s.start("Generating migration diff from Prisma schema");
await asyncExec(
`npx prisma migrate diff --script --from-local-d1 --to-schema-datamodel ${
args.schema || "./prisma/schema.prisma"
} >> ${migrationPath}`
);
s.stop("Generating migration diff from Prisma schema");
if (args["create-only"]) {
outro(`Migration created at ${migrationPath.replace(projectRoot, ".")}`);
process.exit();
}
}
if (command === "apply" || command === "create") {
const database = await getDatabase();
const s = spinner();
s.start("Applying migrations");
await asyncExec(
`npx wrangler d1 migrations apply ${database} ${
args.remote ? "--remote" : "--local"
}`
);
s.stop("Applying migrations");
s.start("Generating Prisma client");
await asyncExec(
`npx prisma generate ${args.schema ? `--schema ${args.schema}` : ""}`
);
s.stop("Generating Prisma client");
outro("Migrations applied");
}
async function getDatabase() {
if (databases.length === 0) {
log.error("No D1 databases found in wrangler.toml");
process.exit(1);
}
database =
database ||
(await select({
message: "Select a database",
options: databases,
initialValue: databases[0].value,
}));
if (isCancel(database)) {
process.exit(1);
}
return database;
}
@aimproxy
Copy link

aimproxy commented Dec 19, 2025

Thanks for sharing this! Really helpful approach for integrating Prisma with D1 migrations.

I ended up building a CLI tool that automates this workflow with a different approach. Here's what it does:

The Process

  1. Reads your wrangler.jsonc to find configured D1 databases
  2. Locates the local D1 SQLite file in .wrangler/state/v3/d1/miniflare-D1DatabaseObject/ (which uses hash-based filenames)
  3. Creates an empty wrangler migration via wrangler d1 migrations create <database> <name>
  4. Generates SQL diff using prisma migrate diff:
    • Sets DATABASE_URL env var to point to the local D1 SQLite file
    • Compares current database state (--from-config-datasource) against Prisma schema (--to-schema)
    • Outputs SQL script that gets appended directly to the wrangler migration file

Key differences from your approach

  • No intermediate Prisma migration files - works directly with wrangler's migration system
  • Uses prisma migrate diff instead of prisma migrate dev --create-only
  • Automatically finds and points to the local D1 database for accurate diffing
  • Single command creates and populates the migration in one go

The trick was creating a prisma.config.ts that reads DATABASE_URL from environment variables, then injecting that path at runtime:

DATABASE_URL="file:/path/to/local.sqlite" npx prisma migrate diff ...

package.json

{
  "scripts": {
    "prisma-d1-cli": "node -r esbuild-register ./prisma-d1-cli.ts"
  },
  "devDependencies": {
    "@clack/prompts": "^0.11.0",
    "esbuild-register": "^3.6.0",
    "jsonc-parse": "^2.0.0",
    "prisma": "^7.1.0"
  },
  "dependencies": {
     "@prisma/adapter-d1": "^7.2.0",
     "@prisma/client": "^7.1.0",
   }
}

prisma.config.ts

import 'dotenv/config'
import { defineConfig, env } from "prisma/config";

/**
 * Prisma setup for the project.
 */
export default defineConfig({
	schema: 'prisma/schema.prisma',
	datasource: { url: env('DATABASE_URL') },
});

prisma-d1-cli.ts

import fs from "node:fs/promises";
import path from "node:path";
import { exec } from "node:child_process";
import {
	intro,
	outro,
	log,
	select,
	text,
	spinner,
	isCancel,
} from "@clack/prompts";
import parseArgv from "tiny-parse-argv";
import { parse } from "jsonc-parse";

(async () => {
	const args = parseArgv(process.argv.slice(2));

	const projectRoot = path.resolve();

	const asyncExec = (command: string) =>
		new Promise<string>((resolve, reject) => {
			exec(command, (error, stdout, stderr) => {
				if (error) {
					reject(stderr);
				} else {
					resolve(stdout);
				}
			});
		});

	intro("D1 Prisma Migrate CLI");

	if (args.help) {
		log.message(`migrate
  Create a new migration from Prisma schema changes`);
		process.exit(0);
	}

	// biome-ignore lint/suspicious/noExplicitAny: <explanation>
	let wranglerConfig: any;

	// Check wrangler.toml to see what D1 namespaces are used
	try {
		const wranglerJsonc = await fs.readFile("wrangler.jsonc", "utf-8");
		wranglerConfig = parse(wranglerJsonc)
		//   wranglerConfig = toml.parse(wranglerToml);
	} catch {
		log.error("Could not read wrangler.toml");
		process.exit(1);
	}

	const databases: { value: string; label: string; id: string }[] =
		wranglerConfig.d1_databases?.map((db: { database_name: string; database_id: string }) => ({
			value: db.database_name,
			label: db.database_name,
			id: db.database_id,
		})) || [];

	if (databases.length === 0) {
		log.error("No D1 databases found in wrangler.jsonc");
		process.exit(1);
	}

	const database = databases[0].value;
	const databaseId = databases[0].id;

	// Find the actual SQLite file in the D1 directory
	const d1Dir = '.wrangler/state/v3/d1/miniflare-D1DatabaseObject';
	let localDbPath: string;

	try {
		const files = await fs.readdir(d1Dir);
		const sqliteFile = files.find(f => f.endsWith('.sqlite'));
		if (!sqliteFile) {
			log.error("Could not find D1 SQLite database file");
			process.exit(1);
		}
		localDbPath = path.join(d1Dir, sqliteFile);
	} catch (error) {
		log.error(`Could not read D1 directory: ${error}`);
		process.exit(1);
	}

	const migrationName = await text({
		message: "What is the name of the migration?",
		validate: (input) => {
			if (input.length === 0) {
				return "Migration name cannot be empty";
			}
		},
	});

	if (isCancel(migrationName)) {
		process.exit(1);
	}

	const s = spinner();
	s.start("Creating migration");

	try {
		const result = await asyncExec(
			`npx wrangler d1 migrations create ${database} ${migrationName}`
		);

		s.stop("Creating migration");

		const migrationPath = result
			.trim()
			.split("\n")
			.find((line) => line.endsWith(".sql"));

		if (!migrationPath) {
			log.error("Could not find migration path");
			process.exit(1);
		}

		s.start("Generating migration diff from Prisma schema");

		const absoluteDbPath = path.resolve(localDbPath);

		await asyncExec(
			`DATABASE_URL="file:${absoluteDbPath}" npx prisma migrate diff --from-config-datasource --to-schema ./prisma/schema.prisma --script >> ${migrationPath}`
		);

		s.stop("Generating migration diff from Prisma schema");

		outro(`Migration created at ${migrationPath.replace(projectRoot, ".")}`);
	} catch (error) {
		s.stop("Creating migration");
		log.error(`Error: ${error}`);
		process.exit(1);
	}
})();

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment