Point0by 1gr14
  • Меню
    • GitHub
    • Блог
  • Introduction
    • Overview
    • Getting Started
    • Full Overview
    • Benchmarks
    • Points
  • Points
    • Page
    • Layout
    • Component
    • Provider
    • Mountable
    • Query
    • Infinite Query
    • Mutation
    • Action
    • Root
    • Base
    • Plugin
  • Methods
    • Validation
    • Loader
    • Context
    • Middleware
    • Loading & error
    • .with
    • Mapper
    • Transformer
    • Stage Methods
  • Core
    • Navigation
    • SSR
    • RSC
    • Request
    • Response
    • Error handling
    • Env
    • Head
    • MDX
    • Assets
    • File upload
    • OpenAPI
    • Query client
    • Events
    • Infer
  • Engine
    • Engine Config
    • Engine Runtime
    • CLI
    • Dev
    • Build
    • Compiler
    • Generator
    • Project MCP
    • Docs MCP
    • Importer
    • Public dir
    • Testing
    • Deploy
    • Bun or Vite
  • Extra
    • SsrStore
    • CookieStore
    • Basic Auth
    • CORS
    • Cache-Control
    • Compress
  • Examples
    • Basic
    • Vite
    • Better Auth
    • Capacitor
    • Expo
  • 1gr14
    • Главная
    • Start0
    • Поддержать
    • Обучение
    • Группа
    • Блог
    • Автор
  • Сообщество
    • Discord
    • Telegram
  • Опенсорс
    • Point0
    • Route0
    • Error0
    • Flat
  • Аккаунт
    • Войти
    • Регистрация
Point0by 1gr14
Создаю опенсорс во славу Господа Иисуса Христа ☦️
С любовью ко всем разработчикам на свете ❤️
Условия использованияПолитика конфиденциальностиСергей Дмитриев 2026 😎

Dev

  • Раздел: Engine

On this page

  1. What a dev run is
  2. Two ports in dev, one in prod — and no CORS
  3. Server restarts vs client restarts
  4. Server hot mode (--hot)
  5. Why not bun --hot
  6. How it works: the content-addressed store
  7. Hot vs cold
  8. What hot-swaps and what doesn't
  9. Crash resilience
  10. The HMR fix: export anything from a point file
  11. The dev lifecycle: no orphans
  12. Graceful teardown
  13. Ports are named, never killed
  14. Stability under rapid edits
  15. Reference
  16. CLI flags
  17. engine.dev() options
  18. Env vars (dev tuning)
  19. Related

point0 dev starts your whole app for development: the server, the client dev servers, file watching, and codegen on the fly. It's a thin wrapper over engine.dev() — the CLI parses flags and calls it. The canonical basic example wires it as:

// package.json
"scripts": {
  "dev": "point0 dev --hot"
}
bun run dev
# regenerates points/routes, starts the client dev server, boots the server,
# and watches src — edit a page and it hot-swaps without restarting the server.

What a dev run is

A dev run is a small process tree:

point0 dev                 # the orchestrator (the engine.dev() process)
├─ server child            # a `bun run` of your server entry (or in-process for Vite)
└─ client dev server(s)    # one per client scope, in the orchestrator process

The orchestrator owns the file watcher. The client dev servers always run inside the orchestrator process as a single instance — they own HMR / Fast Refresh and are never restarted by an edit. The server runs as a child process (for the bun-native pipeline) and is the thing that restarts.

engine.dev() does this in order:

await engine.dev()
// 1. apply the app logger
// 2. install dev shutdown handlers (SIGINT/SIGTERM/SIGHUP)
// 3. generate points/routes once, then watch and regenerate on change
// 4. start the client dev server(s) in this process
// 5. start the server child (bun-native) or in-process Vite server

It defaults NODE_ENV to development when unset; an explicit --mode / NODE_ENV is preserved.

Two ports in dev, one in prod — and no CORS

