import { pageMeta } from "../../meta" import { CodeBlock } from "../../highlight" // Pure content page — no React interactivity (TOC/copy/search are the layout enhancer + // the Nira island), so ship zero framework JS and avoid hydrating the inline-script DOM. export const hydrate = false export const meta = pageMeta( "Nifra — Deployment", "One Nifra app, one command per target — Bun, Node (Docker), Deno Deploy, Cloudflare Pages, Vercel Edge.", ) const SCAFFOLD = `# Scaffold the multi-target site with a chosen default deploy target: bun create nifra my-app --deploy vercel # or: bun | node | deno | cf-pages # add --framework to pick the UI too (react default): bun create nifra my-app --framework svelte --deploy vercel cd my-app && bun install bun run dev # local preview bun run build # builds for the chosen target bun run deploy # runs that target's deploy CLI (you stay logged-in to the vendor) # Or add --ci github to also emit a deploy-on-push GitHub Actions workflow: bun create nifra my-app --deploy cf-pages --ci github # → .github/workflows/deploy.yml (builds on every push/PR, deploys on push to main)` const CF = `// _worker.ts — the edge SSR entry import { toFetchHandler } from "@nifrajs/core" import { createWebApp } from "@nifrajs/web" import { reactAdapter } from "@nifrajs/web-react" import { clientEntry, manifest } from "./server-manifest" // generated by buildServer const app = createWebApp({ adapter: reactAdapter, manifest, clientEntry }) export default toFetchHandler(app) // a Workers/Pages fetch handler // build.ts — buildClient (→ /assets) + buildServer (edge conditions → _worker.js) import { buildClient, buildServer } from "@nifrajs/web/build"` export default function Deployment() { return (
app.fetch(request) is a pure Web-standard handler, so the same app runs
anywhere. Pick a runtime; the code doesn't change.
create-nifra's site template ships every target's build + server entry
and config. Pass --deploy <target> to make one the default — it points{" "}
build/deploy at that target and fills in your project name. The
per-target build:*/deploy:* scripts stay, so you can switch any time.
| target | build → | deploy | config scaffolded |
|---|---|---|---|
| Bun (flagship) |
dist-bun/
|
bun run start (any host)
|
— |
| Node |
dist-node/
|
docker build … && docker run
|
Dockerfile + .dockerignore
|
| Deno Deploy |
dist-deno/
|
deployctl deploy
|
deno.json
|
| Cloudflare Pages |
dist/
|
wrangler pages deploy
|
wrangler.toml
|
| Vercel Edge |
.vercel/output/
|
vercel deploy --prebuilt
|
Build Output API v3 |
Nifra never runs the deploy or enters your cloud credentials — it scaffolds the config + a{" "}
deploy script that shells out to the vendor CLI you've already authed.
Add --ci github to emit a .github/workflows/deploy.yml tuned to the
chosen target — it builds on every push/PR and deploys on a push to main.{" "}
cf-pages uses cloudflare/wrangler-action, vercel the prebuilt{" "}
vercel deploy, deno deployctl (OIDC). The workflow's header
comment lists the exact repo secrets to set (e.g. CLOUDFLARE_API_TOKEN,{" "}
VERCEL_TOKEN). Self-hosted bun/node have no universal push-to-deploy,
so their workflow builds + uploads the bundle as an artifact and leaves a clearly-marked,
host-specific deploy step for you to fill in (Fly, a registry push, SSH, …).
app.listen(3000) — the native Bun server. Sits at the raw Bun.serve{" "}
ceiling (see benchmarks).
Every core: Bun is single-threaded per process — for multi-core boxes,
spawn one process per core, each binding the same port with{" "}
app.listen(PORT, {"{ reusePort: true }"}); the kernel load-balances connections
across them (Linux balances ~evenly). A supervisor that spawns + restarts workers is ~20
lines — see examples/cluster.ts. Anything shared across workers (rate limits,
sessions, pub/sub) needs a shared store, as in any multi-instance deploy.
@nifrajs/node — serve(app, {"{ port }"}) bridges to node:http.
@nifrajs/deno — serves app.fetch on Deno.serve.
Self-hosting (no CDN in front)? Hand @nifrajs/node's serve a{" "}
static mount and it serves the client bundle from disk — traversal-guarded,
content-typed, with an immutable cache — before the app runs, leaving the SSR fast path
untouched: serve(app, {'{ port, static: { dir: new URL("./assets/", import.meta.url) } }'}).
On Cloudflare/Vercel the platform serves assets, so you don't need it there.
buildServer bundles with edge conditions (workerd,{" "}
edge-light) into a _worker.js; toFetchHandler(app) is
the handler. For Pages, a _routes.json serves the client bundle statically.
SSR verified on real workerd — this very site runs there.
Deploy: wrangler pages deploy dist (Pages) or wrangler deploy{" "}
(Workers). Deno Deploy and Vercel Edge reuse the same build with their conditions.