---
title: "Cloudflare Wrangler Usage Tips"
description: "A few tips for using the Cloudflare Wrangler CLI."
---

> Documentation Index
> Fetch the complete documentation index at: https://luozhouyang.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Cloudflare Wrangler Usage Tips

`wrangler` is Cloudflare's official tool for managing Cloudflare Workers.

## Explicitly Creating a Local D1 Database

After you've configured `d1_databases` in `wrangler.toml` or `wrangler.jsonc`, you may want to create a SQLite database locally to simulate D1.

```toml
d1_databases = [
  { binding = "DB", database_name = "my_db", database_id = "1234567890" }
]
```

By default, wrangler does not create this local database immediately. To create it immediately locally, you need to:
* Run `npx wrangler dev`
* Run a SQL query, for example `npx wrangler d1 execute my_db --local --command='SELECT * FROM my_table'`

After that, you can see the SQLite database created under the `.wrangler/state/v3/d1/` directory:

```bash
.wrangler
├── state
│   └── v3
│       ├── cache
│       │   └── miniflare-CacheObject
│       ├── d1
│       │   └── miniflare-D1DatabaseObject
│       │       ├── 14c8722131cce17d31bc37d958e4eacf978b0d4d2c28dea6784e37418eeb3643.sqlite
│       │       ├── 14c8722131cce17d31bc37d958e4eacf978b0d4d2c28dea6784e37418eeb3643.sqlite-shm
│       │       └── 14c8722131cce17d31bc37d958e4eacf978b0d4d2c28dea6784e37418eeb3643.sqlite-wal
│       └── workflows
└── tmp

8 directories, 3 files
```

Then you can set up a `drizzle-dev.config.ts` file, point the data source at this local SQLite database, and do CRUD locally.

## Configuring D1 and Drizzle-ORM for the Development Environment (the wrong turn)

drizzle-orm can provide a config file for each environment. Usually you'll set up two environments, **development** and **production**.

For the local development environment, here is an example config, `drizzle-dev.config.ts`:

```typescript
import { defineConfig } from 'drizzle-kit';
import fs from "fs";
import path from "path";

function resolveDbPath() {
const dir = path.resolve(".wrangler/state/v3/d1/miniflare-D1DatabaseObject");
try {
    const files = fs.readdirSync(dir);
    const sqliteFile = files.find((file) => file.endsWith('.sqlite'));
    if (!sqliteFile) {
        throw new Error(`No .sqlite file found in ${dir}`);
    }
    return path.join(dir, sqliteFile);
} catch (err) {
    console.error('Error resolving database path:', err);
    // Return a default or throw an error if the directory/file can't be found
    // For now, let's throw to make it obvious something is wrong.
    throw new Error('Could not resolve the local D1 database path. Please ensure you have mannual created the local db correctly.');
}
}

export default defineConfig({
schema: './src/db/schemas/*',
out: 'drizzle/migrations',
dialect: 'sqlite',
dbCredentials: {
    url: resolveDbPath(),
},
});

```

That way you can generate migration files for schema changes:
```bash
npx drizzle-kit generate --config drizzle-dev.config.ts --name='update_tables'
```

For convenience, add a `script` to `package.json`:

```json
"scripts": {
  "db:generate": "drizzle-kit generate --config drizzle-dev.config.ts",
}
```

Example of using the script:

```bash
npm run db:generate -- --name='init_tables'
```

## Configuring D1 and Drizzle-ORM for the Development Environment (the correct way)

Actually, the `drizzle.config.ts` file is not mandatory, especially at **runtime**, where it is not needed.

For a Cloudflare Workers project, the main purpose of providing a local `drizzle.config.ts` file is to generate `migrations` files locally. Applying the migrations to the D1 database, however, has to be done with `wrangler`.

After some experimentation, drizzle can be configured with the following steps:
Step 1: Create a `drizzle.config.ts` file in the project with the following content:
```typescript
import { defineConfig } from "drizzle-kit";

export default defineConfig({
dialect: "sqlite",
schema: "./src/db/schemas",
driver: "d1-http",
out: "./drizzle/migrations",
dbCredentials: {
    accountId: process.env.CF_ACCOUNT_ID!,
    databaseId: process.env.CF_DATABASE_ID!,
    token: process.env.CF_ACCESS_TOKEN!,
},
});

```

You can leave all the `dbCredentials` values empty in `.env.local`; drizzle never really uses them, because the runtime code does not read `drizzle.config.ts`.

Step 2: Generate the migration files using the `drizzle.config.ts` file above:
```bash
npx drizzle-kit generate --name=your_message
```

Step 3: Use `wrangler` to apply them to the **local** development D1 database:
```bash
npx wrangler d1 apply $your_database_name --local
```
This step applies the migrations to the SQLite database file under the `.wrangler/state/v3/d1/` directory; that is D1's local development database.

Step 4: Connect to the D1 database with `drizzle-orm` at **runtime**:
```typescript

import { drizzle } from "drizzle-orm/d1"
import { Database } from "../types"
import * as schemas from "./schemas"

export function getDatabase(db: D1Database): Database {
return drizzle(db, { schema: schemas })
}

```

