Edge & bindings
On Cloudflare and other edge runtimes, your platform bindings (KV, D1, Durable Objects, secrets) reach handlers through a fully-typed c.env.
Typed c.env
server<Env>() threads your Env type through the whole app: c.env is Env in every handler and in derive/decorate middleware - read c.env.KV directly, no cast. app.fetch(req, { env }) / toFetchHandler type-check the bindings too. In @nifrajs/web, a route's loader/action gets the same typed env.
// server<Env>() types the platform env end-to-end - c.env is your Env in every
// handler AND in derive/decorate middleware, with no cast.
interface Env { KV: KVNamespace; DB: D1Database }
const app = server<Env>()
.get("/u/:id", async (c) => {
const cached = await c.env.KV.get(c.params.id) // c.env is typed Env
return cached ?? "miss"
})
export default { fetch: (req: Request, env: Env, ctx: ExecutionContext) =>
app.fetch(req, { env, waitUntil: ctx.waitUntil.bind(ctx) }) }Export through toFetchHandler rather than handing the app out directly. Besides env and ctx.waitUntil, it tells nifra the request came from the platform's own HTTP parser, which lets the bounded-body cap trust the declared Content-Length as a real transport frame instead of re-counting every JSON body. A bare export default app has no such signal and pays the extra pass unless you set server({ trustBodyFraming: true }).
Background work
c.waitUntil(promise) keeps work alive after the response is sent - cache writes, analytics, fan-out - without blocking the user. (Typing is not validation: platform bindings are trusted inputs; validate anything untrusted at the boundary.)
// c.waitUntil keeps background work alive past the response (analytics, cache writes).
app.post("/event", (c) => {
c.waitUntil(c.env.KV.put("last", Date.now().toString()))
return { ok: true } // returns immediately; the put finishes in the background
})Validation on the edge
Edge runtimes block dynamic code generation (new Function), which trips many schema libraries. Nifra's t handles it transparently: it compiles a fast validator on Bun and Node, and falls back to an eval-free checker on Cloudflare Workers, Vercel Edge, and Deno Deploy - the same routes validate everywhere, with no edge-specific schema module. Because core validates any Standard Schema, you can also bring Zod, Valibot, or ArkType (all eval-free) - only t also emits OpenAPI from the same definition.
Size is the other edge axis. t is TypeBox-backed - it carries the whole JSON Schema type system - so a validated app on t is heavier than one on a lean validator. Validation is per route, so reach for t where you want the contract (OpenAPI/MCP for free) and Valibot where the Workers cold-start bundle is what matters: a Valibot-validated Nifra app measures ~16 KB gzipped. Both are first-class, and you can mix them in one app.
Scheduled (cron)
Pass scheduled to toFetchHandler to also export a Workers cron handler for a [triggers] schedule. It gets the platform controller plus the same typed env and waitUntil as your request handlers.
// doc-check: skip - `env.KV` is typed by your app's `Env` (the ./app import), supplied by the reader.
import { toFetchHandler } from "@nifrajs/core/server"
import { app } from "./app"
// Export fetch + a cron handler. Wire the schedule in wrangler.toml: [triggers] crons = ["0 * * * *"]
export default toFetchHandler(app, {
scheduled: (controller, { env, waitUntil }) =>
waitUntil(env.KV.put("last-run", String(controller.scheduledTime))),
})