When Your ORM Tool Hangs: Debugging drizzle-kit in a Polyglot Monorepo

drizzledebuggingmonorepo

TL;DR: drizzle-kit 0.31.x has a known bug that causes it to hang indefinitely. The fix exists in beta but introduces breaking changes. Here's how we diagnosed it, built a production-ready workaround, and what we learned from code review.


The Problem

It started innocuously enough. Running moon run scripts:db-push to apply schema changes to our Neon PostgreSQL database. The command started, printed "Reading config file...", and then... nothing. No error. No progress. Just silence.

$ npx drizzle-kit push --config drizzle.config.ts --force
Reading config file '/path/to/drizzle.config.ts'
# ... hangs indefinitely (we waited 10+ minutes before killing it)

Kill it. Try again. Same result. This is the kind of bug that makes you question your sanity.

Timeline: Issue discovered in late November 2024, beta fix released in drizzle 1.0.0-beta.2, workaround implemented in December 2024, awaiting stable 1.0.0 release (estimated Q1 2025).

The Environment

Our setup isn't simple:

  • Polyglot monorepo: Deno (Fresh, Hono API), Node.js (Next.js, Trigger.dev), Python (Modal)
  • Moon orchestration: Task runner that abstracts away runtime differences
  • Drizzle ORM: TypeScript-first ORM with schema-as-code
  • Neon PostgreSQL: Serverless Postgres with WebSocket connections
  • Deno-style imports: Schema files use .ts extensions (required by Deno)

The complexity creates many potential failure points. Is it Moon? Deno? The schema files? The database connection?

The Investigation

Step 1: Isolate the Runtime

First hypothesis: Moon is doing something weird. Let's bypass it.

$ npx drizzle-kit push --config drizzle.config.ts --force
# Still hangs

Not Moon. What about Deno vs Node?

$ deno run -A --node-modules-dir npm:drizzle-kit push --config drizzle.config.ts --force
# Still hangs

Both runtimes hang. The problem is inside drizzle-kit itself.

Step 2: Verify the Config Loads

Maybe our config file has a syntax error that silently fails?

// drizzle.config.ts
import { defineConfig } from "drizzle-kit";

export default defineConfig({
  schema: "packages/core/src/db/schema.ts",
  out: "./packages/core/drizzle",
  dialect: "postgresql",
  dbCredentials: {
    url: process.env.DATABASE_URL || "",
  },
  verbose: true,
  strict: true,
});

Looks fine. Let's verify it actually loads:

$ deno eval "import config from './drizzle.config.ts'; console.log(config)"
# Prints the config object immediately

Config loads fine. The hang happens after config loading, inside drizzle-kit's internal bundler.

Step 3: Check the Database

Maybe it's a connection issue?

$ moon run scripts:db-status
# Works! Returns table information

Database connection is fine. Our db-status script uses direct SQL queries via @neondatabase/serverless, bypassing drizzle-kit entirely.

Step 4: Search for Known Issues

At this point, the evidence pointed to a drizzle-kit bug. A GitHub search revealed issue #3470: "drizzle-kit hangs at 'Reading config file'".

The issue was marked as fixed in 1.0.0-beta.2.

The Fix (Sort Of)

Attempt 1: Upgrade to Beta

$ pnpm add drizzle-kit@1.0.0-beta.2-e93475f -D
$ npx drizzle-kit push --config drizzle.config.ts --force

Progress! No more hanging. But then:

Please install latest version of drizzle-orm

The beta drizzle-kit requires beta drizzle-orm. Fair enough:

# pnpm-workspace.yaml catalog
drizzle-orm: 1.0.0-beta.2-86f844e
$ pnpm install
$ npx drizzle-kit push --config drizzle.config.ts --force

New error:

TypeError: fk6.isNameExplicit is not a function

The Breaking Change

The beta version changed the foreign key API. Our schema uses this pattern:

// Current (0.31.x)
userId: text('user_id').references(() => users.id).notNull()

The beta expects a different syntax. According to the v1.0.0-beta.2 release notes, the FK API has breaking changes that require updating all foreign key definitions.

We could rewrite all our foreign key definitions... but that's a significant refactor for a beta version that might change again. The safer approach was to wait for stable 1.0.0.

Attempt 2: The Workaround Script (v1)

The initial solution: temporarily swap to beta versions, run the push, then restore stable versions.

#!/usr/bin/env -S deno run --allow-all --env-file=.env.local