So all you need to provide is the `D1Database` object, which is the D1 Binding declared in the Cloudflare Workers `env`. The configuration looks like this:
```json
{
"d1_databases": [
		{
			"binding": "DB",
			"database_name": "xxxxx",
			"database_id": "xxxxx",
			"migrations_dir": "drizzle/migrations"
		}
	],
}
```

After running `npx wrangler typegen`, you can find the `Env` definition in the generated `worker-configuration.d.ts` or a similar file, for example:
```typescript
declare namespace Cloudflare {
	interface Env {
		CF_ACCOUNT_ID: string;
		CF_DATABASE_ID: string;
		CF_ACCESS_TOKEN: string;
		DB: D1Database;
	}
}
interface CloudflareBindings extends Cloudflare.Env {}

```

## Disposing Service Bindings in Practice

wrangler 4.x supports implicit disposal with the `using` keyword. But while writing the code you may run into an error like `Object cannot be disposed`.

Looking at the `tsconfig.ts` file, I found:

```json
{
"compilerOptions": {
    "target": "ES2017"
}
}
```
In this case, if you want to dispose in the client Worker, one workable approach is to **call the dispose method explicitly**. Example code (nextjs):
```typescript
import { getCloudflareContext } from "@opennextjs/cloudflare";

export async function runRpcUserService<T>(
  callback: (userService: IUserService) => Promise<T>
) {
  const { env } = getCloudflareContext();
  // @ts-expect-error: ignore type error
  const userService = (await env.PICKNAMES_CORE.getUserService()) as IUserService;
  try {
return await callback(userService);
  } finally {
userService[Symbol.dispose];
  }
}

export async function runRpcChannelService<T>(
  callback: (channelService: IChannelService) => Promise<T>
) {
  const { env } = getCloudflareContext();
  // @ts-expect-error: ignore type error
  const channelService =(await env.PICKNAMES_CORE.getChannelService()) as IChannelService;
  try {
return await callback(channelService);
  } finally {
channelService[Symbol.dispose];
  }
}

```

But this leads to the following problem: at `build` time you get `Error: Expected an assignment or function call and instead saw an expression.`

So I changed it to:
* `userService[Symbol.dispose]` -> `userService[Symbol.dispose]()`
* `channelService[Symbol.dispose]` -> `channelService[Symbol.dispose]()`

In the build stage the error above no longer appears. But at runtime — that is, when running `npm run build` — the following error appears:
* `TypeError: r[Symbol.dispose] is not a function`

After some analysis, I concluded that the error was caused by the `as IUserService` and `as IChannelService` type casts. Since those two interface types are not a `Disposeable`, dispose-related errors show up at compile time or runtime.

See the Cloudflare official docs: [Use RpcTarget class to handle Durable Object metadata](https://developers.cloudflare.com/durable-objects/examples/reference-do-name-using-init/)
There is a piece of code:
```typescript
export default {
  async fetch(request, env, ctx): Promise<Response> {
let id: DurableObjectId = env.MY_DURABLE_OBJECT.idFromName(
  new URL(request.url).pathname,
);
let stub = env.MY_DURABLE_OBJECT.get(id);

//  * Set the Durable Object metadata using the RpcTarget
//  * Notice that no await is needed here

const rpcTarget = stub.setMetaData(id.name ?? "default");

// Call the Durable Object method using the RpcTarget.
// The DO identifier is stored in the Durable Object's storage
const greeting = await rpcTarget.computeMessage("world");

// Call the Durable Object method that does not use the Durable Object identifier
const simpleGreeting = await rpcTarget.simpleGreeting("world");

// Clean up the RpcTarget.
try {
  (await rpcTarget)[Symbol.dispose]?.();
  console.log("RpcTarget cleaned up.");
} catch (e) {
  console.error({
    message: "RpcTarget could not be cleaned up.",
    error: String(e),
    errorProperties: e,
  });
}

return new Response(greeting, { status: 200 });
  },
} satisfies ExportedHandler<Env>;
```

So I changed the code above to:
```typescript
export async function runRpcUserService<T>(
  callback: (userService: IUserService) => Promise<T>
) {
  const { env } = getCloudflareContext();
  // @ts-expect-error: ignore type error
  const userService = await env.PICKNAMES_CORE.getUserService();
  try {
return await callback(userService);
  } finally {
// if (typeof userService[Symbol.dispose] === 'function') {
//   userService[Symbol.dispose]();
// }
(await userService)[Symbol.dispose]?.();
  }
}

export async function runRpcChannelService<T>(
  callback: (channelService: IChannelService) => Promise<T>
) {
  const { env } = getCloudflareContext();
  // @ts-expect-error: ignore type error
  const channelService = await env.PICKNAMES_CORE.getChannelService();
  try {
return await callback(channelService);
  } catch (e) {
console.error(e);
  } finally {
// if (typeof channelService[Symbol.dispose] === 'function') {
//   channelService[Symbol.dispose]();
// }
(await channelService)[Symbol.dispose]?.();
  }
}
```

Compared with before, this approach:
* removes the problems caused by the type casts
* although the RPC call returns type `any`, the `callback` parameter gives it a concrete type, so callers still get type hints.

Problem solved.

Source: https://luozhouyang.com/cloudflare/wrangler-tips/index.mdx
