Skip to content

Local development ​

While you work on your functions, your app runs on your laptop. The engine must be able to call it. There are two ways:

  • A local engine: the whole engine and its dashboard on your machine. No internet, no https, and you can reset it at any time. This needs access to the engine's code or its private Docker image, so it is for team members.
  • The hosted engine through a tunnel: your app on your laptop, the real engine calling it through a public https URL.

A local engine on your machine ​

The engine is not open source. Team members run it one of two ways.

From the engine's repo (needs Node 22+, Bun 1.4+ and the repo's dependencies, pnpm install):

bash
bun run dev

From the Docker image (private, ghcr.io/eadwincode/flow-dev). Log in once with a GitHub token that has read:packages:

bash
echo "$TOKEN" | docker login ghcr.io -u <your github user> --password-stdin
docker run --rm -p 127.0.0.1:8787:8787 -p 127.0.0.1:5173:5173 \
  --add-host=host.docker.internal:host-gateway \
  -v flow-dev-data:/data ghcr.io/eadwincode/flow-dev

Either way you get:

WhatWhere
The engine (your app's FLOW_BASE_URL)http://127.0.0.1:8787
The dashboardhttp://localhost:5173

Keep the ports on 127.0.0.1 as above, so others on your network can not reach them.

The first start ​

The first start makes the engine's secrets and prints a setup code. Open the dashboard: it sends you to /setup. Type the code and make a staff user. A staff user is in no workspace yet, so make one:

  1. Sign up as a normal user, as in the Quick start. With bun run dev, sign-up is off until you add SIGNUP_OPEN=1 to workers/dashboard/.dev.vars. Emails are not sent: each one is written to a file under workers/dashboard/.wrangler/tmp/email/, and the log prints its path, so open it for the link.
  2. Give the new account a plan without paying: as the staff user, open the staff console (/staff), open the account, and click Comp the plan.

Runs, apps, keys and users are kept between starts. To start over, run bun run dev --reset, or delete the Docker volume (docker volume rm flow-dev-data).

Point your app at it ​

There are two ways: with no keys (quickest), or with keys, as in production.

With no keys ​

The local engine runs in local mode (bun run dev and the Docker image turn it on with FLOW_LOCAL_DEV=1). In local mode the engine takes a sync, an event or an app's call that has no key, and puts it in the built-in workspace, the one your local login is an Owner of. So you need no Add app, no signing key and no event key. Set only the engine's address:

bash
FLOW_BASE_URL=http://127.0.0.1:8787

When FLOW_BASE_URL is on localhost, 127.0.0.1 or host.docker.internal, the SDK turns on dev mode by itself, and logs that it did. For any other engine address, FLOW_LOCAL_DEV=1 turns it on.

  1. Start your app. Its sync at start adds it to the dashboard's Apps page, with its functions. The engine makes a signing key for the app and signs its calls with it; in dev mode your app does not check them.
  2. Send an event to http://127.0.0.1:8787/e with no key (with curl, flow.send() or client.Send()), and watch the run in the dashboard.

The app's URL is the one in its sync, so the engine must reach your app at the address the SDK sees (see Syncing while you work). For Docker, where that is not true, use Change URL on the Apps page or the steps with keys below.

A key that you do send is still checked: a wrong key gets 401, as in production.

Dev mode turns on by itself from SDK v0.10.0. With v0.9.0, also set FLOW_DEV=1. v0.8.0 and before still ask for FLOW_SIGNING_KEY and FLOW_EVENT_KEY: use the steps with keys below.

With keys, as in production ​

Use this to test your app as it will run in production, with the signature check on.

  1. In the local dashboard, Apps, then Add app. Generate a signing key and an event key, and copy them.

  2. Start your app with:

    bash
    FLOW_BASE_URL=http://127.0.0.1:8787
    FLOW_SIGNING_KEY=<the signing key from Add app>
    FLOW_EVENT_KEY=<the event key>

    A local FLOW_BASE_URL turns on dev mode, so to keep the signature check on, pass isDev: false (TypeScript) or Dev: flow.BoolPtr(false) (Go) in code. The local engine signs its calls like the real one, so your app runs as it will in production.

  3. In Add app, type your endpoint's URL as the engine reaches it, and click Connect:

    The engine runs...Your app runs...URL to type
    with bun run devon your machinehttp://127.0.0.1:<port>/api/flow
    in Dockeron your machinehttp://host.docker.internal:<port>/api/flow
    in Dockerin the same compose filehttp://<service>:<port>/api/flow

    Plain http is fine here. The local engine allows it; the hosted one only takes public https URLs.

  4. Send an event to http://127.0.0.1:8787/e (with curl, flow.send() or client.Send()), and watch the run in the dashboard.

Use 127.0.0.1 rather than localhost for the engine: it listens on IPv4, and some runtimes (Node among them) try localhost as IPv6 first. Open the dashboard as http://localhost:5173: its login cookie is Secure, and browsers allow that on http://localhost.

Syncing while you work ​

Each time your app starts, it syncs its functions by itself (the SDK compares a hash of its functions with the engine's copy). So after you change a function, restart the app. You can also click Resync on the Apps page.

In dev mode, a PUT to your own endpoint syncs too, and saves the URL the request used as the app's URL. So send it with the address the engine uses (without dev mode, the app needs the PUT signed and answers a plain one with 401):

bash
# bun run dev, app on your machine: the engine uses the same address as you
curl -X PUT http://127.0.0.1:<port>/api/flow

# engine in Docker: give the engine's address in the Host header
curl -X PUT http://127.0.0.1:<port>/api/flow -H "Host: host.docker.internal:<port>"

The answer lists what changed, for example:

json
{"ok":true,"created":false,"urlChanged":null,"modified":[],"archived":[]}

Your app in Docker ​

Run your app next to the engine in one compose file:

yaml
services:
  flow-dev:
    image: ghcr.io/eadwincode/flow-dev:latest
    ports: ["127.0.0.1:8787:8787", "127.0.0.1:5173:5173"]
    volumes: [flow-dev-data:/data]

  shop:
    build: .
    ports: ["127.0.0.1:3000:3000"]
    environment:
      FLOW_BASE_URL: http://flow-dev:8787
      FLOW_SIGNING_KEY: ${FLOW_SIGNING_KEY}
      FLOW_EVENT_KEY: ${FLOW_EVENT_KEY}

volumes:
  flow-dev-data:

In Add app, the URL is http://shop:3000/api/flow. Your browser still uses http://localhost:5173 for the dashboard.

If the engine runs with bun run dev and only your app is in Docker, use FLOW_BASE_URL=http://host.docker.internal:8787 in the app, the URL http://host.docker.internal:<port>/api/flow in Add app, and allow that host: FLOW_ALLOW_HTTP_HOSTS=host.docker.internal bun run dev.

What works locally ​

Everything the engine does: steps and retries, sleeps, waiting for events, sending events from steps, invoking other functions, flow control, cron triggers, failure handlers, the run timeline, and realtime. Cron triggers fire on time.

Old runs go away as in production: 3 days after a run ends (4 days if it failed). Keep on a run's page keeps it. The engine's daily cleanup runs at 03:17 UTC; bun run dev --no-cron turns it off.

The hosted engine through a tunnel ​

Without a local engine, use the hosted one. It calls your app over https, so it can not reach localhost on your laptop. Use a tunnel, for example Cloudflare's:

bash
cloudflared tunnel --url http://localhost:3000

It prints a public URL like https://random-words.trycloudflare.com. Then:

  1. Use an app ID of your own for your laptop, for example shop-dev-ada, so your local functions never replace the real app's functions. Add it as a separate app in the dashboard.
  2. Start your app with that app's signing key.
  3. Connect it with the tunnel URL and the path: https://random-words.trycloudflare.com/api/flow.

Each time the tunnel gives you a new URL, use Change URL on the Apps page.

A tunnel like this goes through Cloudflare's proxy, which ends a call that sends nothing for about 100 seconds. The SDK sends a small heartbeat every 20 seconds while a step runs, so steps can still run up to their step timeout.

Dev mode turns off the signature check in the SDK. Use it only for a quick test that nothing but you can reach. A tunnel makes your app public, so run it with keys and dev mode off in code: otherwise anyone could call your functions.

Troubleshooting ​

ProblemCause and fix
docker run says denied or unauthorizedYou are not logged in to ghcr.io, or your token has no read:packages, or no access to the package.
Add app says app url must be httpsThe host is not allowed over plain http. bun run dev allows localhost and 127.0.0.1; add others with FLOW_ALLOW_HTTP_HOSTS. The Docker image allows any host.
Add app says the app did not answerThe engine can not reach the URL you typed. From inside Docker, localhost is the container itself: use host.docker.internal or the compose service name.
Steps fail after a curl -X PUT sync, and the app shows DownUnder Docker, a PUT saved 127.0.0.1 as the app's URL. Send it again with the engine's address in the Host header, or use Resync.
The app logs sync needs a signing key or flow.send needs an event key in dev modeYour SDK is too old for keyless local mode. Use the steps with keys.
A sync or event with no key gets 401The engine is not in local mode (it needs FLOW_LOCAL_DEV=1; bun run dev and the Docker image set it), or it is the hosted engine, which always needs keys.
An app that runs with keys does not check signatures, and logs dev mode is onIts FLOW_BASE_URL is local, which turns on dev mode. Pass isDev: false / Dev: flow.BoolPtr(false) in code.
Every call to your app gets 401FLOW_SIGNING_KEY is not the key from the local dashboard. Each engine has its own keys.
Sending an event gives 401 invalid event keyUse an event key from the local dashboard, and send it to http://127.0.0.1:8787/e.
The app logs the sync failed (400): invalid url at startThe engine does not know the app's URL yet. Add the app (Connect), or, in dev mode, sync once with curl -X PUT <app-url>.