Skip to content

Local development

jiayang dev runs your app on your machine with a small copy of the platform’s front door in front of it. Every request it passes to your app carries a real, signed identity token, so the code that verifies callers runs exactly as it does in production.

It needs no account, and you don’t have to be signed in.

  1. In your project’s directory, start it:

    Terminal window
    jiayang dev
    Making a signing key for this run ...
    Your app is at http://127.0.0.1:8787, signed in as you@example.com.
    Be someone else at http://127.0.0.1:8787/.jiayang.

    Your app’s own output follows.

  2. Open http://127.0.0.1:8787, not the port your app listens on. Requests to your app’s own port skip the front door and carry no token.

    With the app from the SDK overview, which greets its caller:

    Terminal window
    curl http://127.0.0.1:8787/
    hello you@example.com
  3. Stop it with Ctrl-C. Your app stops with it, along with anything it started.

If jiayang dev picks the wrong way to start your app, say it yourself after --:

Terminal window
jiayang dev -- npm run dev
Option Default What it does
[DIR] . The project to run
--as <EMAIL> you@example.com Who you are signed in as
--role <ROLE> owner Your role: viewer, editor or owner
--port <PORT> 8787 Where the front door listens
--app-port <APP_PORT> 8788 Where your app listens. Your app is told it as PORT.
--app-id <APP_ID> 00000000-0000-4000-8000-000000000000 The app id tokens are made out to, which your app checks as the audience
-- <COMMAND> worked out from the project What starts your app

To see the app as a viewer from the start:

Terminal window
jiayang dev --as someone@example.com --role viewer

With no command after --, it looks at the project the way jiayang deploy does, and starts it like this:

The project is jiayang dev runs
A static site wrangler dev serving the site’s build output as the last build left it
A Worker with a wrangler config wrangler dev with that config
A Worker without one wrangler dev on its entry file (worker.main in jiayang.json, else index.js, src/index.ts and the like), with the compatibility date and flags from jiayang.json
A container with container.start in jiayang.json that command
A Node server npm start
FastAPI uvicorn <module>:app --host 127.0.0.1 --port 8788
Flask flask --app <module> run --host 127.0.0.1 --port 8788
Django with a manage.py python manage.py runserver 127.0.0.1:8788
Django without one gunicorn <project>.wsgi:application --bind 127.0.0.1:8788
Streamlit streamlit run <script> --server.port 8788 --server.address 127.0.0.1 --server.headless true
Gradio, or another Python script python <module>.py
Go with a main.go go run .
Rust cargo run
A Dockerfile, and no container.start nothing: name the command after --

Django needs a <project>/wsgi.py either way, manage.py or not. Without one, jiayang dev stops: name the command after --, or in container.start in jiayang.json.

A Dockerfile says how to build an image, not how to run the app on your machine, so jiayang dev asks you to say it. So does a Go project with no main.go at its root: jiayang dev -- go run ./cmd/server.

The ports in the table are the --app-port.

wrangler dev is the project’s own wrangler when it has version 4 or newer installed, and npx --yes wrangler@4.134.0 otherwise. Set JIAYANG_WRANGLER to run a different one.

A static site is served as its last build left it. To see a change, build again, or run the project’s own dev server with jiayang dev -- npm run dev.

jiayang dev starts your app with the same variables the platform sets, pointed at itself:

Variable Value
PORT 8788 (the --app-port)
JIAYANG_APP_ID 00000000-0000-4000-8000-000000000000 (the --app-id)
JIAYANG_IDENTITY_ISSUER http://127.0.0.1:8787
JIAYANG_JWKS_URL http://127.0.0.1:8787/.jiayang/jwks.json

A Worker doesn’t read the environment it starts in, so jiayang dev passes these to wrangler dev as --var bindings. Your env has them either way.

The key that signs the tokens is made when jiayang dev starts and ends when it stops. Nothing it signs is trusted anywhere else.

Open http://127.0.0.1:8787/.jiayang in your browser. It shows who every request is signed as, and a form to change it:

  • Become signs every request after it as the email and role you enter.
  • Nobody sends requests with no identity at all, so you see what a stranger sees.

The change applies to the next request. Your app doesn’t restart.

The form only accepts a post from its own page. A request from anywhere else gets 403 not from here.

It does what the edge does, in the same order:

  1. It deletes every X-Jiayang-*, Cf-Access-* and Cf-Container-* header the client sent, spelled with dashes or underscores.
  2. It adds a fresh X-Jiayang-Identity token, lasting 60 seconds, plus X-Jiayang-Email and X-Jiayang-Role for display.
  3. It passes the request to your app, and your app’s answer back unchanged. Streamed responses, server-sent events and WebSockets work, so hot reload does too.

A forged header does nothing here, as in production:

Terminal window
curl -H 'X-Jiayang-Email: boss@example.com' -H 'X-Jiayang-Identity: forged' http://127.0.0.1:8787/
hello you@example.com

It only answers requests addressed to localhost, 127.0.0.1, [::1] or a name ending in .localhost, such as api.localhost. A request for any other name gets 421. That stops a web page from pointing a name it controls at your machine and sending requests your app would take as yours.

The tokens are real, and signed the same way. Some values in them are made up:

  • sub is dev: and the email, like dev:you@example.com, not a usr_… id.
  • The workspace id is 00000000-0000-4000-8000-0000000000de.
  • kind is always user. There are no bypass tokens here, and no webhook tokens: a route that calls requireWebhook refuses every request with invalid identity token. Try a webhook route on the platform, with a test delivery from the provider. See Public paths and webhooks.
  • Anyone you type is let in. jiayang dev doesn’t know who your app is shared with.

X-Jiayang-Role is only sent by jiayang dev. In production, read the role from the verified token.

The SDKs have no switch that skips the signature check, because a switch like that can be left on in production. jiayang dev gives them real tokens to check instead, so your code runs the same checks on your machine as it does deployed.

The front door can’t listen because something else has the port:

Making a signing key for this run ...
error: couldn't listen on 127.0.0.1:8787: Address already in use (os error 48)

Stop whatever holds it, or pick another port with --port.

Your app hasn’t started yet, or listens on a different port from the one it’s told:

Nothing is listening on 127.0.0.1:8788 yet (Connection refused (os error 61)).

Make your app listen on PORT. Some dev servers ignore it and need their port set with a flag of their own. Pass --app-port to match.

An email without an @:

error: "someone" doesn't look like an email address

Every request gets 401 from your app. Check that you opened port 8787 and not your app’s own port, and that your app reads the variables above rather than values of its own.

See also Troubleshooting.