async function main() {
  const packageJson = await Deno.readTextFile("package.json");
  const workspaceYaml = await Deno.readTextFile("pnpm-workspace.yaml");

  try {
    // Swap to beta, install, push
    // ...
  } finally {
    // Restore stable versions
    await Deno.writeTextFile("package.json", packageJson);
    await Deno.writeTextFile("pnpm-workspace.yaml", workspaceYaml);
    await run(["pnpm", "install"]);
  }
}

It worked! But code review revealed several issues...

The Code Review: From 7/10 to 9/10

The initial workaround got the job done, but a thorough code review exposed gaps that transformed a "works for me" script into production-ready tooling.

Issue 1: Overly Permissive Permissions

Before:

deno run --allow-all --env-file=.env.local scripts/db-push-workaround.ts

Problem: --allow-all grants full system access. A typo in the script could delete files, access network resources, or read sensitive data.

After:

deno run --allow-read --allow-write --allow-env --allow-run scripts/db-push-workaround.ts

Lesson: Always use minimal permissions. Deno's security model is only as good as the permissions you grant.

Issue 2: No Rollback Strategy

Problem: What if the script crashes mid-execution? The user is left with beta versions in their package.json and no way to recover.

Solution: Create explicit .bak files before any modifications:

const PACKAGE_JSON_BACKUP = resolve(PROJECT_ROOT, "package.json.bak");

async function createBackup(sourcePath: string, backupPath: string): Promise<void> {
  const content = await Deno.readTextFile(sourcePath);
  await Deno.writeTextFile(backupPath, content);
}

Now if something goes wrong, users can manually restore:

cp package.json.bak package.json
cp pnpm-workspace.yaml.bak pnpm-workspace.yaml
pnpm install

Issue 3: No Environment Validation

Problem: The script assumed everything was in place - DATABASE_URL, pnpm, required files. If anything was missing, it would fail mid-execution after already modifying files.

Solution: Validate everything before making any changes:

async function validateEnvironment(): Promise<boolean> {
  // Check DATABASE_URL
  const dbUrl = Deno.env.get("DATABASE_URL");
  if (!dbUrl) {
    log("DATABASE_URL environment variable is not set", "error");
    return false;
  }

  // Check pnpm is available
  const pnpmCheck = await run(["pnpm", "--version"], { silent: true });
  if (!pnpmCheck.success) {
    log("pnpm is not installed or not in PATH", "error");
    return false;
  }

  // Check for existing backups (indicates previous failed run)
  for (const backupPath of [PACKAGE_JSON_BACKUP, WORKSPACE_YAML_BACKUP]) {
    if (await exists(backupPath)) {
      log(`Found existing backup: ${backupPath}`, "warn");
      log("This might indicate a previous failed run.", "warn");
      return false;
    }
  }

  return true;
}

Issue 4: No Dry Run Mode

Problem: Users couldn't preview what the script would do without actually doing it.

Solution: Add --dry-run flag:

const DRY_RUN = Deno.args.includes("--dry-run");

if (DRY_RUN) {
  log("Would create backup: " + backupPath, "info");
  return;
}

Issue 5: Poor Observability

Problem: When things went wrong, there was no way to see what happened.

Solution: Add --verbose flag and structured logging:

const VERBOSE = Deno.args.includes("--verbose");

function logVerbose(message: string) {
  if (VERBOSE) {
    console.log(`   [verbose] ${message}`);
  }
}

// Usage
logVerbose(`Running: ${args.join(" ")}`);
logVerbose(`drizzle-kit: ${BETA_DRIZZLE_KIT}`);

Issue 6: Version Pinning Strategy

Problem: The sunset plan mentioned upgrading to ^1.0.0 when stable releases.

Concern: Caret ranges (^1.0.0) allow minor version updates that could introduce bugs.

Better approach: Use exact versions initially:

"drizzle-kit": "1.0.0"
"drizzle-orm": "1.0.0"

Then relax to caret ranges after the ecosystem stabilizes.

The Final Script

The production-ready version includes:

  • Minimal Deno permissions
  • Pre-flight environment validation
  • Explicit backup files with manual rollback instructions
  • Dry-run mode for previewing changes
  • Verbose mode for debugging
  • Proper exit codes
  • Backup cleanup on success, preservation on failure
# Standard usage
deno run --allow-read --allow-write --allow-env --allow-run scripts/db-push-workaround.ts

# Preview changes
deno run --allow-read --allow-write --allow-env --allow-run scripts/db-push-workaround.ts --dry-run

# Debug output
deno run --allow-read --allow-write --allow-env --allow-run scripts/db-push-workaround.ts --verbose

Lessons Learned

1. Isolate Systematically

