Open source, MIT licensed

The AI-native full-stack TypeScript framework.

Change a route, and the build fails until the client, the docs, the agent tools, and the security policy agree. A change made by you or by a coding agent cannot half-land.

Runs on Bun, Node, Deno, and the edge.

Rename title to heading on the note route, and add a route that creates notes.

src/server.ts

.get("/notes/:id", async (c) => {
const note = await notes.find(c.params.id)
return { id: note.id, title: note.title }
return { id: note.id, heading: note.title }
})
.post("/notes", {
body: NoteInput,
}, (c) => notes.insert(c.body))

A coding agent gets a plain request, and edits two routes.

nifra check
✗ typecheck failed - the frontend/backend contract is broken
routes/note.tsx:14 Property 'title' does not exist on type '{ id: string; heading: string; }'.
...
✗ effect/capability assurance: 1
POST /notes evidence exceeds its declaration: db.write
✗ check failed: 2 errors

The page that still reads the old field stops compiling, and the write nobody declared is flagged.

src/server.ts

.post("/notes", {
body: NoteInput,
capabilities: ["db.write"],
}, (c) => notes.insert(c.body))
nifra assure
✖ POST /notes (authenticated-write) is missing nifra.authenticated
1 assurance failure across 2 routes.

Client fixed, write declared. Now policy objects: a domain write with no authentication.

src/server.ts

.use(jwt({
key: env.JWT_SECRET,
algorithms: ["HS256"],
}))
nifra check
✓ typecheck passed
...
✓ effect/capability assurance: none
✓ check passed
nifra assure
✓ route assurance: 2 routes classified; all required evidence is present. Capability assurance covered 2 routes. No declared response/error schemas were found.

One middleware supplies the evidence. Every gate agrees, and the change ships.

One request followed through every gate: a coding agent is asked to rename a field and add a route, nifra check fails on the stale client and the undeclared write, nifra assure names the unauthenticated write, and one auth middleware turns every gate green.
  • 24×dynamic SSR vs Next.js (React on Node, same machine)
  • 120krequests per second on Bun - 98% of the raw-runtime ceiling
  • 5 + 4UI frameworks + runtime families, from one app
  • 55typed packages, all optional

How these were measured

One framework in place of a stack you wire together.

  • express / fastify / hono@nifrajs/core
  • tRPC / OpenAPI client codegen@nifrajs/client
  • BullMQ (single-node jobs)@nifrajs/jobs
  • multer@nifrajs/uploads
  • dotenv + envalid@nifrajs/env
  • supertest@nifrajs/testing
All 16 replacements

01 · Agent-native

The same gates hold when an agent writes the code.

nifra mcp gives Claude Code, Cursor, and other MCP clients the live route table, a scaffolder that puts files in the right place, and the same check and assure gates your CI runs.

  1. nifra_contextRead the live app

    Routes, schemas, middleware, and conventions - the real API surface, not stale docs.

  2. nifra_scaffoldWrite in the right place

    URL patterns resolve to framework-correct files, handlers, loaders, and typed clients.

  3. nifra_runVerify the behavior

    HTTP, SSR, and WebSocket checks run against the current workspace.

  4. nifra_checkBlock drift

    Typecheck and route-contract checks, with the fix suggested, before CI goes green.

  5. nifra_assureProve the security posture

    Policy classifies every route - an unauthenticated write fails the build, named.

A 60-second terminal recording against a real app. The agent's edits are replayed from commits; every line of output is the CLI's own.

02 · End-to-end types

The client is inferred from the server.

Import the app's type and the path, params, body, and response are typed. There is no codegen step to forget and no schema file to drift.

server.ts
import { server } from "@nifrajs/core/server"
import { t } from "@nifrajs/schema"

const body = t.object({ name: t.string() })

// A typed API. No frontend required.
export const app = server()
  .get("/users/:id", (c) => ({ id: c.params.id }))
  .post("/users", { body }, (c) => {
    // c.body is validated and typed before this runs.
    return { id: crypto.randomUUID(), name: c.body.name }
  })

// Bun. Node, Deno, and the edge are one line each.
export default { fetch: app.fetch }
client.ts
import { client } from "@nifrajs/client"
// A type import: server code never ships to the client.
import type { app } from "./server"

const api = client<typeof app>("https://api.example.com")

// The path autocompletes. Params and body are typed.
const res = await api.users({ id: "42" }).get()

if (res.ok) {
  res.data.id   // typed from the route's return
} else {
  res.error     // failures are returned, never thrown
}

03 · Assurance

Security posture is a policy the build enforces.

Declare what a route may touch, write the rule once, and nifra assure names every route that breaks it. An unauthenticated write is a failed build, not a review comment.

  • nifra checkTypecheck plus contract rules: hand-rolled fetch to your own API, interpolated SQL, backend code imported by a page.
  • nifra assureClassifies every route against policy and lists the evidence each one is missing.
  • nifra levelsReports which assurance levels the app reaches: L0 typed contract, L1 route assurance, L2 capability lockfile.
nifra.assurance.ts
// nifra.assurance.ts - security posture as policy, not convention
import {
  defineAssuranceConfig,
  NIFRA_ASSURANCE,
} from "@nifrajs/core/assurance"
import { backend } from "./backend/app"

