Skip to content

About

Drizzle-kit on Deno: patched for native TS and JSR imports.

Resources

Stars

1 star

Watchers

0 watching

Forks

Repository files navigation

drizzle-kit-deno

JSR CI

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, and pull are supported. studio has not been tested and will probably not work.

ℹ️ Supported databases: PostgreSQL (via PGlite or remote), SQLite/LibSQL (via @libsql/client or node:sqlite).

Quick Start

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

Installation

# In your Deno project with drizzle-kit installed:
deno run --allow-read=./node_modules --allow-write=./node_modules jsr:@hotsauce/drizzle-kit-deno-patch

Or add to your deno.jsonc tasks:

{
  "tasks": {
    "patch": "deno run --allow-read=./node_modules --allow-write=./node_modules/.deno/drizzle-kit@0.31.10 jsr:@hotsauce/drizzle-kit-deno-patch"
  }
}

The Problem

Drizzle-kit is designed for Node.js and has several incompatibilities with Deno:

  1. Unnecessary transpilation overhead — drizzle-kit uses esbuild for TypeScript transpilation. Why download extra tooling when Deno can run TypeScript natively?

  2. 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.

  3. 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.

  4. CommonJS require() calls — These should be replaced with import() since we're using TypeScript and ES modules now.

  5. Eager OS system calls — os.homedir() and os.tmpdir() are called at load time, triggering permission prompts even when your command doesn't need them.

  6. LibSQL web client only — Deno uses the web version of @libsql/client by default, which is great for serverless platforms like Deno Deploy but doesn't support local file: URLs. This breaks drizzle-kit when using SQLite database files.

The Solution

A patch script that modifies drizzle-kit's bundled bin.cjs file to:

  1. Block path traversal — Disables walkForTsConfig and recursivelyResolveSync so drizzle-kit stays within your project directory.

  2. Skip unnecessary transpilation — Disables safeRegister (esbuild) since Deno handles TypeScript natively.

  3. Use native imports — Replaces require() with import() for loading config and schema files.

  4. Minimize environment access — Stubs color support functions to avoid eager env var checks at load time.

  5. Defer OS calls — Defers os.homedir() and os.tmpdir() calls to avoid permission prompts.

  6. Support local SQLite files — Allows switching to @libsql/client/node via the LIBSQL_JS_NODE env var for local file: URL support.

Required Permissions

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-write paths depend on your config:

  • ./drizzle is the default migrations output directory
  • ./data is for PGlite local databases; for remote databases, use --allow-net instead

Features

  • 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:sqlite instead of @libsql/client by setting SQLITE_NODE=1. No additional dependencies required.

  • LibSQL / SQLite support - Alternatively, use @libsql/client for SQLite databases; set LIBSQL_JS_NODE=1 for local file: URLs.

Using node:sqlite with drizzle-kit

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.

Configuration

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",
  },
});

Running drizzle-kit with node:sqlite

# 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

Why use node:sqlite?

  • No native dependencies - @libsql/client requires native FFI bindings, while node:sqlite is built into Deno 2.x and Node.js 22+
  • Simpler permissions - No --allow-ffi or --allow-sys required
  • Consistent runtime - Use the same SQLite driver for both your app (via drizzle-orm/sqlite-proxy) and drizzle-kit migrations

Project Structure

├── 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

Programmatic Usage

import {
  patchDrizzleKit,
  SUPPORTED_VERSIONS,
} from "jsr:@hotsauce/drizzle-kit-deno-patch";

console.log("Supported versions:", SUPPORTED_VERSIONS);
await patchDrizzleKit();

How It Works

Example Permission Sets

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_URL is exposed
  • No network access - For local PGlite databases; add --allow-net for remote databases

The Patch Script

The patch script:

  1. Finds the drizzle-kit binary in node_modules/
  2. Checks the version (tested with 0.30.6, 0.31.8, 0.31.9, 0.31.10)
  3. Applies patches using regex replacements
  4. Marks the file as patched to avoid re-patching
  5. 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); });

Running drizzle-kit

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

Supported drizzle-kit versions

  • 0.30.6
  • 0.31.8
  • 0.31.9
  • 0.31.10 (recommended)

The patch script will warn but attempt to patch other versions.

A note on tsx and esbuild in your lockfile

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:

  • tsx is bundled inside drizzle-kit's bin.cjs as a lazy CommonJS factory.
  • The single callsite that would invoke it (ensureTsxRegistered) checks typeof globalThis.Deno !== "undefined" and returns early under Deno, so the factory body — and the require("esbuild") inside it — never runs.
  • Our patch rewrites the require() calls that load your config/schema into native await 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.

Testing

A comprehensive test suite verifies the patch works across all supported drizzle-kit versions.

Testing Strategy

The test suite performs the following checks for each version:

  1. Setup - Creates an isolated test environment with a minimal Deno project
  2. Install - Installs the specific drizzle-kit version via deno install
  3. Patch - Applies the patch script and verifies it completes successfully
  4. Verify Marker - Checks that the patch marker is present in bin.cjs
  5. 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
  6. Runtime Tests (full mode only):
    • drizzle-kit --help - Verifies basic CLI functionality
    • drizzle-kit generate - Verifies config and schema loading works
    • drizzle-kit migrate - Applies migrations to a local PGlite DB, verifies expected table/columns exist, and checks migrations were recorded as applied
    • drizzle-kit push - Pushes schema directly to a separate PGlite DB, verifies expected table/columns exist
    • drizzle-kit pull - Introspects a PGlite DB (created via raw SQL) and verifies a schema file is generated with expected table definitions

Run all tests locally

deno task test

Test a specific version

deno task test 0.31.10

Quick test (patch only, no runtime tests)

deno task test:quick

Test options

Options:
  --quick, -q    Quick test (only verify patch applies, skip runtime tests)
  --keep, -k     Keep test directories after completion
  --help, -h     Show this help message

CI

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.

Troubleshooting

"Pattern not found" errors

The patch script uses regex patterns to find code to patch. If drizzle-kit's code changes significantly, patterns may not match. Check:

  1. Is the drizzle-kit version supported?
  2. Has the bundled code structure changed?

Permission denied errors

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 generate

Command hangs after completion

The "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.

Why This Approach?

Deno has excellent Node.js compatibility, but drizzle-kit's architecture poses challenges:

  1. Bundled code - drizzle-kit ships a single bundled bin.cjs file
  2. Load-time side effects - Many operations happen at import time, before any CLI logic runs
  3. 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.

About

Drizzle-kit on Deno: patched for native TS and JSR imports.

Resources

Stars

1 star

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages