Docs

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

Shell
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 build

The 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.

TS
// 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.

TS
// 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.

TS
// 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:

JSON
{
  "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.

TS
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 flat ui/resourceUri key.
  • Telemetry integrations use ObservationAdapter; the AgentSpan, AgentSpanExporter, and SpanExporter aliases are removed.
  • Invalid HTTP method overrides always fail closed with 400.
  • nifra build always emits a complete deploy directory and defaults to Bun; nifra start runs the generated server.js.

8. Run the release gates

  1. Run nifra check --json and fix every error.
  2. If the project has nifra.assurance.ts, run nifra assure --json.
  3. If capabilities are configured, review a new snapshot and run nifra capabilities check --json.
  4. Run the application test suite and a production build.
  5. Exercise authentication, redirects, backend mounting, SSE/WebSocket routes, and each deploy adapter the application uses.