export default defineAssuranceConfig({
  source: backend,
  capabilities: {
    definitions: [{ id: "db.write", zone: "domain", access: "write" }],
    provenance: {
      // Classification needs a capability definition and static
      // provenance coverage for the effect it protects.
      imports: [{ specifier: "./db/write.ts", capabilities: ["db.write"] }],
      forbiddenImports: [],
    },
  },
  policy: {
    rules: [
      {
        // Every domain write must prove authentication.
        name: "authenticated-write",
        match: { access: "write", zone: "domain" },
        require: [NIFRA_ASSURANCE.AUTHENTICATED],
      },
    ],
  },
})

// $ nifra assure
// ✖ POST /notes (authenticated-write) is missing nifra.authenticated

04 · Try it

Run a real Nifra app without installing anything.

This is the real @nifrajs/core, running in this tab. Nothing is sent to a server. Pick an example or edit the code, then press Run.

Open the full playground

05 · Performance

Measured against the frameworks you would otherwise pick.

Same machine, same workload, every framework in its production mode. The method and the full tables are on the benchmarks page.

Dynamic SSR, React

Requests per second, higher is better

  1. Nifra + React (Bun)37,893
  2. Nifra + React (Node)34,311
  3. Remix (Node)2,421
  4. Next.js (Node)1,412

JSON API on Node

Requests per second on GET /users/:id, higher is better

  1. Nifra65,905
  2. Elysia58,040
  3. Fastify57,051
  4. Express35,780
  5. Hono22,868

Node is shown for the API chart. The Bun and Deno tables, including the rows where Nifra is not first, are on the benchmarks page.

06 · Portable

One app. Five UI frameworks, four runtime families.

The adapter is one line. Loaders, actions, streaming, and islands stay the same across React, Solid, Vue, Preact, and Svelte, and the same app runs on Bun, Node, Deno, and the edge.

  • Bun
    Native dev speedexport default { fetch: app.fetch }
  • Node
    Mature, everywhereserve(app, { port: 3000 })
  • Deno
    Secure by defaultserve(app, { port: 3000 })
  • Cloudflare
    Workers / Pagesexport default toFetchHandler(app)
  • Vercel
    Edge functionsexport default toFetchHandler(app)
See the five-framework demo
TS
// The adapter is the one line that changes per framework:
import { reactAdapter } from "@nifrajs/web-react"
export default createWebApp({ adapter: reactAdapter, manifest, clientEntry })

// routes/hello.tsx - your page, written in React
export function Page({ data }: { data: { name: string } }) {
  return <h1>Hello {data.name}</h1>
}
TS
// The adapter is the one line that changes per framework:
import { solidAdapter } from "@nifrajs/web-solid"
export default createWebApp({ adapter: solidAdapter, manifest, clientEntry })

// routes/hello.tsx - same page, Solid's fine-grained JSX
export function Page(props: { data: { name: string } }) {
  return <h1>Hello {props.data.name}</h1>
}
TS
// The adapter is the one line that changes per framework:
import { vueAdapter } from "@nifrajs/web-vue"
export default createWebApp({ adapter: vueAdapter, manifest, clientEntry })

<!-- routes/hello.vue - same page, as a Vue SFC -->
<script setup lang="ts">defineProps<{ data: { name: string } }>()</script>
<template><h1>Hello {{ data.name }}</h1></template>
TS
// The adapter is the one line that changes per framework:
import { preactAdapter } from "@nifrajs/web-preact"
export default createWebApp({ adapter: preactAdapter, manifest, clientEntry })

// routes/hello.tsx - same page; Preact's 3 KB runtime, identical React API
export function Page({ data }: { data: { name: string } }) {
  return <h1>Hello {data.name}</h1>
}
TS
// The adapter is the one line that changes per framework:
import { svelteAdapter } from "@nifrajs/web-svelte"
export default createWebApp({ adapter: svelteAdapter, manifest, clientEntry })

<!-- routes/hello.svelte - same page, as a Svelte component -->
<script lang="ts">export let data: { name: string }</script>
<h1>Hello {data.name}</h1>

07 · Ecosystem

Fewer dependencies to keep in agreement.

The common parts of a TypeScript stack ship as optional @nifrajs packages that share one set of types.

API & server
express / fastify / hono@nifrajs/core
helmet + cors + express-rate-limit@nifrajs/middleware
swagger-jsdoc@nifrajs/schema/openapi
Typed client & data
tRPC / OpenAPI client codegen@nifrajs/client
TanStack Query (loader cache + mutations)@nifrajs/web
Background work
BullMQ (single-node jobs)@nifrajs/jobs
node-cron@nifrajs/cron
Files & media
multer@nifrajs/uploads
flydrive (fs + R2 blob storage)@nifrajs/storage
sharp + srcset glue (responsive images)@nifrajs/image
App services
dotenv + envalid@nifrajs/env
keyv / node-cache@nifrajs/cache
i18next (locale + ICU formatting)@nifrajs/i18n
contentlayer@nifrajs/content
Testing & mocks
supertest@nifrajs/testing
json-server (contract mocks)@nifrajs/mock
Browse every package

Start with one route.

Scaffold an app, change a route, and watch the build tell you what else has to change.

Read the docs