Upgrading from Nifra 1.x to 2.0
Nifra 2.0 removes the compatibility layer retained through 1.x and makes optional runtime systems explicit, instance-scoped plugins. The upgrade command handles deterministic package and import edits; this guide covers the structural changes it deliberately cannot guess.
1. Run the executable upgrade
nifra upgrade 2.0.0 # dry-run: inspect every planned edit
nifra upgrade 2.0.0 --write # apply edits, then run nifra check
bun install
bun run test
bun run buildThe command updates existing @nifrajs/*, Nifra, and create-nifra dependency ranges to 2.0.0 while preserving caret/tilde/exact style. It also replaces the removed @nifrajs/budget dependency with @nifrajs/core and moves its imports to @nifrajs/core/budget. Dry-run is the default; --write applies and verifies with nifra check.
2. Install optional server systems explicitly
Idempotency, the effect ledger, MCP declarations, SSE, WebSockets, and Node-direct resolution are no longer enabled by server options, side-effect imports, or process-global state. Install only the systems an app uses with .use(); registration fails loudly if a required plugin is missing.
// doc-check: skip - migration fragment uses the application's existing stores and handlers
// 1.x
import "@nifrajs/core/ws"
const app = server({ idempotencyStore, effectLedger: { sink } })
.ws("/chat", socketHandler)
// 2.0
import { effectLedger } from "@nifrajs/core/effect-ledger"
import { idempotency } from "@nifrajs/core/idempotency-plugin"
import { mcp } from "@nifrajs/core/mcp"
import { streaming } from "@nifrajs/core/sse"
import { websocket } from "@nifrajs/core/ws"
const app = server()
.use(idempotency({ store: idempotencyStore }))
.use(effectLedger({ sink }))
.use(mcp()) // only when the app declares tools/resources/prompts
.use(streaming()) // only when the app declares SSE routes
.use(websocket())
.ws("/chat", socketHandler)The @nifrajs/node adapter installs nodeDirect() automatically. Only applications calling app.resolveNode() directly need to install it.
3. Use the lean package entry points
The @nifrajs/core and Nifra roots now expose the lean HTTP server surface. Prefer @nifrajs/core/server for server(), and import contracts, assurance, capabilities, budgets, manifests, reflection, SSE, WebSockets, and other optional systems from their documented subpaths.
// doc-check: skip - before/after migration block includes the removed 1.x package
// 1.x
import { createRequestBudget } from "@nifrajs/budget"
// 2.0
import { createRequestBudget } from "@nifrajs/core/budget"Contract-generated adversarial testing lives in @nifrajs/testing; the deprecated core invariant runner is removed.
4. Update full-stack backend mounts
createWebApp now auto-mounts a backend only through the platform-aware, symbol-keyed BackendMount interface. The old duck-typed api.fetch(url, init) convention is removed. Normal applications should pass the result of inProcessClient(backend) or testClient(backend); both already implement the interface and forward env and waitUntil.
// doc-check: skip - fragment uses the application adapter, manifest, and client entry
import { inProcessClient } from "@nifrajs/client"
import { createWebApp } from "@nifrajs/web"
import { backend } from "./backend"
const app = createWebApp({
adapter,
manifest,
clientEntry,
api: inProcessClient(backend),
})5. Declare external library mounts
Better Auth and similar libraries can own routes such as /auth/** outside the Nifra typed contract. A relative fetch to that library is intentional, but otherwise resembles a hand-rolled own-API call. Declare only the exact mounted prefix in nifra.check.json so nifra check does not keep CI red:
{
"externalMounts": ["/auth"]
}Prefixes are segment-anchored: /auth covers /auth and its children, never /authors. Traversal paths are not suppressed. The normalized allowlist is echoed in human, JSON, and MCP check results, so every bypass remains auditable. This option affects only the typed-client lint; it does not mount, authorize, or trust the library.
6. Narrow typed-client failures by status
Routes with declared error schemas now return a failure union discriminated by HTTP status. After checking !result.ok, also narrow result.status before reading typed failure data. Undeclared statuses and transport status 0 remain unknown.
const result = await api.users({ id }).get()
if (!result.ok) {
if (result.status === 404) {
result.data.message // typed from errors[404]
} else {
// Undeclared status or transport status 0: result.data is unknown.
}
}7. Apply the remaining web and protocol changes
- Redirects accept an options object as their second argument.TS
// 1.x redirect("/done", 307) // 2.0 redirect("/done", { status: 307 }) - Replace the removed prerender wrapper with
enumerateStaticRoutes(). - Fragment navigation resolves element IDs only.
- MCP Apps metadata uses only
_meta.ui.resourceUri; remove the deprecated flatui/resourceUrikey. - Telemetry integrations use
ObservationAdapter; theAgentSpan,AgentSpanExporter, andSpanExporteraliases are removed. - Invalid HTTP method overrides always fail closed with 400.
nifra buildalways emits a complete deploy directory and defaults to Bun;nifra startruns the generatedserver.js.
8. Run the release gates
- Run
nifra check --jsonand fix every error. - If the project has
nifra.assurance.ts, runnifra assure --json. - If capabilities are configured, review a new snapshot and run
nifra capabilities check --json. - Run the application test suite and a production build.
- Exercise authentication, redirects, backend mounting, SSE/WebSocket routes, and each deploy adapter the application uses.