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.
Run it
Section titled “Run it”-
In your project’s directory, start it:
Terminal window jiayang devMaking 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.
-
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 -
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 --:
jiayang dev -- npm run devOptions
Section titled “Options”| 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:
jiayang dev --as someone@example.com --role viewerHow it starts your app
Section titled “How it starts your app”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.
What your app is told
Section titled “What your app is told”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.
Be someone else
Section titled “Be someone 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.
What it does to each request
Section titled “What it does to each request”It does what the edge does, in the same order:
- It deletes every
X-Jiayang-*,Cf-Access-*andCf-Container-*header the client sent, spelled with dashes or underscores. - It adds a fresh
X-Jiayang-Identitytoken, lasting 60 seconds, plusX-Jiayang-EmailandX-Jiayang-Rolefor display. - 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:
curl -H 'X-Jiayang-Email: boss@example.com' -H 'X-Jiayang-Identity: forged' http://127.0.0.1:8787/hello you@example.comIt 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.
How the tokens differ from production
Section titled “How the tokens differ from production”The tokens are real, and signed the same way. Some values in them are made up:
subisdev:and the email, likedev:you@example.com, not ausr_…id.- The workspace id is
00000000-0000-4000-8000-0000000000de. kindis alwaysuser. There are no bypass tokens here, and no webhook tokens: a route that callsrequireWebhookrefuses every request withinvalid 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 devdoesn’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.
Why the SDKs have no development mode
Section titled “Why the SDKs have no development mode”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.
When it goes wrong
Section titled “When it goes wrong”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 addressEvery 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.