A patch to make drizzle-kit compatible with Deno and leverage its secure permission model.
⚠️ Important: This patch modifies drizzle-kit's bundled code. Only use in production if you understand exactly what the patch does. Review scripts/patch-drizzle-kit.ts before deploying.
ℹ️ Supported commands:
generate,migrate,push, andpullare supported.studiohas not been tested and will probably not work.
ℹ️ Supported databases: PostgreSQL (via PGlite or remote), SQLite/LibSQL (via
@libsql/clientornode:sqlite).
See the example/ folder for a complete working example.
# 1. Install dependencies
deno install
# 2. Patch drizzle-kit
deno run --allow-read=./node_modules --allow-write=./node_modules jsr:@hotsauce/drizzle-kit-deno-patch
# 3. Run drizzle-kit commands with minimal permissions
deno run \
--allow-env=DATABASE_URL \
--allow-read=.,./node_modules \
--allow-write=./drizzle \
./node_modules/drizzle-kit/bin.cjs generate
# 4. Success! Check ./drizzle for your generated migrations# In your Deno project with drizzle-kit installed:
deno run --allow-read=./node_modules --allow-write=./node_modules jsr:@hotsauce/drizzle-kit-deno-patchOr add to your deno.jsonc tasks:
Drizzle-kit is designed for Node.js and has several incompatibilities with Deno:
-
Unnecessary transpilation overhead — drizzle-kit uses esbuild for TypeScript transpilation. Why download extra tooling when Deno can run TypeScript natively?
-
Overly broad environment access — Several libraries eagerly check environment variables at load time (dotenv, chalk, etc.). Your drizzle-kit command likely only needs
DATABASE_URL, not access to your entire environment. -
Unconstrained path traversal — drizzle-kit walks parent directories looking for
tsconfig.json, potentially all the way to your home directory. This is unnecessary; blocking it tightens your security posture. -
CommonJS
require()calls — These should be replaced withimport()since we're using TypeScript and ES modules now. -
Eager OS system calls —
os.homedir()andos.tmpdir()are called at load time, triggering permission prompts even when your command doesn't need them. -
LibSQL web client only — Deno uses the web version of
@libsql/clientby default, which is great for serverless platforms like Deno Deploy but doesn't support localfile:URLs. This breaks drizzle-kit when using SQLite database files.
A patch script that modifies drizzle-kit's bundled bin.cjs file to:
-
Block path traversal — Disables
walkForTsConfigandrecursivelyResolveSyncso drizzle-kit stays within your project directory. -
Skip unnecessary transpilation — Disables
safeRegister(esbuild) since Deno handles TypeScript natively. -
Use native imports — Replaces
require()withimport()for loading config and schema files. -
Minimize environment access — Stubs color support functions to avoid eager env var checks at load time.
-
Defer OS calls — Defers
os.homedir()andos.tmpdir()calls to avoid permission prompts. -
Support local SQLite files — Allows switching to
@libsql/client/nodevia theLIBSQL_JS_NODEenv var for localfile:URL support.
Each drizzle-kit command requires specific permissions:
| Command | Permissions |
|---|---|
--help |
--allow-read=.,./node_modules |
generate |
--allow-env=DATABASE_URL --allow-read=.,./node_modules --allow-write=./drizzle |
migrate |
--allow-env=DATABASE_URL --allow-read=.,./node_modules --allow-write=./data,./drizzle |
push |
--allow-env=DATABASE_URL --allow-read=.,./node_modules --allow-write=./data,./drizzle |
pull |
--allow-env=DATABASE_URL --allow-read=.,./node_modules --allow-write=./data,./drizzle-pull |
Note: The
--allow-writepaths depend on your config:
./drizzleis the default migrations output directory./datais for PGlite local databases; for remote databases, use--allow-netinstead
-
JSR packages in schema - Your schema files can import from JSR (
@std/*, etc.) since Deno handles the imports natively -
Node SQLite support (recommended) - Use
node:sqliteinstead of@libsql/clientby settingSQLITE_NODE=1. No additional dependencies required. -
LibSQL / SQLite support - Alternatively, use
@libsql/clientfor SQLite databases; setLIBSQL_JS_NODE=1for localfile:URLs.
If you prefer using Deno/Node's built-in node:sqlite module instead of
@libsql/client, you can enable it with the SQLITE_NODE=1 environment
variable. This support is built directly into the patched drizzle-kit - no
additional packages needed.
Your drizzle.config.ts is straightforward:
import { defineConfig } from "drizzle-kit";
export default defineConfig({
schema: "./schema.ts",
out: "./drizzle",
dialect: "sqlite",
dbCredentials: {
url: "file:./data.db",
},
});# Generate migrations
SQLITE_NODE=1 deno run \
--allow-env --allow-read=.,./node_modules --allow-write=./drizzle \
./node_modules/drizzle-kit/bin.cjs generate
# Apply migrations
SQLITE_NODE=1 deno run \
--allow-env --allow-read=.,./node_modules --allow-write=./data.db,./data.db-wal,./drizzle \
./node_modules/drizzle-kit/bin.cjs migrate
# Push schema directly
SQLITE_NODE=1 deno run \
--allow-env --allow-read=.,./node_modules --allow-write=./data.db,./data.db-wal,./drizzle \
./node_modules/drizzle-kit/bin.cjs push- No native dependencies -
@libsql/clientrequires native FFI bindings, whilenode:sqliteis built into Deno 2.x and Node.js 22+ - Simpler permissions - No
--allow-ffior--allow-sysrequired - Consistent runtime - Use the same SQLite driver for both your app (via
drizzle-orm/sqlite-proxy) and drizzle-kit migrations
├── mod.ts # Library entry point
├── scripts/
│ ├── patch-drizzle-kit.ts # The patch script
│ └── test-patch.ts # Test suite
├── deno.jsonc # Library config
└── example/ # Example project
├── deno.jsonc # Example config with permission sets
├── drizzle.config.ts # Example drizzle config
└── schema.ts # Example schema
import {
patchDrizzleKit,
SUPPORTED_VERSIONS,
} from "jsr:@hotsauce/drizzle-kit-deno-patch";
console.log("Supported versions:", SUPPORTED_VERSIONS);
await patchDrizzleKit();The example/deno.jsonc defines permission sets for convenience:
{
"tasks": {
"db:generate": "deno run --allow-env=DATABASE_URL --allow-read=.,./node_modules --allow-write=./drizzle ./node_modules/drizzle-kit/bin.cjs generate",
"db:migrate": "deno run --allow-env=DATABASE_URL --allow-read=.,./node_modules --allow-write=./data,./drizzle ./node_modules/drizzle-kit/bin.cjs migrate",
"db:push": "deno run --allow-env=DATABASE_URL --allow-read=.,./node_modules --allow-write=./data,./drizzle ./node_modules/drizzle-kit/bin.cjs push",
"db:pull": "deno run --allow-env=DATABASE_URL --allow-read=.,./node_modules --allow-write=./data,./drizzle ./node_modules/drizzle-kit/bin.cjs pull"
}
}This provides fine-grained control over:
- File system access - Only reads project files, only writes to migrations/DB directories
- Environment variables - Only
DATABASE_URLis exposed - No network access - For local PGlite databases; add
--allow-netfor remote databases
The patch script:
- Finds the drizzle-kit binary in
node_modules/ - Checks the version (tested with 0.30.6, 0.31.8, 0.31.9, 0.31.10)
- Applies patches using regex replacements
- Marks the file as patched to avoid re-patching
- Reports which patches succeeded or failed
Key patches explained:
// Disable TypeScript config walking (permission issues)
function walkForTsConfig(directory, readdirSync) {
return void 0; // PATCHED: disabled for Deno
}
// Disable esbuild-based TypeScript registration
safeRegister = async () => {
return { unregister: () => {} }; // PATCHED: esbuild disabled for Deno
}
// Use import() instead of require() for TypeScript files
const required = await import(path4); // PATCHED: import for Deno TS support
// Force exit after CLI command completes
run([...]).then(() => { setTimeout(() => process.exit(0), 50); });Instead of running drizzle-kit generate, run the binary directly with Deno:
# Node.js way (doesn't work with Deno)
npx drizzle-kit generate
# Deno way (after patching) - with minimal permissions
deno run \
--allow-env=DATABASE_URL \
--allow-read=.,./node_modules \
--allow-write=./drizzle \
./node_modules/drizzle-kit/bin.cjs generate
# Or use a task defined in deno.jsonc
deno task db:generate- 0.30.6
- 0.31.8
- 0.31.9
- 0.31.10 (recommended)
The patch script will warn but attempt to patch other versions.
Starting in drizzle-kit@0.31.10, drizzle-kit declares tsx (and transitively
esbuild) as runtime dependencies in its package.json, replacing the older
esbuild-register chain. After deno install, you'll see many
@esbuild/<platform>@* entries appear in deno.lock and a tsx package on
disk.
This is expected and harmless under Deno. None of that code executes:
tsxis bundled inside drizzle-kit'sbin.cjsas a lazy CommonJS factory.- The single callsite that would invoke it (
ensureTsxRegistered) checkstypeof globalThis.Deno !== "undefined"and returns early under Deno, so the factory body — and therequire("esbuild")inside it — never runs. - Our patch rewrites the
require()calls that load your config/schema into nativeawait import(), so Deno handles TypeScript directly.
You can verify this empirically: the included tests run drizzle-kit without
granting --allow-ffi or --allow-run, both of which esbuild's native binary
requires. If esbuild were actually loading, the tests would fail with a
permissions error.
The lockfile bloat is purely metadata: tsx declares every
@esbuild/<platform> binary as an optionalDependency, so the lockfile —
which must be platform-agnostic — records all of them even though only the
binary for your current platform is materialized on disk.
If you want this fixed upstream, the right ask is for drizzle-kit to mark
tsx/esbuild as optionalDependencies so Deno/Bun users don't resolve them
at all.
A comprehensive test suite verifies the patch works across all supported drizzle-kit versions.
The test suite performs the following checks for each version:
- Setup - Creates an isolated test environment with a minimal Deno project
- Install - Installs the specific drizzle-kit version via
deno install - Patch - Applies the patch script and verifies it completes successfully
- Verify Marker - Checks that the patch marker is present in
bin.cjs - Verify All Patches - Confirms each individual patch was applied:
- Critical patches (must succeed):
walkForTsConfig,safeRegister,config loading,CLI exit handler - Optional patches (may vary by version): color stubs, dotenv stubs, homedir/tmpdir deferrals
- Critical patches (must succeed):
- Runtime Tests (full mode only):
drizzle-kit --help- Verifies basic CLI functionalitydrizzle-kit generate- Verifies config and schema loading worksdrizzle-kit migrate- Applies migrations to a local PGlite DB, verifies expected table/columns exist, and checks migrations were recorded as applieddrizzle-kit push- Pushes schema directly to a separate PGlite DB, verifies expected table/columns existdrizzle-kit pull- Introspects a PGlite DB (created via raw SQL) and verifies a schema file is generated with expected table definitions
deno task testdeno task test 0.31.10deno task test:quickOptions:
--quick, -q Quick test (only verify patch applies, skip runtime tests)
--keep, -k Keep test directories after completion
--help, -h Show this help message
Tests run automatically via GitHub Actions:
- On push to
main- Full tests for all supported versions (when patch files change) - On pull requests - Full tests in parallel matrix + quick smoke test
- Manual trigger - Can be run manually via workflow dispatch
Each version is tested in parallel using a matrix strategy for faster feedback.
See .github/workflows/test-patch.yml for the CI configuration.
The patch script uses regex patterns to find code to patch. If drizzle-kit's code changes significantly, patterns may not match. Check:
- Is the drizzle-kit version supported?
- Has the bundled code structure changed?
Make sure your permission set includes all required paths and env vars. Use
DENO_TRACE_PERMISSIONS=1 to see which permissions are being requested:
DENO_TRACE_PERMISSIONS=1 deno run --permission-set=drizzle-kit ./node_modules/drizzle-kit/bin.cjs generateThe "CLI exit handler" patch adds process.exit(0) after commands complete. If
this patch fails, drizzle-kit may hang. Check the patch results for failures.
Deno has excellent Node.js compatibility, but drizzle-kit's architecture poses challenges:
- Bundled code - drizzle-kit ships a single bundled
bin.cjsfile - Load-time side effects - Many operations happen at import time, before any CLI logic runs
- CommonJS assumptions - Uses
require()for dynamic imports of user config files
Patching the binary directly is the most reliable solution until drizzle-kit adds official Deno support.
{ "tasks": { "patch": "deno run --allow-read=./node_modules --allow-write=./node_modules/.deno/drizzle-kit@0.31.10 jsr:@hotsauce/drizzle-kit-deno-patch" } }