nifra
The full-stack TypeScript framework built for AI agents - and for the humans who work alongside them.
Coding agents drift. They call an endpoint that moved, expect a response shape that changed, or hand-roll fetch with ad-hoc types that fall out of sync the moment a route changes. nifra removes that class of bug at the framework level:
| |
|---|
| Typed client | client<typeof app> infers every path, param, body, and response from your server's TypeScript type. Any mismatch is a compile error. |
nifra check | Runs typecheck + typed-client lint in one command. Add it to CI - it fails the moment the frontend and backend drift. |
| AGENTS.md | Every scaffold ships a conventions file. Agents (Claude Code, Cursor, Copilot) read it and follow nifra's rules from the first prompt. |
nifra context | Prints this project's real API surface - routes + schemas - as Markdown. Paste into any agent prompt, or let nifra mcp deliver it automatically. |
nifra mcp | An MCP server that feeds Claude Code, Cursor, and Copilot Chat this project's live route and schema data. |
| Versioned transports | One bounded codec registry for plain JSON or rich values across HTTP, loaders, and WebSocket frames. |
| Durable effects | Postgres, SQLite, and Durable Object stores plus leased, cursor-bounded reconciliation for approvals and sagas. |
The rest is a fast, contract-first full-stack TypeScript stack: routing, validated I/O, SSR, loaders/actions, auth, WebSockets, MDX, and multi-runtime deployment.
Use nifra from your AI assistant
nifra's own docs, runnable examples, and API types are a live remote MCP server - listed in the official MCP registry - so Claude, Cursor, Codex, and any MCP client learn nifra from the source instead of guessing from stale training data:
- Claude Code:
claude mcp add --transport http nifra-docs https://mcp.nifra.dev
- Claude.ai / Desktop: Settings โ Connectors โ Add custom connector โ
https://mcp.nifra.dev
- Cursor / VS Code / other MCP clients: point them at
https://mcp.nifra.dev
Inside a project, nifra mcp additionally serves your app's live routes and schemas to the agent - so it writes against the code you have, not the code it remembers.
The backend
import { server } from "@nifrajs/core/server"
import { t } from "@nifrajs/schema"
export const app = server()
.get("/users/:id", (c) => ({ id: c.params.id }))
.post("/users", { body: t.object({ name: t.string() }) }, (c) => {
return { id: crypto.randomUUID(), name: c.body.name }
})
.listen(3000)
export type App = typeof app
The typed client - the anti-drift seam
import { client } from "@nifrajs/client"
import type { App } from "./server"
const api = client<App>("http://localhost:3000")
const res = await api.users({ id: "42" }).get()
if (res.ok) res.data.id
else res.error
The client never throws - every call returns { ok, status, data, error }, so the happy path and the failure path are both in the types.
nifra ships a purpose-built toolchain so coding agents stay correct as the codebase evolves.
AGENTS.md - generated per scaffold, teaches the agent nifra's non-obvious rules:
- validate every input at the boundary with
t or any Standard Schema
- always call this app's own API through
client<typeof app> - never hand-roll fetch
- never top-level-import server-only code into a route module
Adding nifra to an existing app? Run nifra init-agents. It writes the agent-discovery files for you - .mcp.json + .cursor/mcp.json (registering this project's nifra MCP), a CLAUDE.md MCP-first preamble, and an AGENTS.md section - no-clobber, so it never overwrites a file you've customized. (nifra check also nudges you when a project has no .mcp.json.)
Or connect the MCP server by hand so the agent reads your live routes, verifies endpoints, and gates drift from inside its tool loop. Run once from your project root:
claude mcp add nifra -- bunx nifra mcp
Once connected, the agent has fifteen tools - no setup per prompt:
| Tool | What it does |
|---|
nifra_context | This project's live routes + schemas + the exact typed-client call signature per route (Markdown). |
nifra_routes | The same routes as structured JSON ({ method, path, call, body?, query?, response? }) - for programmatic use. |
nifra_openapi | OpenAPI 3.1 generated from backend route schemas, as JSON or YAML. |
nifra_check | Typecheck + drift lint, returned as structured JSON with safe fix suggestions. |
nifra_assure | Classify every route and verify required/forbidden enforcement evidence. |
nifra_levels | The cumulative verification ladder (L0 typed contract โ L4 invariants): what the project proves, and why each level it misses does not hold. |
nifra_doctor | Flags undeclared imports and duplicate physical Nifra/React installs. |
nifra_run | Calls a route in-process (via @nifrajs/runner) - the agent self-verifies an endpoint without booting a server. |
nifra_render | Server-renders a page to HTML - verify SSR output. |
nifra_ws | Opens a real Bun WebSocket against the current app, sends test frames, and returns structured evidence. |
nifra_test | Runs bounded bun test and returns structured stdout, stderr, timing, and summary. |
nifra_scaffold | URL pattern โ the correct routes/ file for the chosen UI framework. |
nifra_docs / nifra_example | Search the docs / fetch a version-checked snippet that compiles as-is (no hallucinated APIs). |
nifra_types | Look up the exact current TypeScript signature for any public Nifra export. |
nifra_fix | Apply safe mechanical fixes, then return unresolved diagnostics. |
No MCP? The same data is available as plain commands - paste into any prompt, or run in CI:
nifra context
nifra check
nifra assure
nifra capabilities check
nifra manifest emit
nifra manifest diff old.json new.json
nifra doctor
nifra sync-manifest
Learn nifra from any assistant. The docs, example, and type tools are also hosted,
project-independent, at mcp.nifra.dev - add that one URL to Claude, Cursor, VS Code, or ChatGPT and it
learns nifra from the same verified corpora, no checkout. Read-only, no key.
claude mcp add --transport http nifra-docs https://mcp.nifra.dev
See Coding agents for per-client setup.
Upgrading from 1.x? Run nifra upgrade 2.0.0 as a dry-run, then follow the
Nifra 2.0 migration guide.
Install
bun add @nifrajs/core
bun add @nifrajs/client
bun add @nifrajs/schema
bun add @nifrajs/middleware
nifra is ESM-only and Bun-native (it uses Bun.serve). It runs on Bun; the client is environment-agnostic.
Use @nifrajs/core (or @nifrajs/core/server) for the ordinary HTTP runtime. Nifra keeps the package
root deliberately lean and splits everything else across documented subpaths - most apps only ever touch
a handful, so start with those and reach for the rest when a concept actually comes up:
- Everyday -
@nifrajs/core/server (the runtime), .../contract (defineContract + implement),
.../router, .../cookies, plus @nifrajs/schema (the t builder) and @nifrajs/client (the typed
client). This is the 80% API.
- Advanced, opt in when you need it -
.../assurance, .../capabilities, .../idempotency,
.../effect-ledger, .../durable-execution, .../causality, .../classification, .../manifest,
.../reflection, .../diff, .../mcp, .../sse, .../webhook, .../budget, .../seo, .../mount. Each is a separate documented
subpath, so you never pay (in bundle size or in concepts to learn) for one you don't import.
@nifrajs/schema's t is a TypeBox-backed builder: it validates at the request boundary and - because a TypeBox schema is a JSON Schema - generates OpenAPI with no extra work. Bring your own Standard Schema (zod, valibot, arktype) too; they validate identically.
import { server } from "@nifrajs/core/server"
import { t, toOpenAPI } from "@nifrajs/schema"
const app = server().post("/users", { body: t.object({ name: t.string() }) }, (c) => ({
id: "u1",
name: c.body.name,
}))
const openapi = toOpenAPI(app)
Invalid bodies are rejected with a structured 422 before your handler runs.
Graduate to a contract - handlers unchanged
When you want a decoupled, versionable API surface, lift the same routes into a contract. Handlers written inline lift over unchanged.
import { defineContract, implement } from "@nifrajs/core/contract"
import { t } from "@nifrajs/schema"
const contract = defineContract({
getUser: { method: "GET", path: "/users/:id", response: t.object({ id: t.string(), name: t.string() }) },
createUser: { method: "POST", path: "/users", body: t.object({ name: t.string() }), response: t.object({ id: t.string(), name: t.string() }) },
})
const app = implement(contract, {
getUser: (c) => ({ id: c.params.id, name: "ada" }),
createUser: (c) => ({ id: "new", name: c.body.name }),
})
The client can now be built from the contract alone (client(contract, url)) - no dependency on the server's source. This is the shape agents reference: nifra context emits the live contract; nifra check enforces it.
Harden it
import { server } from "@nifrajs/core/server"
import { cors, securityHeaders, rateLimit, MemoryStore } from "@nifrajs/middleware"
const app = server()
.use(securityHeaders())
.use(cors({ origin: ["https://app.example.com"], credentials: true }))
.use(rateLimit({
store: new MemoryStore(),
max: 100,
windowMs: 60_000,
key: (req) => req.headers.get("x-user-id") ?? "anonymous",
}))
.get("/", () => ({ ok: true }))
server({ requestTimeoutMs: 5_000, gracefulSignals: true })
Official hardening modules also publish route evidence. Add a nifra.assurance.ts policy and run
nifra assure in CI to fail when a new route is unclassified or misses required authentication, CSRF,
rate-limit, body-limit, idempotency, IP, or security-header enforcement. The proof is built from route
reflection, so it adds no request-path work. See Security & hardening.
Routes may also declare effect tokens ({ capabilities: ["db.read"] }). Add capability definitions and
approved/forbidden import provenance to the same nifra.assurance.ts: nifra check then blocks raw
effect imports and declaration/evidence drift, while nifra capabilities check also compares the
deterministic capabilities.lock.json. GET/HEAD domain writes fail unconditionally; mutating effects
must carry the request-idempotency or durable-command evidence required by their definition. Disabled
apps retain the existing request hot path; enabled routes pay only when they call useCapability.
For owned effects, prefer executeCapability(c, id, metadata, run): it assigns an effectId, records
intent plus exactly one automatic terminal outcome, forwards c.signal, and supports token-only async
aroundCapability() admission policies with fail-closed denial, timeout, and abort behavior. The original
synchronous beacon remains available for adapter hot paths.
For long-running or crash-sensitive effects, @nifrajs/core/durable-execution adds a signed,
single-use approval coordinator (tenant/principal/operation bound), a durable effect journal,
reconciliation reports, and a typed saga engine with reverse compensation and persisted retry state.
Crash-ambiguous executions and compensations stop in manual review; an operator can apply a
provider-confirmed outcome with resolveAmbiguity() (bound to the exact stored effect ID), then call
resume() or compensate() without replaying an unknown effect.
These require an explicitly durable store in production; the saga store owns encrypted business input
and compensation arguments, while the sealed ledger and @nifrajs/otel/effects remain token-only.
An approved provenance import is the explicit trust boundary: its provider internals are not scanned,
while every unapproved local wrapper remains transitively scanned for raw-effect bypasses.
For deployment promotion, nifra manifest emit combines those schemas and proofs with field-level
response sensitivity (classified(schema, "pii")) in one deterministic, hash-verified artifact.
Operator code may sign it with Ed25519 through a KMS/HSM callback; nifra manifest diff fails closed on
breaking contracts, lost assurance, expanded effects, or increased response sensitivity.
Runs on the edge, too
Bun is the first-class runtime (app.listen()), but the whole lifecycle is app.fetch(Request): Promise<Response> with zero Bun APIs - so the same app deploys to Cloudflare Workers (export default app), Deno (Deno.serve(app.fetch)), or Node (via the @nifrajs/node adapter). See Deployment and Edge & bindings.
Principles (enforced, not aspirational)
- Reject invalid input at three boundaries - compile-time (types), boot-time (config throws loudly), request-time (Standard Schema โ structured
422). "Genuine fallback" is a documented whitelist; everything else rejects.
- Tests everywhere, six kinds - unit, type-level (
*.test-d.ts), property/fuzz, mode-conformance, benchmark-regression, security-guardrail.
- Speed is a measured goal - tracked with the
oha HTTP matrix (bun run bench:http) across Bun, Node, and Deno against raw runtime handlers plus representative API framework baselines.
- Production-grade by default - graceful shutdown, redacting logs, idempotent guards, integer-money discipline; nothing is "we'll fix it later".
Packages
| Package | What it is |
|---|
@nifrajs/core | Router, fully-inferred server, contracts, lifecycle middleware, hardening |
@nifrajs/client | End-to-end-typed, never-throwing client (Eden-style proxy) |
@nifrajs/schema | TypeBox-backed t builder + toOpenAPI |
@nifrajs/middleware | CORS, security headers, rate limiting |
@nifrajs/testing | Contract-derived hostile inputs, response conformance, runtime matrices, test sessions |
@nifrajs/node | Run a nifra app on Node's http server (opt-in) |
@nifrajs/cli | nifra check, nifra context, nifra mcp - the agent toolchain |
Examples
Runnable, type-checked apps live in examples/:
bun run examples/inline-server.ts
bun run examples/contract-client.ts
bun run examples/schema-openapi.ts
bun run examples/hardened.ts
bun run examples/edge.ts
Develop
bun install
bun run check
bun run build
bun run check:publish
bun run bench:http
MIT licensed.