Vite plugin
Run your frontend and the concile backend with one vite command, on one origin.
Usually, working with a Vite app and a concile backend means juggling two separate terminals: one
for vite and another for concile dev. You also have to set up a custom proxy just so your
browser can talk to both. But with @concile/vite, you can collapse all of that into a single handy
command! Just run vite, and our plugin automatically fires up the backend right alongside it, all
on the same browser origin.
Having everything on one origin is actually a bigger deal than just saving you a terminal window.
Since the client connects to ${location.host}/api/sync, running the engine on the exact same host
and port as your frontend means you can completely skip writing proxy configurations and forget
about tricky CORS issues. It really just works right out of the box!
You'll find two helpful modes available through a simple concile() call:
- Proxy mode (the default option) spins up
concile devas a child process and automatically hooks up Vite's proxy to it. Your backend runs exactly as it would if you started it manually. - Embed mode actually boots up the engine right inside Vite's own process. Because there is no child process and no proxy hop, the engine acts as seamless Vite middleware, and the reactive WebSocket easily shares Vite's HTTP server.
Set up
Install the plugin
@concile/vite goes right next to your existing dev dependencies. It needs vite 5 or newer, and
it drives @concile/cli (which is already a dev dependency if you followed the
quickstart):
npm install -D @concile/vitepnpm add -D @concile/vitebun add -d @concile/viteAdd it to vite.config.ts
import { defineConfig } from "vite";
import { concile } from "@concile/vite";
export default defineConfig({
plugins: [concile()],
});That's the entire configuration. You do not need to add any options.
Run vite
npx viteThe backend starts up right along with the dev server. You will see its log lines appear in the same
terminal, prefixed with [concile]. Open your app at the Vite URL, and you'll find the engine's
surfaces are there too, all on the very 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, totally untouched.
The two modes
Proxy mode spawns concile dev as a child process on a free port and injects three entries right
into Vite's dev-server proxy: /api (which is WebSocket-aware, so /api/sync upgrades pass through
seamlessly), /_dashboard, and /_admin. Vite merges these with any server.proxy entries of your
own, ensuring your unrelated proxy rules are kept perfectly intact.
Since the child process is the real concile dev, everything on the local
development page works exactly as described. Codegen runs on start, the
child watches the concile/ directory and hot-reloads your functions on every save, and the
ephemeral admin key is printed in its piped output.
Here are a few handy mechanics you should definitely keep in mind:
- CLI resolution. The plugin runs your app's local
node_modules/.bin/concileif it exists, and falls back tonpx concileotherwise. You can override this behavior with thecommandoption. - Readiness. Vite patiently waits for the backend to accept TCP connections before serving. It
polls every 200ms with a 30-second timeout. If the child process exits early or never comes up,
vitefails with a clear error instead of serving a dead proxy. - Cleanup. The child process gets killed when the Vite server closes and on
SIGINT,SIGTERM, or process exit. This means a stoppedvitewill never leave an orphaned backend behind. - Extra flags. Anything that
concile devaccepts can be easily forwarded throughargs. For example, you can passargs: ["--database-url", "postgres://..."].
Embed mode is entirely opt-in:
export default defineConfig({
plugins: [concile({ mode: "embed" })],
});Instead of spawning a child process, the plugin boots the engine right inside Vite's process. It
uses @concile/cli's shared boot core, reached via a dynamic import. If @concile/cli isn't
installed, embed mode will fail at startup and give you a clear, helpful message. The engine paths
are served as connect middleware, and /api/sync gets its own WebSocket upgrade listener that
coexists nicely with Vite's HMR socket on the same server. There is no second process and absolutely
no proxy hop.
Startup writes to concile/_generated/ and logs the admin key:
[concile] embed → engine in-process on Vite's origin (admin key: ...)The database is SQLite and lives at <project root>/.concile/dev.db by default. You can easily
point dataPath somewhere else, or set databaseUrl to a postgres:// connection string to use
Postgres instead. The admin key is a fresh ephemeral key generated per run
unless you explicitly provide an adminKey.
The component set (your concile.config.ts composition) is fixed at boot, which is exactly how
concile dev and concile serve behave. If you want to add or remove a component, you just need to
restart vite.
A different database file than concile dev
Embed mode's default SQLite path is .concile/dev.db, while concile dev (and proxy mode by
extension) defaults to .concile/data.db. If you switch modes, you will be looking at a different,
initially empty database. If you want the exact same data in both, you can point dataPath for
embed mode and --data for proxy mode (via args) to the same file. Just be careful never to run
both modes against it at once, as the engine is strictly single-writer.
Options
All options are completely optional. mode and functionsDir apply to both modes, while the rest
are specific to one mode and simply ignored by the other.
Prop
Type
Hot reload and HMR
Two independent reload loops run side by side, and neither steps on the toes of the other:
- Frontend HMR is Vite's, completely unchanged. Editing a component hot-swaps it in the browser exactly as you would expect in any Vite app.
- Backend hot reload is concile's. Saving a file under
concile/reloads your schema and functions and regeneratesconcile/_generated/, without ever dropping your live subscriptions. A client watching a query stays happily connected and re-runs against the new code on the very next invalidation. A broken save simply logs✗ reload failedand keeps your previous working functions running in the meantime.
In proxy mode, the child process's own file watcher handles this, exactly as described under local
development. In embed mode, the plugin subscribes to Vite's own
file watcher instead. Any changes under functionsDir (except for _generated/) are debounced for
50ms and then re-pushed in-process.
Regenerated _generated/ files are treated as ordinary source files as far as Vite is concerned.
This means a codegen change that your frontend imports, like a new function on api, flows right
into the browser through Vite's normal HMR system.
Limits
- The plugin is strictly for development. It only shapes
vite's dev server. Runningvite buildproduces your frontend just like always, and you will 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 here. You should use proxy mode or an S3-backed presigned upload for big files. - Embed mode needs Vite's own HTTP server. If you use
server.middlewareMode(which is how some SSR frameworks host Vite), there is no server to attach to. This means the/api/syncWebSocket never wires up and engine cleanup doesn't run. The plugin will warn you loudly in this case, so stick to proxy mode there. - Proxy mode is two processes. This is completely by design to ensure zero divergence from the
real
concile dev, but it means backend state lives in the child process. If you ever need to poke the engine directly from Vite's process, that's exactly what embed mode is for.
Related
- Local development: everything the backend does under the plugin, including flags, hot reload, and the dashboard.
- Quickstart: install concile and see the whole loop run end to end.