In production the server and the client are served from one origin on a single port: the same server that renders pages also answers queries and mutations. In development point0 splits them across two ports on purpose — a dedicated client dev server (one per client scope) and the server — so the client dev server can stay alive and never lose its HMR / Fast Refresh state when the server restarts.

That split would normally mean cross-origin requests, but point0 keeps everything same-origin for the browser:

  • The client dev server is the only origin the browser talks to. It serves the page and its assets, and it forwards every other request through to the server — including the query/mutation POSTs your code fires. Each forwarded request is tagged x-point0-forwarded-from-dev-client so the server answers it instead of bouncing it back. So there are no CORS preflights and no @point0/cors needed in dev. The proxy is byte-transparent, too — a Content-Encoding your server sets (a gzip/brotli compression middleware) rides through untouched, so origin compression behaves in dev exactly as in prod.
  • Hit the server port directly and dev redirects you to the client. A page URL opened on the server's port 302s to the matching client dev server port (same path), so the browser lands on the origin that owns HMR. (Production does no such redirect.)

If you turn the client side off (point0 dev --side server), there's no client dev server to forward through — the browser hits the server cross-origin, so add the @point0/cors middleware yourself, as for any external client.

Server restarts vs client restarts

The client never restarts on an edit. The client dev server owns HMR (Bun's HTMLBundle bundler, or Vite when configured). It stays alive for the whole session and patches modules in place. Because it's also the front door the browser hits and proxies everything through to the server (see above), even a full server restart is invisible in the browser — point0 retries connection-refused quietly across the brief moment the server re-binds its port, so the client proxy doesn't 502.

The server, by default, restarts on every change. Without --hot there is no true hot reload: any watched change kills the server child and respawns it. That's slow and drops all in-memory state — DB pools, caches, the open socket:

point0 dev
# edit any server file →
# Server restarting... (changed: src/pages/home.tsx)
# Server started http://localhost:3000 in 412ms

The child runs as a plain bun run (never bun --watch) — the orchestrator's own import-graph watcher is the sole watcher and owns every restart. Bursts of edits coalesce into one respawn (a 120 ms settle window), and a respawn is polite: SIGTERM first, a grace window for your shutdown hooks, then SIGKILL for stragglers.

To keep server state across edits, turn on hot mode.

Server hot mode (--hot)

--hot keeps the server process stable — React, react-dom, the engine, Prisma all load once and never reload — and picks up changes by re-importing just the edited points with a fresh module identity. Edit a page and the SSR output updates with no restart; the process pid stays the same:

point0 dev --hot
# edit src/pages/home.tsx →
# Server hot reloading... (changed: src/pages/home.tsx)
# Server hot reloaded
# (no "Server started" — same process, DB pool and caches intact)

Enable it three ways — the flag, the dev() option, or an env var:

point0 dev --hot                          # CLI flag
await engine.dev({ serverHot: true }) // programmatic
POINT0_DEV_SERVER_HOT=true point0 dev     # env-var fallback

The explicit option wins; otherwise it falls back to the env var. Off by default. Bun-native only — under Vite the dev path is unchanged. Experimental.

Why not bun --hot

There are two reasons; the second is decisive.

First, bun --hot does a partial module-graph reload, which tears React's cross-module dispatcher singleton (Invalid hook call / more than one copy of React) because react-dom/server lives in the unwatched framework dist and doesn't reload with your edited component. Reproduced on the real app, and not fixable with accept-boundaries.

Second, the files point0 rewrites on the fly (the content-hashed modules it feeds the server, see below) never land in Bun's own hot store, so native Bun hot reload simply doesn't fire for them. It's an open Bun limitation (oven-sh/bun#5844 — filed against --watch, but --hot is affected the same way).

point0's hot mode sidesteps both: it never reloads the framework and manages its own module store.

How it works: the content-addressed store

In hot mode the orchestrator compiles each of your source files, writes it under a content-hash filename, and rewrites relative import specifiers to its deps' hashed names. Bare specifiers (react, @prisma/*, @point0/*) are left alone, so they resolve to the same cached singletons the renderer already uses.

node_modules/.cache/server-hot/<scope>-<port>/

The store lives there, keyed by <scope>-<port>, so two --hot processes on the same folder but different ports can't clobber each other. On a change, only the changed file and its importer chain get new hashes; everything else keeps its hash. The server child re-imports the current points aggregator per request, gated by a manifest hash: unchanged → cached module (singletons live); changed → fresh module identity. (point0 prune deletes this cache.)

Hot vs cold

Not every file is safe to hot-swap — a file with boot side effects (crons, queues, the DB client) needs a full restart. point0 classifies default-hot:

  • Every server file is hot by default — editing it hot-swaps.
  • Cold roots restart on edit. Three sources:
    1. The in-file marker — this file and its static-import subtree are cold:
      import '@point0/core/cold' // editing this file (or its static deps) restarts the server
    2. A config glob on the server importer:
      // engine.ts (server config)
      importer: {
        cold: ['**/prisma.*']
      } // matched against the file's own path
    3. Auto — a file the store provably can't flatten (a location-relative import.meta, or an un-rewritable relative import like a generated client's ./enums dir or wasm). These are logged at startup: N file(s) can't be flattened, running cold (edit => restart): ….
  • The server entry is cold by default — it is boot.

