Skip to main content

Local Development

Alchemy supports local development mode where resources can be simulated locally using tools like Miniflare for Cloudflare Workers. This allows you to iterate quickly without deploying to production.

Enabling Local Mode

Run your Alchemy script with the --local or --dev flag:
bun ./alchemy.run.ts --local
Or enable it programmatically:
const app = await alchemy("my-app", {
  local: true
});
Local development mode is in beta. Report any issues to GitHub Issues.

How Local Mode Works

When local: true is set, Alchemy:
  1. Skips cloud provider API calls for supported resources
  2. Starts local simulators (like Miniflare for Cloudflare Workers)
  3. Returns mock data for resources that don’t support local development
  4. Binds local resources together so they can interact

Supported Resources

Cloudflare Workers

Workers run locally using Miniflare:
import alchemy from "alchemy";
import { Worker } from "alchemy/cloudflare";
import path from "node:path";

const app = await alchemy("my-app", {
  local: true
});

const worker = await Worker("api", {
  entrypoint: path.join(import.meta.dirname, "src", "index.ts"),
  bindings: {
    API_KEY: alchemy.secret.env.API_KEY
  }
});

console.log(worker.url);  // http://localhost:8787

await app.finalize();

Cloudflare D1 Database

D1 databases use local SQLite:
import { Worker, D1Database } from "alchemy/cloudflare";

const app = await alchemy("my-app", {
  local: true
});

const db = await D1Database("database", {
  name: "my-database"
});

const worker = await Worker("api", {
  entrypoint: "./src/index.ts",
  bindings: {
    DB: db  // Binds to local SQLite database
  }
});

await app.finalize();

Cloudflare KV, R2, and More

Most Cloudflare resources work locally through Miniflare:
import { Worker, KVNamespace, R2Bucket } from "alchemy/cloudflare";

const app = await alchemy("my-app", {
  local: true
});

const kv = await KVNamespace("cache", {
  name: "my-cache"
});

const bucket = await R2Bucket("storage", {
  name: "my-bucket"
});

const worker = await Worker("api", {
  entrypoint: "./src/index.ts",
  bindings: {
    CACHE: kv,
    BUCKET: bucket
  }
});

await app.finalize();

Watch Mode

Combine local mode with watch mode for automatic reloading:
bun --watch ./alchemy.run.ts --local
Or use the Alchemy CLI:
alchemy dev
This automatically:
  • Restarts your script when files change
  • Reloads local workers with new code
  • Preserves local state between reloads
Watch mode is perfect for rapid iteration during development.

Testing Local Resources

Once your resources are running locally, you can test them:

HTTP Requests

import alchemy from "alchemy";
import { Worker } from "alchemy/cloudflare";

const app = await alchemy("my-app", {
  local: true
});

const worker = await Worker("api", {
  entrypoint: "./src/index.ts"
});

console.log(`Worker running at: ${worker.url}`);

await app.finalize();

// Test with curl:
// curl http://localhost:8787

Programmatic Testing

const worker = await Worker("api", {
  entrypoint: "./src/index.ts"
});

await app.finalize();

// Make request to local worker
const response = await fetch(worker.url);
const data = await response.text();
console.log(data);

Local vs Production Differences

Resource Behavior

Some resources behave differently in local mode:
const app = await alchemy("my-app", {
  local: true
});

const worker = await Worker("api", {
  entrypoint: "./src/index.ts"
});

// worker.url = http://localhost:8787
// Runs in Miniflare locally

State Storage

In local mode, state is still persisted to .alchemy/ but resources run locally:
.alchemy/
  my-app/
    john/           # Your stage
      state.json    # Resource state
  logs/             # Local process logs
  pids/             # Local process PIDs

Conditional Local Behavior

Check if running in local mode within resources:
import { Scope } from "alchemy";

const scope = Scope.current;

if (scope.local) {
  console.log("Running in local development mode");
  // Use local configuration
} else {
  console.log("Running in production mode");
  // Use production configuration
}

Environment Variables

Use different environment variables for local vs production:
ALCHEMY_PASSWORD=local-dev-password
API_KEY=test-api-key
DATABASE_URL=http://localhost:5432
alchemy.run.ts
import "dotenv/config";
import alchemy from "alchemy";

// Load appropriate .env file based on mode
const envFile = process.argv.includes("--local") 
  ? ".env.local" 
  : ".env.production";

const app = await alchemy("my-app");
// Resources will use appropriate environment variables

Tunneling

Create a public URL for your local resources using tunnels:
bun ./alchemy.run.ts --local --tunnel
This creates a public URL you can share:
Worker running at: https://random-id.trycloudflare.com
Use tunnels to test webhooks and integrations that require public URLs.

Example: Full Local Development Setup

import "dotenv/config";
import alchemy from "alchemy";
import { Worker, D1Database, R2Bucket } from "alchemy/cloudflare";
import path from "node:path";

const app = await alchemy("my-app");
// Run with: bun --watch ./alchemy.run.ts --local

// Database (local SQLite in dev)
const db = await D1Database("database", {
  name: "my-database"
});

// Storage (local filesystem in dev)
const bucket = await R2Bucket("storage", {
  name: "my-bucket"
});

// Worker (local Miniflare in dev)
const worker = await Worker("api", {
  entrypoint: path.join(import.meta.dirname, "src", "index.ts"),
  bindings: {
    DB: db,
    BUCKET: bucket,
    API_KEY: alchemy.secret.env.API_KEY
  }
});

console.log(`
πŸš€ Worker running at: ${worker.url}

Test with:
  curl ${worker.url}
  curl ${worker.url}/api/users
`);

await app.finalize();

Troubleshooting

Port Already in Use

If port 8787 is already in use, specify a different port:
const worker = await Worker("api", {
  entrypoint: "./src/index.ts",
  dev: {
    port: 8788
  }
});

Local State Not Persisting

Local state is stored in .alchemy/ - make sure this directory is not gitignored (only .alchemy/*.sqlite should be ignored):
# .gitignore
.alchemy/*.sqlite
.alchemy/logs/
.alchemy/pids/

Resources Not Reloading

Ensure you’re using --watch or bun --watch:
# βœ… This will reload on changes
bun --watch ./alchemy.run.ts --local

# ❌ This won't reload
bun ./alchemy.run.ts --local

Best Practices

1
Use Local Mode for Development
2
Develop and test locally before deploying:
3
# Development
bun --watch ./alchemy.run.ts --local

# Production deploy
bun ./alchemy.run.ts
4
Separate Environment Variables
5
Keep local and production configs separate:
6
.env.local        # Local development
.env.production   # Production
.env.example      # Template (commit this)
7
Test Thoroughly Locally
8
Test all functionality locally before deploying:
9
// Test script
import alchemy from "alchemy";

const app = await alchemy("my-app", { local: true });

// Create resources...

await app.finalize();

// Run tests
const response = await fetch(worker.url + "/api/users");
assert(response.ok);
10
Use Tunnels for External Services
11
When testing webhooks or external integrations:
12
bun ./alchemy.run.ts --local --tunnel
# Use the public URL for webhook configuration

Next Steps

Build docs developers (and LLMs) love