Deployment
A built Fluixi app is a runtime-neutral fetch handler: (Request) => Promise<Response>.
Everything platform-specific is an adapter around that one function.
fluixi build → dist/client (assets, prerendered pages)
dist/server (the handler)
fluixi start → serves it with the node adapter
The three shapes
A static site. With prerender every route is written to
dist/client as HTML. Upload that directory anywhere — there is no server to run.
A Node server. fluixi start runs the node adapter: it listens on a port and serves
dist/client from disk, falling through to the handler for anything not found. This is the
default and needs no configuration.
An edge or serverless runtime. Those platforms do not want a listening process; they
want a module exporting fetch. That is what the web adapter returns:
import { createProdHandler, webAdapter } from '@fluixi/start';
const handler = await createProdHandler(/* … */);
export default webAdapter.serve({ handler, cfg, clientDir });
// → { fetch: (request) => Promise<Response> }
The platform serves the static assets itself, usually from its CDN, so the adapter only has to hand over the handler.
Naming the target
Declaring the adapter in fluixi.config.ts is what tells the build where the app is
going:
import { defineConfig } from '@fluixi/start/config';
import { nodeAdapter } from '@fluixi/start/adapter';
export default defineConfig({
adapter: nodeAdapter,
});
fluixi start serves with that adapter too — with webAdapter it has nothing to run, and
says so instead of pretending to have started.
How the server gets bundled
The two targets want opposite things from dist/server, so the adapter carries a bundle
mode and fluixi build follows it:
bundle |
dist/server |
needs node_modules at runtime |
|---|---|---|
'external' (node) |
your app plus import '@fluixi/core' |
yes |
'inline' (web) |
one self-contained file | no |
| unset (no adapter) | Vite's own heuristic | usually |
A long-running Node process runs from the app directory, where the dependencies are already
installed — copying the framework into the bundle only makes the build slower. On the
examples/start-app app that is dist/server/entry-server.js at 4.6 KB instead of
76 KB — the app's own code and nothing else — with the whole build dropping from 1.9 s to
0.8 s. The rendered HTML is byte-identical either way.
An edge or serverless runtime is the opposite case: you upload a file, not an install, so
nothing may be left to resolve — 'inline' puts everything in the bundle.
'external' externalizes the @fluixi/* packages your app declares as dependencies,
and nothing else. Under a strict node_modules layout (pnpm) a package you never declared
is not resolvable from your app root, so externalizing one would produce a bundle importing
something Node cannot find. Each package is listed with every subpath it exports, because
half-externalizing one — @fluixi/core an import, @fluixi/core/router-next copied into the
bundle — would put two routers with two sets of module state in the same server.
ssrNoExternal still wins over all of this — it is the app saying "bundle this one anyway".
Only the build reads
bundle. Dev never externalizes:fluixi devloads server modules through Vite so that editing them still triggers HMR.
The hosted platforms
Three adapters emit the layout their platform reads. Each inlines the framework and
returns a fetch handler; what differs is where the entry goes and what declares the
routing.
import { cloudflareAdapter } from '@fluixi/start/adapter';
// or netlifyAdapter, vercelAdapter
export default defineConfig({ adapter: cloudflareAdapter });
what fluixi build writes |
what you deploy | |
|---|---|---|
cloudflareAdapter |
dist/client/_worker.js + _routes.json |
publish dist/client |
netlifyAdapter |
.netlify/functions-internal/fluixi-server.mjs |
publish dist/client |
vercelAdapter |
.vercel/output/ (Build Output API v3) |
nothing — Vercel reads it directly |
Each one serves static files first and only reaches the server for a miss: Cloudflare
through the ASSETS binding, Netlify through preferStatic, Vercel through a
filesystem route ahead of the function. A prerendered page is a file, so it stays one.
The entry is generated and bundled into a single file — no chunks. On Cloudflare that is not a size choice: the worker is written into the directory you publish, so a split chunk would be a publicly downloadable piece of your server.
An app with no server entry (a fully prerendered SPA) gets the static output alone, which is not a failure — there is nothing to wrap.
Writing an adapter
An adapter is a name, a serve function, and optionally the bundle mode its platform needs:
import type { Adapter } from '@fluixi/start';
export const myAdapter: Adapter = {
name: 'my-platform',
bundle: 'inline',
serve({ handler, cfg, clientDir }) {
// Either take over the process — listen, block, never return —
// or return { fetch } for the platform to invoke.
return { fetch: handler };
},
};
serve receives the handler, the resolved config and the built client directory. A
filesystem runtime uses clientDir to serve assets; an edge runtime ignores it because the
platform already does.
An adapter can also emit the platform's output layout, through an optional build
hook that runs after the client build, the server build and prerendering:
export const myAdapter: Adapter = {
name: 'my-platform',
bundle: 'inline',
serve: ({ handler }) => ({ fetch: handler }),
async build({ clientDir, bundleEntry }) {
await bundleEntry(myEntrySource, `${clientDir}/_worker.js`);
},
};
bundleEntry inlines the server bundle and the HTML template into one file. Both have
to be inlined rather than read: createProdHandler reads index.html off disk and
imports the server entry by path, which is exactly what a worker cannot do — so the
generated entry imports them statically and calls createHandlerFrom instead. Same
handler, same dispatch order, no filesystem.
What to deploy
| Path | What it is | Needed at runtime |
|---|---|---|
dist/client |
assets, prerendered HTML | yes — by the server or a CDN |
dist/server |
the fetch handler | only for SSR |
A fully prerendered site needs dist/client alone. With an 'external' build, dist/server
is not enough on its own — ship package.json and install dependencies where it runs.
Before you deploy
prerenderrequiresssr: true. Prerendering is server rendering moved to build time; with SSR off there is nothing to render with.- Middleware does not run for prerendered pages. They are files. A per-request check — auth, geolocation — has to live somewhere that actually runs per request.
- Check the version stamps. The mount element carries
fluixi,fx-domandfx-reactive, andwindow.Fluixireports the same. If they disagree in a deployed build, the install resolved two copies — worth catching before it becomes a bug report.
Next: Dependency injection.