Cold flows downward through static imports and stops at a lazy import() (the hot boundary). Importers of a cold file stay hot: a page importing prisma (cold) still hot-swaps; prisma just isn't re-evaluated, because its content hash is unchanged. Prisma forces a minimal cold boundary even on day one — its generated client (a directory + wasm + import.meta) can't be flattened, so it's externalized to its real path and runs cold.

The decision per change: changed file is cold → full restart; otherwise → hot-swap.

What hot-swaps and what doesn't

Editing an existing file behaves as you'd expect:

ChangeResult
Edit a page / layout / componenthot-swap (pid stable)
Edit a mutation / query / loaderhot-swap (next call returns the edited value)
Edit a server loader, shared lib, or .mdx page bodyhot-swap
Delete a fileroute drops, tree stays alive
Rename a fileroute follows the new file
Edit a @point0/core/cold file, the boot entry, or app.clientfull restart

Two MVP cuts to know:

  • Adding a brand-new file restarts. The new point is picked up automatically — the generator regenerates the aggregators — but it comes in via a child restart, not a hot-swap.
  • The client app shell (app.client) is not in the store — editing it restarts.

Crash resilience

--hot is built so a typo never forces you to re-run point0 dev:

  • A hot-file compile error is caught and the previous store is kept: Hot reload failed — keeping the previous store (fix the error and save again). Fix and save → it recovers.
  • A cold/boot syntax error prints the error and leaves the tree alive; the child exits, and the next save respawns it.
  • Starting --hot on an already-broken hot file: the store builds, the child crashes importing it before binding the port, and that never-booted child is dropped without tearing down the tree. The next save respawns it: Server failed to boot (entry "...", code N) — fix the error and save; it will start automatically.
  • An initial store build failure defers the server child entirely; the watcher retries the build on each save and starts the server the moment it succeeds.

To bound memory across a long session, the orchestrator periodically restarts instead of hot-swapping (every 200th hot reload by default) to release Bun's module cache; the store dir is mark-and-swept on disk.

The HMR fix: export anything from a point file

A point file exports points — objects and method-decorated functions, not React components. Bun's bundler and Vite only enable React Fast Refresh for a module that looks like it exports a component; a plain points file wouldn't qualify, and every edit would trigger a full page reload instead of HMR.

point0 fixes this in the compiler. It appends a decoy component-shaped function to the final point in the chain:

// what the compiler appends to the last point in a file
._tail(function X() { return null })

