pw dev
pw devThis is the everyday command. It takes no arguments because the project file defines the loop.
What it does on startup
Section titled “What it does on startup”- starts the Devbox services declared in
devbox.json; - runs
pw generate; - applies pending migrations, unless
migration.autoisfalse; - builds the Tailwind stylesheet and starts its watcher, if Tailwind is enabled;
- starts the development identity provider, if
dev.idp.enabledistrue; - starts the telemetry viewer, unless
dev.otel.enabledisfalse; - prepares local JSONL capture, unless
dev.logs.enabledisfalse; - builds and runs
project.main.
After startup, it polls watched files twice a second. A change repeats only the
steps affected by that file rather than rebuilding the entire environment. The
restarted application does not reprint its
configuration summary:
a restart that resolved to the same configuration prints reloaded, and one
that did not prints the rows that moved.
What it watches
Section titled “What it watches”- the project’s own Go,
.pw.html, and.pw.sqlsources; - the migration directory;
- the Tailwind input, when Tailwind is enabled;
- anything matched by
dev.watch.includesinpopcornweb.toml.
The walk covers the module, not the [generate] purposes: any Go source is a
rebuild input, including files no purpose generates from. .git, vendor,
node_modules, .devbox, and the public tree are always skipped.
dev.watch.includes takes relative glob patterns for inputs the walk does not
reach. dev.watch.excludes skips a subtree, which is what to reach for when a
large dependency tree makes the walk the slowest step of the loop. Absolute
paths are rejected in both:
[dev.watch]includes = ["config.dev.toml", "assets/**/*.svg"]excludes = ["web/node_modules"]The port
Section titled “The port”The application binds server.port, and in development it does not insist on
it. A port already taken — a second project open in another terminal, a process
left behind by a loop that did not unwind — moves the run to the next free one
rather than ending it:
WARN the configured port could not be bound, so this development run moved to the next free one configured_port=8080 port=8081
listening on http://localhost:8081Both numbers are reported, and they mean different things: server.port in the
configuration tree is what the file asked for, and the listening line at the
end is the address that answers. The second one is where your browser goes. The
console links what the application announced rather than what the project file
says, so its application link follows the shift too.
The search gives up ten ports along, and only a development run makes it.
APP_ENV=stg, APP_ENV=prod, and every other named environment bind what they
were configured with and fail if they cannot, because a health check, a reverse
proxy, and an operator all go to the port the file names. An unset APP_ENV
resolves to development, so a deployment that never set the variable shifts as
well — which is why the warning names the environment, and why setting
APP_ENV is what restores the strict bind.
When development needs a fixed port anyway — an external OAuth provider with a
registered callback, say — nothing shifts while the port is free, so the fix is
to stop whatever holds it. pw doctor reports a bound
server.port before the loop starts, which is faster than reading it off a
warning after the fact.
Services
Section titled “Services”The services declared in devbox.json — Valkey by default — run under the
Devbox process manager with its full-screen terminal UI disabled. Their logs
join the same stream as generation, migration, and application output, one
prefixed line per event, instead of painting over it:
[valkey ] 1:M 27 Jul 2026 23:02:32.103 * Ready to accept connections tcpA project that needs no service can drop the package from devbox.json;
pw dev starts whatever Devbox declares and nothing else.
Tailwind
Section titled “Tailwind”During development the watcher always produces unminified CSS, regardless of
assets.tailwind.minify, because minification is the slow part of the loop. A
failed CSS watcher does not take down the server. pw dev keeps running and
falls back to watching the input file directly.
tailwindcss must be on PATH, which is what devbox shell is for. See
Styling.
Migrations
Section titled “Migrations”Pending migrations are applied before the application starts, and again when a file in the migration directory changes. Turn this off when you would rather control it yourself:
[migration]auto = falseDevelopment identity provider
Section titled “Development identity provider”pw dev starts a local OpenID Provider when dev.idp.enabled is true, injects
its issuer and credentials into the application process, and stops it with the
loop. Editing the roster reloads it without a restart.
[dev.idp]enabled = trueSee Development Identity Provider for the roster format, the claims, and what the provider does and does not implement.
Telemetry viewer
Section titled “Telemetry viewer”pw dev also runs a loopback OpenTelemetry receiver and a browser UI, and
points the application at it through the standard OTLP environment variables. It
is on by default, so traces and correlated log records are readable without a
collector:
pw dev: telemetry viewer http://127.0.0.1:54321[dev.otel]enabled = trueIt starts nothing when OTEL_EXPORTER_OTLP_ENDPOINT is already set, leaving your
own collector in charge. See
Development Telemetry Viewer.
Local structured logs
Section titled “Local structured logs”Application logs still appear as readable terminal text. Independently of the
viewer, pw dev also captures the structured form in .log/pw-dev-*.jsonl.
One invocation owns one file across application rebuilds, and the file appears
only after the first record.
[dev.logs]enabled = truedirectory = ".log"The directory is relative to the project and is not cleaned automatically. Disable this switch to retain only the terminal and configured OTLP outputs. See Telemetry for the schema and DuckDB queries.
The console
Section titled “The console”pw dev serves a browser console beside the application, on a fixed loopback
port, holding the panes the loop needs: project state, static assets, the
database and its declared queries, the template storybook, pw doctor, and the
telemetry viewer above.
pw dev: console http://127.0.0.1:18081[dev.console]enabled = trueport = 18081It is what the rest of the loop is read through, and none of it exists in a release build. See Development Console.
Test data endpoints
Section titled “Test data endpoints”The application pw dev builds carries the pwdev build tag, and in the
development environment that binary serves POST /_pw/test/seed/{dataset} and
GET /_pw/test/assert/{dataset} on its own listener, for loopback callers
only. A browser test suite uses them to reset and verify the database through
the same testdata/seed files pw seed reads. A release build carries no
endpoint bytes. See E2E Testing.
Stopping
Section titled “Stopping”Ctrl-C cancels the whole loop, stopping the application, Tailwind watcher, and
Devbox services. That is the only thing that ends it. An application that exits
on its own — a compile error, a panic, a clean return — is reported as
application exited: … and the loop keeps watching, because a project spends
most of the time between two working states in a state that does not run. The
next change you save rebuilds and restarts it.