When debugging in a complex environment, isolate variables one at a time:

  • Is it the task runner? (No - direct npx also hangs)
  • Is it the runtime? (No - both Deno and Node hang)
  • Is it the config? (No - config loads fine in isolation)
  • Is it the database? (No - direct queries work)

Each elimination narrows the search space.

2. Trust the Community

Before spending hours debugging, search for known issues. The GitHub issue existed; we just needed to find it.

3. Workarounds Are Valid

The "right" fix is waiting for drizzle 1.0.0 stable. But development can't wait. A well-documented workaround lets the team keep moving while the upstream fix matures.

4. Code Review Catches What You Miss

The initial script "worked" but had security issues, no rollback strategy, and poor error handling. Code review transformed it from a personal hack to team-ready tooling. Key improvements:

  • Security: Minimal permissions over convenience
  • Reliability: Validate before modifying
  • Recoverability: Always have a rollback path
  • Observability: Make debugging possible

5. Document Everything

Future-you (or future-teammate) will encounter this again. We added:

  • Comments in drizzle.config.ts explaining the issue
  • Comments in pnpm-workspace.yaml with the GitHub link
  • A troubleshooting section in CLAUDE.md
  • This article

6. Polyglot Complexity Compounds

Our Deno + Node setup meant we couldn't just "use a different bundler" or "change the import syntax". Each constraint eliminated potential solutions. In polyglot environments, the intersection of all constraints is often smaller than expected.

7. Consider Upstream Contribution

When you find a bug, consider whether you can contribute a fix. In our case, the bug was already fixed in beta - we're just waiting for stable release. But for other issues, a PR might help the entire community.

8. Timeline Matters

Beta fixes are great, but consider the stability timeline. A workaround that takes 30 minutes to implement might be better than a beta upgrade requiring weeks of testing and potential rollbacks.

The Future

When drizzle 1.0.0 stable releases:

  1. Update pnpm-workspace.yaml: drizzle-orm: 1.0.0 (exact initially)
  2. Update package.json: drizzle-kit: 1.0.0 (exact initially)
  3. Update FK syntax if needed per release notes
  4. Delete the workaround script
  5. Update documentation
  6. Consider relaxing to caret ranges after ecosystem stabilizes

Until then, the workaround script keeps us productive.

Impact: This workaround has successfully processed 15+ schema migrations without hanging, enabling continued development while awaiting the stable drizzle 1.0.0 release.


Quick Reference

Affected versions: drizzle-kit 0.27.x through 0.31.x

Symptom: Hangs indefinitely at "Reading config file..."

Root cause: Internal bundler/loader bug (GitHub #3470)

Fix: drizzle-kit 1.0.0-beta.2 (but has breaking FK API changes)

Workaround: Temporarily swap to beta, run push, restore stable

# Standard
deno run --allow-read --allow-write --allow-env --allow-run scripts/db-push-workaround.ts

# Preview changes first
deno run --allow-read --allow-write --allow-env --allow-run scripts/db-push-workaround.ts --dry-run

# Manual rollback (if script fails)
cp package.json.bak package.json
cp pnpm-workspace.yaml.bak pnpm-workspace.yaml
pnpm install

What still works: Direct SQL queries (our db-status script)

What's broken: push, generate, migrate, studio - all drizzle-kit CLI commands

Troubleshooting Common Issues

"Backup files already exist"

Symptom: Script exits with "Found existing backup" warning Cause: Previous failed run left backup files Solution:

# Check what's in the backups
cat package.json.bak | head -10
cat pnpm-workspace.yaml.bak | head -10

# If backups look good, remove them and try again
rm package.json.bak pnpm-workspace.yaml.bak
deno run --allow-read --allow-write --allow-env --allow-run scripts/db-push-workaround.ts

"pnpm install fails during restore"

Symptom: Script crashes when restoring stable versions Cause: Network issues or corrupted pnpm-lock.yaml Solution: Manual restore with fresh lockfile

cp package.json.bak package.json
cp pnpm-workspace.yaml.bak pnpm-workspace.yaml
rm pnpm-lock.yaml  # Force regeneration
pnpm install

"Database push partially succeeds"

Symptom: Some tables created, others missing Cause: Script interrupted mid-execution Solution: Use moon run scripts:db-status to check state, then retry the push

"Permission denied" errors

Symptom: Script fails with file permission errors Cause: Running with insufficient Deno permissions Solution: Ensure all required flags are used

deno run --allow-read --allow-write --allow-env --allow-run scripts/db-push-workaround.ts

Published: December 2024 Updated: December 2024 (v2 - production hardening after code review) Environment: Deno 1.x, Node 20.x, drizzle-orm 0.44.7, drizzle-kit 0.31.8, Neon PostgreSQL