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
httpsURL.
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):
bun run devFrom the Docker image (private, ghcr.io/eadwincode/flow-dev). Log in once with a GitHub token that has read:packages:
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-devEither way you get:
| What | Where |
|---|---|
The engine (your app's FLOW_BASE_URL) | http://127.0.0.1:8787 |
| The dashboard | http://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:
- Sign up as a normal user, as in the Quick start. With
bun run dev, sign-up is off until you addSIGNUP_OPEN=1toworkers/dashboard/.dev.vars. Emails are not sent: each one is written to a file underworkers/dashboard/.wrangler/tmp/email/, and the log prints its path, so open it for the link. - 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:
FLOW_BASE_URL=http://127.0.0.1:8787When 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.
- 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.
- Send an event to
http://127.0.0.1:8787/ewith no key (with curl,flow.send()orclient.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 forFLOW_SIGNING_KEYandFLOW_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.
In the local dashboard, Apps, then Add app. Generate a signing key and an event key, and copy them.
Start your app with:
bashFLOW_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_URLturns on dev mode, so to keep the signature check on, passisDev: false(TypeScript) orDev: 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.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 machine http://127.0.0.1:<port>/api/flowin Docker on your machine http://host.docker.internal:<port>/api/flowin Docker in the same compose file http://<service>:<port>/api/flowPlain
httpis fine here. The local engine allows it; the hosted one only takes publichttpsURLs.Send an event to
http://127.0.0.1:8787/e(with curl,flow.send()orclient.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):
# 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:
{"ok":true,"created":false,"urlChanged":null,"modified":[],"archived":[]}Your app in Docker
Run your app next to the engine in one compose file:
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:
cloudflared tunnel --url http://localhost:3000It prints a public URL like https://random-words.trycloudflare.com. Then:
- 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. - Start your app with that app's signing key.
- 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
| Problem | Cause and fix |
|---|---|
docker run says denied or unauthorized | You 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 https | The 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 answer | The 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 Down | Under 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 mode | Your SDK is too old for keyless local mode. Use the steps with keys. |
A sync or event with no key gets 401 | The 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 on | Its 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 401 | FLOW_SIGNING_KEY is not the key from the local dashboard. Each engine has its own keys. |
Sending an event gives 401 invalid event key | Use 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 start | The 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>. |