That statically-declared, capitalized function X is all the bundler's static pass needs to wire up Fast Refresh. At runtime _tail returns the real thing — a mountable point's actual mount component, or the decoy decorated with the point's methods for everything else. The inline render function of a page/layout/component is also hoisted to a top-level declaration so Fast Refresh tracks edits to the render body.

The payoff: a single file can export a page, a query, a mutation, a provider, and plain values all at once, and HMR keeps working.

// one file, mixed exports — HMR survives all of them
export const ideaQuery = root.lets
  .query() /* ... */
  .query()
export const likeIdea = root.lets
  .mutation() /* ... */
  .mutation()
export const ideaPage = root.lets
  .page('/ideas/:id') /* ... */
  .page(/* ... */)

The fix is on by default for the client side and off for the server side. It's not an engine config option — you can only override it per compile through the point0 compile CLI (-h/--hmr, -H/--no-hmr).

The dev lifecycle: no orphans

A dev tree cleans up after itself. The invariant:

No point0 process can outlive whatever launched it.

This is enforced at the kernel level, not by bookkeeping. The CLI passes --no-orphans (Bun >= 1.3.14) explicitly on every dev child it spawns — the server child and the client dev servers (the point0 shebang itself stays flag-free so the bin launches through Bun's Windows shim).

A flagged process exits when its parent dies — even on SIGKILL — and on its own exit SIGKILLs every descendant (Bun re-verifies parentage first, so recycled PIDs are safe). The flag is inherited by nested bun processes, so each child's own spawns are covered automatically. Each app's bunfig.toml extends the invariant one level up, to the bun run dev wrapper itself:

# bunfig.toml
[run]
noOrphans = true

Verified empirically (bun 1.3.14, macOS): SIGKILL the orchestrator and both children are gone, ports free, in ~2s. Because a tree can't leak, there's nothing to find, list, or stop from outside — so point0 has no dev lockfile and no point0 stop command. Stopping a dev run is Ctrl-C, or killing the terminal.

Graceful teardown

--no-orphans SIGKILLs. For a polite shutdown — so your DB pool closes and your job queue drains — engine.dev() installs SIGINT/SIGTERM/SIGHUP handlers that SIGTERM every child first, wait a grace window (POINT0_DEV_SHUTDOWN_GRACE_MS, default 5000 ms), then SIGKILL stragglers. The same path runs if a core child dies unexpectedly — the tree lives and dies as one unit. Ctrl-C stays graceful end-to-end because bun run waits for its child after forwarding SIGINT.

Ports are named, never killed

point0 never kills whoever holds a port. Since trees can't leak, a busy port always means a live process someone owns. A real conflict fails with an error that names the holder, and you decide:

Port 3000 is already in use by pid 123 (bun src/index.server.ts).
Stop that process or change the port.

Under the orchestrator the server child binds with patient retries (POINT0_DEV_BIND_TIMEOUT_MS, default 10000 ms) — because a transient conflict during a respawn is just the predecessor draining its shutdown hooks. Any other dev run binds once.

Windows caveat: --no-orphans is a no-op on Windows, so a SIGKILLed tree can leave children behind. The port-conflict error then names them for a manual kill.

Stability under rapid edits

point0 dev and --hot survive fast edit bursts — the way an AI agent saves, 30–150 ms apart, written atomically (<file>.tmp.<pid>.<hex> + rename). The invariants:

  • One watcher pipeline, strictly serial. Events are deduped by path (latest wins) and drained one at a time; the watcher never runs concurrently with itself.
  • Junk never enters. Atomic-write and editor artifacts (*.tmp.<pid>.<hex>, *.tmp, *.vsctmp, *.crswap, vim/JetBrains/emacs swap files, ~, .DS_Store, …) are dropped at the watcher source, so every consumer — dev restarts, the generator, build --watch — is immune.
  • At most one respawn in flight, one queued. A burst settles (POINT0_DEV_RESTART_SETTLE_MS, default 120 ms) into a single respawn; respawns chain, never run in parallel.
  • A hot edit never kills a live child — an alive-but-booting child plus a hot-node change rebuilds the store only; the child serves the latest code once it's up.

While the server is still booting, the watch set is broad (the whole app-src tree), because the precise import walk can't yet be trusted to include the file that fixes a broken build. Once the server is confirmed up, it narrows to the precise import graph.

Reference

CLI flags

Every flag on point0 dev (each maps to an engine.dev() option). Full CLI reference on CLI.

FlagDefaultWhat it does
--hotoffServer-side hot reload (bun-native). Hot-swap edited points without restarting; cold files still restart. Experimental.
-G, --no-generategenerate onSkip files generation.
--side <server|client>bothServe only one side.
--scope <scope>allServe only one scope.
-w, --watch [glob]engine devWatchGlobWatch files and rebuild on change (comma-separated or repeated; no value = the engine's devWatchGlob).
-W, --no-watchwatch onDisable file watching (no restart / regenerate on change).
--entry <name|path>all server entriesServer entry points, by name or path (comma-separated or repeated).
--engine <path>auto-findPath to the engine file.
--env <name=value>noneDefine env vars (override .env); repeatable.
--mode <mode>developmentproduction | development | test — which .env files apply.
-- <args>noneEverything after -- is forwarded to the spawned bun run.

engine.dev() options

await engine.dev({
  generateFiles, // boolean — run codegen first + watch (default true)
  side, // 'server' | 'client' — one side only
  scope, // a single points scope
  entries, // string[] — server entry names or paths
  bunRunArgs, // string[] — extra args for the server child's `bun run`
  watch, // string | string[] | boolean — watch globs, or true/false
  cwd, // string — defaults to process.cwd()
  serverHot, // boolean — hot mode; defaults to POINT0_DEV_SERVER_HOT
})

Env vars (dev tuning)

Env varDefaultEffect
POINT0_DEV_SERVER_HOTfalseEnable server hot mode (fallback when --hot / serverHot is omitted).
POINT0_DEV_BIND_TIMEOUT_MS10000Server child's port-bind retry window under the orchestrator.
POINT0_DEV_SHUTDOWN_GRACE_MS5000Graceful teardown grace window before SIGKILL.
POINT0_DEV_RESTART_SETTLE_MS120Burst-coalescing settle delay before a respawn.
POINT0_DEV_RESTART_GRACE_MS1500Per-respawn SIGTERM → SIGKILL grace.
POINT0_DEV_SERVER_HOT_RESTART_EVERY200Restart instead of hot-swap every Nth reload, to release Bun's module cache (0 = off).
POINT0_DEV_SERVER_HOT_GC_GRACE_MS30000Disk-GC grace before sweeping stale store files.

POINT0_DEV_CHILD and POINT0_DEV_STORE_DIR are set internally by the orchestrator on the server child (they mark it as orchestrator-owned and hand it the hot store) — you don't set them.

Related

  • CLI — point0 dev is a wrapper over engine.dev(); see also generate, build, compile, prune.
  • Build — the production counterpart, point0 build.
  • Engine runtime — engine.dev() alongside the other engine methods.
  • Bun vs Vite — the two dev pipelines (hot mode is bun-native only).
  • Compiler — where the HMR fix and the -h/--hmr override live.
НазадCLIДалееBuild

Enjoying Point0?

Give it a star on GitHub, ask questions, and follow what's new

Star Point0

A star on GitHub helps more developers find Point0
Star on GitHub

Start0

Point0 integrated into the Start0 SaaS boilerplate — the best way to use it
Explore Start0

YouTube

Tutorials, walkthroughs, and updates
YouTube

Discord

Questions and chat in English
Join Discord

Telegram

News channel and chat in Russian
ChannelChat

Twitter

Posts and updates
Twitter