Vite plugin
Run your frontend and the helipod backend with one vite command, on one origin.
Normally a Vite app and a helipod backend are two terminals: vite in one, helipod dev in the
other, plus a hand-written proxy so the browser can reach both. @helipod/vite collapses that to
one command. You run vite, and the plugin brings the backend up with it, on the same browser
origin.
One origin matters more than one terminal. The client connects to ${location.host}/api/sync, so
when the engine answers on the same host and port as your frontend, there is no proxy config to
write and no CORS to think about. It just works.
The plugin has two modes behind the same helipod() call:
- Proxy mode (the default) spawns
helipod devas a child process and wires Vite's proxy to it. The backend behaves exactly as if you had started it yourself. - Embed mode boots the engine inside Vite's own process. No child process, no proxy hop: the engine answers as Vite middleware, and the reactive WebSocket shares Vite's HTTP server.
Set up
Install the plugin
@helipod/vite goes next to your existing dev dependencies. It needs vite 5 or newer, and it
drives @helipod/cli (already a dev dependency if you followed the
quickstart):
npm install -D @helipod/vitepnpm add -D @helipod/vitebun add -d @helipod/viteAdd it to vite.config.ts
import { defineConfig } from "vite";
import { helipod } from "@helipod/vite";
export default defineConfig({
plugins: [helipod()],
});That's the whole configuration. No options are required.
Run vite
npx viteThe backend starts with the dev server. Its log lines appear in the same terminal, prefixed
[helipod]. Open your app at the Vite URL, and the engine's surfaces are there too, on the same
origin:
/api/*is the engine's HTTP API, and/api/syncis the reactive WebSocket./_dashboardis the dashboard./_admin/*is the admin API the dashboard runs on.
Everything else is your Vite app, untouched.
The two modes
Proxy mode spawns helipod dev as a child process on a free port and injects three entries into
Vite's dev-server proxy: /api (WebSocket-aware, so /api/sync upgrades pass through), /_dashboard,
and /_admin. Vite merges these with any server.proxy entries of your own, so your unrelated proxy
rules are preserved.
Because the child is the real helipod dev, everything on the
local development page holds verbatim: codegen runs on start, the child
watches helipod/ and hot-reloads your functions on every save, and the ephemeral admin key is
printed in its (piped) output.
Some mechanics worth knowing:
- CLI resolution. The plugin runs your app's local
node_modules/.bin/helipodif it exists, and falls back tonpx helipod. Override with thecommandoption. - Readiness. Vite waits for the backend to accept TCP connections before serving (polled every
200ms, 30 second timeout). If the child exits early or never comes up,
vitefails with a clear error instead of serving a dead proxy. - Cleanup. The child is killed when the Vite server closes and on
SIGINT/SIGTERM/process exit, so a stoppedvitenever leaves an orphaned backend behind. - Extra flags. Anything
helipod devaccepts can be forwarded throughargs, for exampleargs: ["--database-url", "postgres://..."].
Embed mode is opt-in:
export default defineConfig({
plugins: [helipod({ mode: "embed" })],
});Instead of spawning a child, the plugin boots the engine inside Vite's process, using
@helipod/cli's shared boot core (reached via a dynamic import; if @helipod/cli isn't
installed, embed mode fails at startup with a clear message). The engine paths are served as
connect middleware, and /api/sync gets its own WebSocket upgrade listener that coexists with
Vite's HMR socket on the same server. There is no second process and no proxy hop.
Startup writes helipod/_generated/ and logs the admin key:
[helipod] embed → engine in-process on Vite's origin (admin key: ...)The database is SQLite at <project root>/.helipod/dev.db by default. Point dataPath somewhere
else, or set databaseUrl to a postgres:// connection string to use
Postgres instead. The admin key is a fresh ephemeral key per run unless you
pass adminKey.
The component set (your helipod.config.ts composition) is fixed at boot, exactly as
helipod dev and helipod serve behave. Adding or removing a component means restarting
vite.
A different database file than helipod dev
Embed mode's default SQLite path is .helipod/dev.db, while helipod dev (and so proxy mode)
defaults to .helipod/data.db. Switch modes and you're looking at a different, initially empty
database. If you want the same data in both, point dataPath (embed) and --data (proxy, via
args) at the same file, but never run both modes against it at once: the engine is
single-writer.
Options
All options are optional. mode and functionsDir apply to both modes; the rest belong to one mode
and are ignored by the other.
Prop
Type
Hot reload and HMR
Two independent reload loops run side by side, and neither steps on the other:
- Frontend HMR is Vite's, unchanged. Editing a component hot-swaps it in the browser exactly as in any Vite app.
- Backend hot reload is helipod's. Saving a file under
helipod/reloads your schema and functions and regenerateshelipod/_generated/, without dropping live subscriptions. A client watching a query stays connected and re-runs against the new code on the next invalidation. A broken save logs✗ reload failedand keeps the previous working functions running.
In proxy mode, the child's own file watcher does this, exactly as described under
local development. In embed mode, the plugin subscribes to
Vite's own file watcher instead: changes under functionsDir (except _generated/) are debounced for
50ms and then re-pushed in-process.
Regenerated _generated/ files are ordinary source files as far as Vite is concerned, so a codegen
change that your frontend imports (a new function on api, say) flows into the browser through
Vite's normal HMR.
Limits
- The plugin is dev only. It shapes
vite's dev server and nothing else.vite buildproduces your frontend as usual, and you deploy the backend separately (self-hosting, deploy and build). - Embed mode caps request bodies at 5 MiB on the engine paths. A larger
file storage upload through
/api/storage/uploadwon't fit; use proxy mode, or an S3-backed presigned upload, for big files. - Embed mode needs Vite's own HTTP server. Under
server.middlewareMode(how some SSR frameworks host Vite) there is no server to attach to, so the/api/syncWebSocket never wires up and engine cleanup doesn't run. The plugin warns loudly; use proxy mode there. - Proxy mode is two processes. That's by design (zero divergence from the real
helipod dev), but it means backend state lives in the child. If you need to poke the engine from Vite's process, that's what embed mode is for.
Related
- Local development: everything the backend does under the plugin, flags, hot reload, and the dashboard.
- Quickstart: install helipod and see the whole loop end to end.