---
title: "Local Development"
description: "Three ways to run the stack locally — pick the one that matches what you're doing"
source: /docs/local-development
---


Three Modes [#three-modes]

Your project ships with three local-dev entry points. Pick based on what you're doing right now.

| Command           | Services                             | Public URLs                            | Use when                                                                 |
| ----------------- | ------------------------------------ | -------------------------------------- | ------------------------------------------------------------------------ |
| `pnpm dev:min`    | web, api, mastra                     | localhost only                         | Fast iteration on UI, APIs, agent prompts — no external callbacks needed |
| `pnpm dev:tunnel` | web, api, mastra + Cloudflare Tunnel | `local-{slug}-{service}.mastrakit.dev` | OAuth logins, webhooks (Stripe, etc.), MCP clients reaching your laptop  |
| `pnpm dev`        | all 5 services (adds auth, metering) | localhost only                         | Working on auth or metering, or parity with full-stack behavior          |

All three run services **natively** on your host (no containers). Auth and metering, when not running locally, are reached via whatever URLs your env files point at — typically your CF dev deploys.

Individual app dev is also available: `pnpm dev:web`, `pnpm dev:api`, `pnpm dev:mastra`, `pnpm dev:auth`, `pnpm dev:metering`.

When to Use Tunnel Mode [#when-to-use-tunnel-mode]

Tunnel mode gives each service a stable public URL on `mastrakit.dev`. Reach for it when:

* **Google/GitHub OAuth** — the callback URL must be publicly reachable
* **Stripe webhooks** — Stripe posts events to a public HTTPS endpoint
* **MCP from ChatGPT or Claude Desktop** — external clients need a URL to connect to your local agent
* **Collaborating** — sharing a live preview with a teammate

For everything else, `dev:min` is faster — no Cloudflare round-trip.

Tunnel URL Pattern [#tunnel-url-pattern]

URLs are derived from your current git branch:

```
local-{slug}-web.mastrakit.dev    → localhost:3000
local-{slug}-api.mastrakit.dev    → localhost:3001
local-{slug}-mastra.mastrakit.dev → localhost:4111
```

The `slug` is your branch name, lowercased and sanitized (e.g. `feat/MAK-33-foo` → `feat-mak-33-foo`). This matches the slug used by deployed Cloudflare Workers — see [Deploy](/docs/deploy) — but tunnel hosts are prefixed with `local-` so they don't collide.

Every workspace/branch gets its own isolated tunnel. No more sharing one `zyme-*` tunnel across everything.

First-Time Tunnel Setup [#first-time-tunnel-setup]

The tunnel script auto-provisions everything on first run, but you need [`cloudflared`](https://developers.cloudflare.com/cloudflare-one/connections/connect-networks/downloads/) installed and authenticated to your Cloudflare account:

```bash
# Install (macOS)
brew install cloudflared

# Authenticate once
cloudflared login
```

Then in any workspace:

```bash
pnpm dev:tunnel
```

On first run for a given branch the script will:

1. Create a named tunnel `local-{slug}`
2. Register DNS routes for the three hostnames above
3. Write a per-workspace config to `.conductor-tunnel-config.yml` (gitignored)
4. Start web, api, mastra, and the tunnel

Subsequent runs skip the provisioning and go straight to starting services.

OAuth & Webhook Registration [#oauth--webhook-registration]

Because each workspace has its own tunnel URLs, external services need to know about them:

* **Google OAuth / GitHub OAuth** — add `https://local-{slug}-web.mastrakit.dev/api/auth/callback/{provider}` to the authorized redirect URIs in your provider's console. Add a new entry whenever you start working on a new long-lived branch.
* **Stripe webhooks** — point your test-mode webhook endpoint at `https://local-{slug}-api.mastrakit.dev/webhooks/stripe`.

For quick one-off Stripe testing without a tunnel, the Stripe CLI forwards webhooks directly to localhost:

```bash
stripe listen --forward-to localhost:3001/webhooks/stripe
```

Short-lived branches don't need external OAuth — use `dev:min` and an email/password login instead.

Other Useful Commands [#other-useful-commands]

```bash
pnpm dev:status    # Show which services are running and on which ports
pnpm dev:stop      # Kill all local dev processes (web, api, mastra, tunnel)
```

Running Multiple Workspaces at Once [#running-multiple-workspaces-at-once]

Cloudflare side is fully isolated — each workspace has its own tunnel, its own URLs, its own DNS records. **Localhost is not isolated** — ports 3000/3001/4111 collide if you run `dev:tunnel` or `dev:min` in two workspaces simultaneously. Work in one workspace at a time, or stop services before switching.

Archiving a Workspace [#archiving-a-workspace]

Tunnels and DNS records created by `dev:tunnel` persist in your Cloudflare account until deleted. When you're done with a branch, clean them up:

```bash
cloudflared tunnel delete local-{slug}
# DNS records under *.mastrakit.dev may need manual cleanup in the CF dashboard
```

Troubleshooting [#troubleshooting]

**"Tunnel creation failed"** — run `cloudflared login` and retry.

**"Could not derive workspace slug"** — you're in detached HEAD. Check out a branch first.

**Services start but URLs return 404** — the tunnel is still starting. Wait \~10 seconds and retry. If it persists, check `/tmp/tunnel.log`.

**Something's broken in localhost-only mode** — confirm your `.env.local` / `.dev.vars` reference the services you're actually running. If `dev:min` skips auth/metering, those env vars should point at your CF dev deploys, not localhost.
