← Files Upstash RedisARCHIVED FILE

skills/upstash-workflow-js/how-to/local-dev.md

5 KB · Oct 3, 2026 · 06:22 UTC

↓ Download file

# Local Development for Upstash Workflow

This documentation explains how to run Upstash Workflow locally using the QStash development server and how to expose your local app using a public tunnel such as ngrok. It focuses on practical usage and TypeScript integration.

## Overview

Upstash Workflow uses Upstash QStash under the hood. During development you can:

- Use the **automatic dev server** by setting `QSTASH_DEV=true`. The SDK downloads the QStash CLI binary, spawns the server, and verifies signatures with its dev keys. No tokens or signing keys required.
- Run a **local QStash development server** manually via the CLI and wire credentials yourself.
- Optionally expose your local server using **ngrok** if you want to test with the production QStash.

---

## Automatic dev server (recommended)

Set `QSTASH_DEV=true` in your environment and that's it. Workflow's `serve()` endpoint and the `Client` both auto-detect it:

```
QSTASH_DEV=true
```

```ts
// app/api/workflow/route.ts
import { serve } from "@upstash/workflow/nextjs";

export const { POST } = serve(async (context) => {
  await context.run("step-1", () => console.log("running locally"));
});
```

```ts
import { Client } from "@upstash/workflow";

const client = new Client({ token: process.env.QSTASH_TOKEN ?? "" });

await client.trigger({
  url: "http://localhost:3000/api/workflow",
});
```

When `QSTASH_DEV=true`:

- The underlying `@upstash/qstash` client downloads the QStash CLI binary on first use, spawns the dev server on port `8080` (override via `QSTASH_DEV_PORT`), and reuses an already-running server on that port instead of spawning a duplicate.
- `serve()` builds a dev-mode `Receiver` internally, so signature verification works against the dev server's deterministic signing keys with no real credentials.
- It's a no-op in production (`NODE_ENV=production`), during `next build`, and in browser/edge runtimes.

**Next.js edge routes:** the edge runtime cannot spawn child processes. If your workflow route runs on the Edge Runtime, call `registerQStashDev()` from `instrumentation.ts` so the binary starts at Next.js boot:

```ts
// instrumentation.ts
import { registerQStashDev } from "@upstash/qstash/nextjs";

export function register() {
  registerQStashDev();
}
```

See the [QStash Local Development skill](../../upstash-qstash-js/fundamentals/local-development.md) for the full reference, including pitfalls and CLI options.

---

## 1. Start the QStash Local Development Server

If you'd rather manage the dev server yourself instead of using the automatic flow above, run the CLI manually.

Use the QStash CLI:

```
npx @upstash/qstash-cli dev
```

The CLI prints:

- QSTASH_TOKEN
- QSTASH_CURRENT_SIGNING_KEY
- QSTASH_NEXT_SIGNING_KEY
- Local server URL (default: `http://127.0.0.1:8080`)

Set these values in your `.env` file so your workflow client uses the local environment.

---

## 2. Set Local Environment Variables

Use the environment values printed by the CLI:

```
QSTASH_URL="http://127.0.0.1:8080"
QSTASH_TOKEN="<token-from-cli>"
QSTASH_CURRENT_SIGNING_KEY="<cur-key>"
QSTASH_NEXT_SIGNING_KEY="<next-key>"
```

These ensure all workflow requests are routed locally.

---

## 3. Trigger Workflows Using Local URLs

A common pattern is determining the base URL dynamically based on environment variables. Below is an example that demonstrates:

- Local development (localhost)
- Production deployments (auto-detected env)

```ts
import { Client } from "@upstash/workflow";

const client = Client();

// In production, Vercel sets VERCEL_URL. Otherwise use localhost.
const BASE_URL = process.env.VERCEL_URL
  ? `https://${process.env.VERCEL_URL}`
  : `http://localhost:3000`;

// Trigger a workflow with retries
const { workflowRunId } = await client.trigger({
  url: `${BASE_URL}/api/workflow`, // Local or production
  retries: 3, // Optional retry logic
});

console.log("Workflow run:", workflowRunId);
```

**Common mistakes:**

- Forgetting to include the full URL including `http://` or `https://`.
- Using a production URL while the local QStash server is running.
- Missing environment variables.

---

## 4. Using ngrok (Optional)

If your workflow must be reachable from the managed Upstash servers (not local), expose your local server publicly.

### Install & authenticate

```
ngrok config add-authtoken <YOUR-AUTH-TOKEN>
```

### Start a tunnel

```
ngrok http 3000
```

ngrok outputs a public URL:

```
Forwarding  https://1234abcd.ngrok.io -> http://localhost:3000
```

Use this public URL instead of localhost:

```ts
const BASE_URL = "https://1234abcd.ngrok.io"; // Public tunnel

await client.trigger({
  url: `${BASE_URL}/api/workflow`,
  retries: 3,
});
```

**Pitfall:** Your ngrok port must match your dev server port, otherwise all workflow calls will fail.

---

## Summary

- Start QStash locally using `qstash-cli dev`.
- Copy generated environment variables into `.env`.
- Use local URLs in TypeScript clients while developing.
- Optionally expose your server with ngrok if you need remote access.

This setup ensures fast workflow iteration without deploying your app.

SHA-256: 65bfacc88d41b0a8aca7b16fa85d760a6fda779088a29d8e88eaa8f087dc6d6b