@nifrajs/cli 0.1.0-alpha.1 → 0.1.0-alpha.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -361,6 +361,7 @@ and `c.query` are the _validated_ types, and a bad request gets a structured
361
361
  `422` automatically. Path params (`:id`) are typed from the pattern.
362
362
 
363
363
  ```ts
364
+ // doc-check: skip — uses the third-party `zod` schema lib (any Standard Schema works); install it to run this.
364
365
  import { server } from "@nifrajs/core"
365
366
  import { z } from "zod" // any Standard Schema works: zod, valibot, arktype…
366
367
 
@@ -412,6 +413,7 @@ body, or return type is a compile error. The result is the same `app` the inline
412
413
  builder produces.
413
414
 
414
415
  ```ts
416
+ // doc-check: skip — uses the third-party `zod` schema lib + an illustrative `users` repo; install zod to run this.
415
417
  import { defineContract, implement } from "@nifrajs/core"
416
418
  import { z } from "zod"
417
419
 
@@ -574,6 +576,7 @@ and the UI reflects it immediately. When the action resolves the real data takes
574
576
  failed submit **reverts** automatically — no manual rollback bookkeeping.
575
577
 
576
578
  ```ts
579
+ // doc-check: skip — component-body fragment: `id` and `todo` are the row's props.
577
580
  import { useFetcher } from "@nifrajs/web-react/fetcher"
578
581
 
579
582
  // Optimistic UI: while a submit is in flight, read the expected value from the in-flight
@@ -691,8 +694,10 @@ carries an `x-nifra-isr: hit | stale | miss` header.
691
694
  ```ts
692
695
  // server.ts — wrap the app with Incremental Static Regeneration.
693
696
  import { createWebApp, withISR, MemoryCacheStore } from "@nifrajs/web"
697
+ import { reactAdapter } from "@nifrajs/web-react"
698
+ import { clientEntry, manifest } from "./server-manifest" // generated by buildServer
694
699
 
695
- const app = createWebApp({ adapter, manifest, clientEntry, api })
700
+ const app = createWebApp({ adapter: reactAdapter, manifest, clientEntry })
696
701
  const store = new MemoryCacheStore() // dev / single-instance only
697
702
 
698
703
  // GET text/html responses are cached + served stale-while-revalidate. Default freshness 60s.
@@ -719,6 +724,7 @@ before it's trusted, so a corrupt or version-skewed entry is treated as a miss,
719
724
  broken.
720
725
 
721
726
  ```ts
727
+ // doc-check: skip — Workers entry: `Env`/`ExecutionContext` globals + your `app` from server.ts.
722
728
  // worker.ts — production uses a SHARED store so the cache + purges hold across instances.
723
729
  import { withISR, KVCacheStore, revalidateEndpoint } from "@nifrajs/web"
724
730
 
@@ -760,6 +766,7 @@ cache. Pass the same `draftSecret` to `createWebApp` and
760
766
  `withISR`; a forged or tampered cookie fails the constant-time signature check.
761
767
 
762
768
  ```ts
769
+ // doc-check: skip — fragment spanning three files: your `app`, `env`, `redirect`, and route `slug`.
763
770
  // 1. A route you gate yourself flips draft mode on for the editor.
764
771
  import { enableDraft, disableDraft } from "@nifrajs/web"
765
772
 
@@ -860,7 +867,11 @@ resolves, you call `stream.close()`, or the client disconnects (
860
867
  Node, Deno, and the edge — no `new Function`, no per-runtime API.
861
868
 
862
869
  ```ts
863
- import { sse } from "@nifrajs/core"
870
+ import { server, sse } from "@nifrajs/core"
871
+
872
+ const app = server()
873
+ // Your pub-sub of choice — subscribe returns an unsubscribe function.
874
+ declare const notifications: { subscribe(on: (n: { id: string }) => void): () => void }
864
875
 
865
876
  // A live feed — push events until the client disconnects.
866
877
  app.get("/notifications", (c) =>
@@ -1023,12 +1034,12 @@ routes, data, streaming — runs on a different framework, unchanged.
1023
1034
  // the same routes, loaders, actions, streaming, <Await>, fetchers, query cache.
1024
1035
  import { createWebApp } from "@nifrajs/web"
1025
1036
  import { reactAdapter } from "@nifrajs/web-react" // ← or web-solid · web-vue · web-preact · web-svelte
1037
+ import { clientEntry, manifest } from "./server-manifest" // generated by buildServer
1026
1038
 
1027
1039
  const app = createWebApp({
1028
1040
  adapter: reactAdapter, // the one line that changes per framework
1029
1041
  manifest,
1030
1042
  clientEntry, // the built client bundle (from buildClient's manifest)
1031
- api,
1032
1043
  })
1033
1044
  ```
1034
1045
 
@@ -1081,6 +1092,7 @@ defined after** `app.use(plugin)` — no extra generics. Wrap a plugin with
1081
1092
  plugins both depend on it), it wires its hooks once.
1082
1093
 
1083
1094
  ```ts
1095
+ // doc-check: skip — fragment: the outer `app`, `verify`, and `db` are your application's.
1084
1096
  import { definePlugin } from "@nifrajs/core"
1085
1097
 
1086
1098
  // A plugin is just (app) => app — call use/derive/decorate or register routes.
@@ -1130,13 +1142,15 @@ short-circuit), `beforeHandle`/`afterHandle` (around the handler),
1130
1142
  error, 404). Hardening middleware uses the same hook model:
1131
1143
 
1132
1144
  ```ts
1133
- import { cors, rateLimit, securityHeaders } from "@nifrajs/middleware"
1145
+ import { server } from "@nifrajs/core"
1146
+ import { cors, rateLimit, securityHeaders, MemoryStore } from "@nifrajs/middleware"
1134
1147
 
1135
1148
  // Hardening middleware is a hook bundle (context-agnostic) — same app.use():
1136
- app
1149
+ const app = server()
1137
1150
  .use(securityHeaders())
1138
- .use(cors({ origins: ["https://app.example"] }))
1139
- .use(rateLimit({ limit: 100, windowMs: 60_000 }))
1151
+ .use(cors({ origin: ["https://app.example"] }))
1152
+ // MemoryStore is dev/single-instance only — use a shared store (Redis, etc.) in production.
1153
+ .use(rateLimit({ store: new MemoryStore(), max: 100, windowMs: 60_000 }))
1140
1154
  ```
1141
1155
 
1142
1156
  ### Official plugins
@@ -1144,9 +1158,10 @@ app
1144
1158
  `@nifrajs/middleware` seeds a few `definePlugin` plugins to build on:
1145
1159
 
1146
1160
  ```ts
1161
+ import { server } from "@nifrajs/core"
1147
1162
  import { requestId, logger, etag } from "@nifrajs/middleware"
1148
1163
 
1149
- app
1164
+ const app = server()
1150
1165
  .use(requestId()) // reuse/generate x-request-id → c.requestId (typed) + response header
1151
1166
  .use(logger()) // one structured line/request: { method, path, status, ms }
1152
1167
  .use(etag()) // content-hash ETag on GET 200s → 304 on matching If-None-Match
@@ -1175,6 +1190,7 @@ cached. For full session-based auth (OAuth, magic links, 2FA), see
1175
1190
  [Auth & sessions](/docs/auth).
1176
1191
 
1177
1192
  ```ts
1193
+ // doc-check: skip — fragment: `app`, `lookupUser`, and the `db` lookup are your application's.
1178
1194
  import { bearer, apiKey } from "@nifrajs/middleware"
1179
1195
 
1180
1196
  // Bearer tokens — verify returns your principal (its type is inferred), 401s missing/invalid:
@@ -1207,9 +1223,10 @@ does custom, typed verification.
1207
1223
  clobbering one a handler already set.
1208
1224
 
1209
1225
  ```ts
1226
+ import { server } from "@nifrajs/core"
1210
1227
  import { compression, cacheControl } from "@nifrajs/middleware"
1211
1228
 
1212
- app
1229
+ const app = server()
1213
1230
  .use(compression()) // gzip compressible responses (Accept-Encoding)
1214
1231
  .use(cacheControl("public, max-age=60")) // Cache-Control on GET/HEAD 2xx (won't clobber)
1215
1232
  // …or a per-path policy — return undefined to leave a response untouched:
@@ -1234,6 +1251,7 @@ that Standard Schema can't expose. For full request/response _schemas_, generate
1234
1251
  exported for build-time generation too.
1235
1252
 
1236
1253
  ```ts
1254
+ // doc-check: skip — fragment: `app`, `db`, and `redis` are your application's clients.
1237
1255
  import { healthcheck, openapi } from "@nifrajs/middleware"
1238
1256
 
1239
1257
  app
@@ -1252,10 +1270,12 @@ connection. It short-circuits before the handler; a concurrent retry gets a `409
1252
1270
  guidance (shared store + DB constraint), and the `Set-Cookie` rule.
1253
1271
 
1254
1272
  ```ts
1273
+ import { server } from "@nifrajs/core"
1255
1274
  import { idempotency, MemoryIdempotencyStore } from "@nifrajs/middleware"
1256
1275
 
1257
1276
  // A retried POST with the same Idempotency-Key replays the first response instead of
1258
1277
  // re-running the side effect. Shared store in production (atomic claim); dev-only memory store.
1278
+ const app = server()
1259
1279
  app.use(idempotency({ store: new MemoryIdempotencyStore() }))
1260
1280
  ```
1261
1281
 
@@ -1270,11 +1290,12 @@ Read the typed claims off the returned plugin — `auth.requireClaims(c.req)` (t
1270
1290
  compares static credentials in **constant time** (or takes a `verify` callback).
1271
1291
 
1272
1292
  ```ts
1293
+ import { server } from "@nifrajs/core"
1273
1294
  import { jwt, jwks, basicAuth } from "@nifrajs/middleware"
1274
1295
 
1275
1296
  // JWT (WebCrypto): an explicit algorithm allowlist is REQUIRED; alg:none and RSA/HMAC confusion are rejected.
1276
1297
  const auth = jwt({ key: process.env.JWT_SECRET!, algorithms: ["HS256"], issuer: "my-app" })
1277
- app
1298
+ const app = server()
1278
1299
  .use(auth) // 401s missing/invalid (optional:true lets them through)
1279
1300
  .get("/me", (c) => auth.requireClaims(c.req)) // typed claims, or throws 401; auth.claims(req) is nullable
1280
1301
  // Asymmetric (rotating keys): key: jwks({ url: "https://issuer/.well-known/jwks.json" }) — https-only, cached.
@@ -1293,10 +1314,12 @@ user's response to another. `MemoryResponseCache` is per-instance and refuses
1293
1314
  pretty-prints JSON responses (capped, with an optional query toggle).
1294
1315
 
1295
1316
  ```ts
1317
+ import { server } from "@nifrajs/core"
1296
1318
  import { cache, MemoryResponseCache, prettyJson } from "@nifrajs/middleware"
1297
1319
 
1298
1320
  // Full response cache: pluggable store, Vary-aware keys, byte cap. Bypasses Set-Cookie and respects
1299
1321
  // Cache-Control (no-store/private). MemoryResponseCache is per-instance — refuses prod unless opted in.
1322
+ const app = server()
1300
1323
  app.use(cache({ store: new MemoryResponseCache(), ttlMs: 30_000, vary: ["accept-language"] }))
1301
1324
  app.use(prettyJson()) // pretty-print JSON responses (size-capped; optional ?pretty query toggle)
1302
1325
  ```
@@ -1313,9 +1336,10 @@ no open redirect); `language` negotiates `Accept-Language` into
1313
1336
  is opt-in; `combine` bundles several middleware into one.
1314
1337
 
1315
1338
  ```ts
1339
+ import { server } from "@nifrajs/core"
1316
1340
  import { methodOverride, trimTrailingSlash, language, timing, poweredBy, combine } from "@nifrajs/middleware"
1317
1341
 
1318
- app
1342
+ const app = server()
1319
1343
  .use(methodOverride()) // POST + X-HTTP-Method-Override → PUT/PATCH/DELETE (a real pre-routing request rewrite)
1320
1344
  .use(trimTrailingSlash()) // canonicalize URLs: 308 redirect (or rewrite), same-origin only, conservative methods
1321
1345
  .use(language({ supported: ["en", "fr"], defaultLanguage: "en" })) // Accept-Language → c.language + Content-Language
@@ -1365,7 +1389,7 @@ bypasses that read path. `c.boundedBody(maxBytes?)` and
1365
1389
  `c.boundedJson<T>(maxBytes?)` extend the **same** cap to those routes.
1366
1390
 
1367
1391
  ```ts
1368
- import { server, t } from "@nifrajs/core"
1392
+ import { server } from "@nifrajs/core"
1369
1393
 
1370
1394
  const app = server()
1371
1395
 
@@ -1378,12 +1402,13 @@ app.post("/import", async (c) => {
1378
1402
  // Responses (caught by the lifecycle like `throw redirect()`), so a handler can't
1379
1403
  // accidentally ignore the cap. The over-cap length is rejected BEFORE buffering;
1380
1404
  // a chunked / length-less body is aborted mid-stream once it crosses the cap.
1381
- return c.json({ received: bytes.byteLength })
1405
+ return { received: bytes.byteLength } // a returned object is serialized as JSON 200
1382
1406
  })
1383
1407
 
1384
1408
  app.post("/rpc", async (c) => {
1385
1409
  const body = await c.boundedJson<{ method: string }>() // default: the server's maxBodyBytes
1386
1410
  // …bad JSON → 400. Then validate `body` with your schema before trusting it.
1411
+ return { method: body.method }
1387
1412
  })
1388
1413
  ```
1389
1414
 
@@ -1400,12 +1425,13 @@ size cap and sniffs the **real** type from magic bytes — never the client-set
1400
1425
  `Blob` is rejected by its `.size` before it's ever buffered.
1401
1426
 
1402
1427
  ```ts
1428
+ // doc-check: skip — fragment: `app`, `save`, `id`, and `env` are your application's.
1403
1429
  import { validateUpload, signDownloadUrl } from "@nifrajs/uploads"
1404
1430
 
1405
1431
  app.post("/avatar", async (c) => {
1406
1432
  const form = await c.req.formData()
1407
1433
  const file = form.get("file")
1408
- if (!(file instanceof Blob)) return c.json({ ok: false, error: "no_file" }, 400)
1434
+ if (!(file instanceof Blob)) { c.set.status = 400; return { ok: false, error: "no_file" } }
1409
1435
 
1410
1436
  // Size cap + REAL type by magic bytes — a .exe renamed .png (or a spoofed
1411
1437
  // Content-Type) is caught, because the bytes win. An oversized Blob is rejected
@@ -1414,14 +1440,14 @@ app.post("/avatar", async (c) => {
1414
1440
  maxBytes: 2_000_000,
1415
1441
  accept: ["image/png", "image/jpeg"], // exact, or "image/*"
1416
1442
  })
1417
- if (!result.ok) return c.json({ ok: false, error: result.reason }, 400)
1443
+ if (!result.ok) { c.set.status = 400; return { ok: false, error: result.reason } }
1418
1444
  // reason: "too_large" | "empty" | "unrecognized" | "type_not_allowed"
1419
1445
 
1420
1446
  await save(result.bytes, `${id}.${result.ext}`) // result.mime / .ext are trustworthy
1421
1447
 
1422
1448
  // Hand back a short-TTL, tamper-evident URL (HMAC over path + expiry):
1423
1449
  const url = await signDownloadUrl(`/files/${id}`, env.FILE_SECRET, { expiresInSeconds: 300 })
1424
- return c.json({ ok: true, url })
1450
+ return { ok: true, url } // a returned object is serialized as JSON 200
1425
1451
  })
1426
1452
  ```
1427
1453
 
@@ -1435,6 +1461,7 @@ download links (HMAC-SHA256 over the path + expiry, constant-time verify). And
1435
1461
  [@nifrajs/image](/docs/images) backend, with no dependency on it:
1436
1462
 
1437
1463
  ```ts
1464
+ // doc-check: skip — fragment: continues the upload handler above (`result.bytes`).
1438
1465
  import { stripImageMetadata } from "@nifrajs/uploads"
1439
1466
  import { bunImageBackend } from "@nifrajs/image/backends"
1440
1467
 
@@ -1452,19 +1479,20 @@ payload. `verifyWebhook` reads the raw body bounded, verifies the HMAC, and hand
1452
1479
  the verified text for you to parse with your own schema.
1453
1480
 
1454
1481
  ```ts
1482
+ // doc-check: skip — fragment: `app`, `env`, `StripeEvent`, and the rotation keys are your application's.
1455
1483
  import { verifyWebhook } from "@nifrajs/core"
1456
1484
 
1457
1485
  app.post("/webhooks/stripe", async (c) => {
1458
1486
  // Reads the raw body BOUNDED (DoS guard), verifies the HMAC CONSTANT-TIME, and only
1459
1487
  // then returns the payload. Never JSON.parse a webhook before the signature checks out.
1460
1488
  const r = await verifyWebhook(c.req, env.STRIPE_WEBHOOK_SECRET, { provider: "stripe" })
1461
- if (!r.ok) return c.json({ ok: false, error: r.reason }, 400)
1489
+ if (!r.ok) { c.set.status = 400; return { ok: false, error: r.reason } }
1462
1490
  // reason: "missing_signature" | "invalid_signature" | "timestamp_out_of_tolerance"
1463
1491
  // | "malformed_signature" | "payload_too_large" | "invalid_content_length"
1464
1492
 
1465
1493
  const event = StripeEvent.parse(JSON.parse(r.payload)) // validate at the trust boundary
1466
1494
  // …handle event… (pair with idempotency below so a redelivery doesn't double-process)
1467
- return c.json({ ok: true })
1495
+ return { ok: true }
1468
1496
  })
1469
1497
 
1470
1498
  // GitHub (sha256=…hex), or any provider via the generic preset:
@@ -1488,6 +1516,7 @@ A dropped connection or an impatient double-tap shouldn't double-charge a card.
1488
1516
  re-running the side effect. It short-circuits in `onRequest`, _before_ the handler.
1489
1517
 
1490
1518
  ```ts
1519
+ // doc-check: skip — fragment: `app`, `chargeCard`, and `id` are your application's.
1491
1520
  import { idempotency, MemoryIdempotencyStore } from "@nifrajs/middleware"
1492
1521
 
1493
1522
  // Dev / single-instance. In production use a SHARED store (Redis, etc.) with an atomic
@@ -1496,7 +1525,7 @@ app.use(idempotency({ store: new MemoryIdempotencyStore() }))
1496
1525
 
1497
1526
  app.post("/charge", async (c) => {
1498
1527
  await chargeCard(/* … */) // the side effect
1499
- return c.json({ ok: true, id })
1528
+ return { ok: true, id }
1500
1529
  })
1501
1530
 
1502
1531
  // A client retrying POST /charge with the same `Idempotency-Key` header gets the FIRST
@@ -1531,13 +1560,14 @@ Transient `5xx` aren't cached, so a failed call stays retryable.
1531
1560
  Every one is constant-time where it compares secrets and **fails closed** by default.
1532
1561
 
1533
1562
  ```ts
1563
+ import { server } from "@nifrajs/core"
1534
1564
  import { jwt, csrf, ipRestriction, bodyLimit } from "@nifrajs/middleware"
1535
1565
 
1536
- app
1566
+ const app = server()
1537
1567
  // JWT: the algorithm allowlist is REQUIRED; alg:none and RSA/HMAC confusion are rejected; exp enforced.
1538
- .use(jwt({ key: env.JWT_SECRET, algorithms: ["HS256"], issuer: "my-app" }))
1568
+ .use(jwt({ key: process.env.JWT_SECRET!, algorithms: ["HS256"], issuer: "my-app" }))
1539
1569
  // Signed double-submit CSRF (HMAC) + Origin/Referer check on unsafe methods. Secret must be >= 32 bytes.
1540
- .use(csrf({ secret: env.CSRF_SECRET }))
1570
+ .use(csrf({ secret: process.env.CSRF_SECRET! }))
1541
1571
  // Allow/deny by IPv4/IPv6 + CIDR. FAILS CLOSED with no trusted client IP; X-Forwarded-For is ignored
1542
1572
  // unless trustedProxies > 0 (set it to the number of proxies you actually run in front of the app).
1543
1573
  .use(ipRestriction({ allow: ["10.0.0.0/8", "::1"], trustedProxies: 1 }))
@@ -1617,6 +1647,7 @@ your nifra server. Read the session with `getSession(auth, request)` (typed
1617
1647
  your tests need no database — and your Better Auth types flow through by inference.
1618
1648
 
1619
1649
  ```ts
1650
+ // doc-check: skip — needs the third-party `better-auth` package + your `db`; install it to run this.
1620
1651
  // auth.ts — your configured Better Auth instance (database, providers, …):
1621
1652
  import { betterAuth as createBetterAuth } from "better-auth"
1622
1653
  export const auth = createBetterAuth({ database: db, emailAndPassword: { enabled: true } })
@@ -1648,11 +1679,11 @@ Stores mirror the ISR discipline: `MemorySessionStore` is prod-guarded;
1648
1679
  ```ts
1649
1680
  // auth.ts — one session manager. LAZY so a route module can import it without shipping it to the
1650
1681
  // browser (see the warning below). Store mode keeps data server-side; cookie mode (no store) is stateless.
1651
- import { createSessions, MemorySessionStore, KVSessionStore } from "@nifrajs/auth"
1682
+ import { createSessions, MemorySessionStore } from "@nifrajs/auth"
1652
1683
 
1653
- let manager
1684
+ let manager: ReturnType<typeof createSessions> | undefined
1654
1685
  export const getSessions = () => (manager ??= createSessions({
1655
- secret: process.env.SESSION_SECRET, // ≥ 16 chars; rotating it invalidates all sessions
1686
+ secret: process.env.SESSION_SECRET!, // ≥ 16 chars; rotating it invalidates all sessions
1656
1687
  store: new MemorySessionStore(), // prod: new KVSessionStore(env.SESSIONS)
1657
1688
  // cookie: { secure: false }, // local http dev only
1658
1689
  }))
@@ -1694,6 +1725,7 @@ createWebApp({ /* … */ api: sessions }) // inject the manager
1694
1725
  as-is, so the guard short-circuits the loader.
1695
1726
 
1696
1727
  ```ts
1728
+ // doc-check: skip — fragment: `api` is the session manager createWebApp injected (see setup above).
1697
1729
  // A protected route's loader — reads the session and redirects when absent.
1698
1730
  import { requireUser } from "@nifrajs/auth" // browser-safe; OK to import in a route module
1699
1731
 
@@ -1900,6 +1932,7 @@ Deno-Deploy), where there's no native codec. `@nifrajs/image/backends` has no
1900
1932
  built in (it never decodes just to read dimensions).
1901
1933
 
1902
1934
  ```ts
1935
+ // doc-check: skip — illustrates third-party codecs (sharp, @jsquash/*); install them to run it.
1903
1936
  import { createImageHandler } from "@nifrajs/image/server"
1904
1937
  import { sharpImageBackend, wasmImageBackend } from "@nifrajs/image/backends"
1905
1938
 
@@ -1939,15 +1972,17 @@ loader can sign inline — even on the edge.
1939
1972
  import { selfHostedLoader, signImageUrl } from "@nifrajs/image"
1940
1973
  import { createImageHandler } from "@nifrajs/image/server"
1941
1974
 
1942
- const resize = selfHostedLoader({ endpoint: "/_image", secret: env.IMAGE_SECRET })
1975
+ const IMAGE_SECRET = process.env.IMAGE_SECRET! // server-only; on Workers read c.env.IMAGE_SECRET instead
1976
+
1977
+ const resize = selfHostedLoader({ endpoint: "/_image", secret: IMAGE_SECRET })
1943
1978
  // → /_image?src=…&w=800&s=<hmac> (stable: an SSR-signed URL hydrates + caches identically)
1944
1979
 
1945
- const image = createImageHandler({ root: "./public", signing: { secret: env.IMAGE_SECRET } })
1980
+ const image = createImageHandler({ root: "./public", signing: { secret: IMAGE_SECRET } })
1946
1981
  // any request without a valid &s= (forged / tampered / expired) → 403, before any fetch or decode
1947
1982
 
1948
1983
  // Time-limited links to private images (server-side only):
1949
1984
  const url = signImageUrl("/_image", { src: "/private/a.jpg", width: 800 }, {
1950
- secret: env.IMAGE_SECRET,
1985
+ secret: IMAGE_SECRET,
1951
1986
  expiresIn: 300, // seconds → adds &exp=
1952
1987
  })
1953
1988
  ```
@@ -1974,8 +2009,10 @@ const info = await readImageDimensions(Bun.file("public/hero.jpg"))
1974
2009
  // → { width: 1200, height: 630, format: "jpeg" } (null if unrecognized)
1975
2010
 
1976
2011
  // Build-time tooling: pre-read sizes into a manifest so <Image> is CLS-safe
1977
- // without hardcoding width/height at every call site.
1978
- imageDimensions(new Uint8Array(headerBytes))
2012
+ // without hardcoding width/height at every call site. The sync variant takes the
2013
+ // raw header bytes you already have in memory (no file read):
2014
+ const headerBytes = await Bun.file("public/hero.jpg").bytes()
2015
+ imageDimensions(headerBytes)
1979
2016
  ```
1980
2017
 
1981
2018
  ### Notes
@@ -2015,7 +2052,7 @@ messages.
2015
2052
  import { negotiateLocale } from "@nifrajs/i18n"
2016
2053
  import { catalogs, locales } from "../catalogs"
2017
2054
 
2018
- export async function loader({ request }) {
2055
+ export async function loader({ request }: { request: Request }) {
2019
2056
  const q = new URL(request.url).searchParams.get("lang")
2020
2057
  const locale = q && locales.includes(q) ? q : negotiateLocale(request, { locales, defaultLocale: "en", cookie: "lang" })
2021
2058
  return { locale, messages: catalogs[locale] } // cookie → Accept-Language → default
@@ -2143,6 +2180,7 @@ handler for a `[triggers]` schedule. It gets the platform controller plus the sa
2143
2180
  typed `env` and `waitUntil` as your request handlers.
2144
2181
 
2145
2182
  ```ts
2183
+ // doc-check: skip — `env.KV` is typed by your app's `Env` (the ./app import), supplied by the reader.
2146
2184
  import { toFetchHandler } from "@nifrajs/core"
2147
2185
  import { app } from "./app"
2148
2186
 
@@ -2302,10 +2340,11 @@ Deploy: `wrangler pages deploy dist` (Pages) or `wrangler deploy`
2302
2340
  `server.ts` to hand-write. (`create-nifra` scaffolds the conventions.)
2303
2341
 
2304
2342
  ```ts
2305
- nifra dev # true-HMR dev server (Vite middleware + nifra SSR) — http://localhost:3000
2343
+ nifra dev # true-HMR dev server (Vite middleware + nifra SSR) — http://localhost:4321
2306
2344
  nifra build # bundle the client (content-hashed, code-split) + write dist/manifest.json (incl. CSS)
2307
2345
  nifra start # serve the built client + SSR on Bun (assets, <link> stylesheets, matched-route preload)
2308
2346
 
2347
+ # dev + start share the default port 4321. Override per run: --port <n> (alias -p) or the PORT env var.
2309
2348
  # flags: --port <n> (dev/start) · --out <dir> (build/start) · --poll (dev; containers/sandboxes)
2310
2349
  ```
2311
2350
 
@@ -2331,6 +2370,7 @@ required; the plugin/condition fields are extras the compiler frameworks need (R
2331
2370
  JSX is Bun-native, so they need none beyond the Vite plugin for dev HMR).
2332
2371
 
2333
2372
  ```ts
2373
+ // doc-check: skip — needs the third-party @vitejs/plugin-react; install it to run this.
2334
2374
  // framework.ts — the one place an app names its framework. The CLI reads these:
2335
2375
  import react from "@vitejs/plugin-react"
2336
2376
  import { reactAdapter } from "@nifrajs/web-react"
@@ -2389,6 +2429,7 @@ without a full page reload. Pass the official plugin for your UI framework, keep
2389
2429
  Nifra routes and loaders, and run the dev server during local development.
2390
2430
 
2391
2431
  ```ts
2432
+ // doc-check: skip — needs the third-party @vitejs/plugin-react + your ./backend; install it to run this.
2392
2433
  // dev.ts — state-preserving HMR for supported UI adapters
2393
2434
  import react from "@vitejs/plugin-react" // your framework's official Vite plugin
2394
2435
  import { createWebApp } from "@nifrajs/web"
@@ -2403,7 +2444,7 @@ const server = await createViteDevServer({
2403
2444
  routesDir,
2404
2445
  clientModule: "@nifrajs/web-react/client",
2405
2446
  plugins: [react()], // Vue: @vitejs/plugin-vue, Svelte: …, etc.
2406
- port: Number(Bun.env.PORT ?? 3000),
2447
+ port: Number(Bun.env.PORT ?? 4321), // nifra's default; --port / PORT override it
2407
2448
  createApp: (clientEntry, importQuery) =>
2408
2449
  createWebApp({
2409
2450
  adapter: reactAdapter,
@@ -2509,6 +2550,7 @@ instead.
2509
2550
  serves SSR, watches the routes, and does a full-page reload on change.
2510
2551
 
2511
2552
  ```ts
2553
+ // doc-check: skip — fragment: routesDir/outDir/clientModule/createApp are your app's dev config.
2512
2554
  // dev.ts — Bun-native live reload
2513
2555
  import { createDevServer } from "@nifrajs/web/dev"
2514
2556
  // builds the client on boot, serves SSR, watches routes, full-page live-reload on change.
@@ -2584,6 +2626,675 @@ own stylesheet. Pass `manifest.routeStyles` to `createWebApp` alongside
2584
2626
  `styles`, and Nifra links the matched route's CSS during SSR. In
2585
2627
  **dev**, Vite injects per-module CSS.
2586
2628
 
2629
+ ## Nifra — Troubleshooting
2630
+
2631
+ > Fixes keyed on the literal error strings Nifra prints: `reached the client bundle` (a node:/native import in the browser bundle), `server-only module reached the client bundle` (the server-only marker), and `resolveDispatcher` / `Invalid hook call` (duplicate React).
2632
+
2633
+ Hit an error? Search this page for the literal message. Each section is keyed on the exact
2634
+ string Nifra prints, with what it means and the fix. The build, `nifra check`, and
2635
+ the runtime all use the same wording so you can grep for it.
2636
+
2637
+ ### `reached the client bundle` — a `node:` / native import leaked to the browser
2638
+
2639
+ The client build refuses to ship a Node built-in (`node:fs`, `node:crypto`,
2640
+ `bun:sqlite`, a native driver like `pg`) to the browser. Bun would
2641
+ silently substitute a polyfill that breaks — or leaks server code — at runtime, so Nifra fails
2642
+ the build instead. The message names the offending builtin and the **import chain**
2643
+ that pulled it in:
2644
+
2645
+ [nifra/web] Node built-in(s) in the client bundle — move them behind a server-only path
2646
+
2647
+ - node:crypto reached the client bundle via routes/x.tsx → ../data.ts → ../db.ts
2648
+ (chunk: x-abc123.js)
2649
+
2650
+ Read the chain right-to-left: `../db.ts` imports `node:crypto`, and a
2651
+ top-level import in `routes/x.tsx` dragged it into the browser. **Fix it one
2652
+ of two ways:**
2653
+
2654
+ -
2655
+ **Move the code into a `*.server.ts` module** (the recommended
2656
+ default). The client build empties `*.server` modules, so their `node:`
2657
+ / native imports never reach the browser. Import the server module only from a loader/action
2658
+ (which run on the server).
2659
+
2660
+ -
2661
+ **Reach the server code from a loader/action, not a route's top level.** A
2662
+ loader runs on the server during SSR; importing the `node:` module inside (or via)
2663
+ a loader keeps it out of the client graph.
2664
+
2665
+ ```ts
2666
+ // doc-check: skip — illustrative two-file layout (a relative cross-file import).
2667
+ // db.server.ts — the `.server` convention: the client build EMPTIES this module, so its
2668
+ // `node:` / native imports never ship to the browser. No marker import needed — the filename is it.
2669
+ import { Database } from "bun:sqlite"
2670
+ export const db = new Database("app.db")
2671
+
2672
+ // routes/notes.tsx — import the server module from a loader; it runs only on the server during SSR.
2673
+ import type { LoaderContext } from "@nifrajs/web"
2674
+ import { db } from "../db.server"
2675
+ export async function loader(_ctx: LoaderContext) {
2676
+ return { notes: db.query("select * from notes").all() }
2677
+ }
2678
+ ```
2679
+
2680
+ [!TIP]
2681
+
2682
+ Run `nifra check` (or `nifra check --json` for agents) to catch this
2683
+ _before_ the build: it reports the same transitive chain (
2684
+ `routes/x → ../data → ./db → node:crypto`) by walking the local module graph, so
2685
+ you see the leak as a lint result, not a failed build.
2686
+
2687
+ ### `server-only module reached the client bundle` — the server-only marker fired
2688
+
2689
+ This is the companion guard for **pure server logic that carries no `node:`
2690
+ import** — a secret-bearing constant, a server-only API call — so the node-builtin guard
2691
+ above has nothing to catch and the `.server` convention needs the file to be
2692
+ _named_ `*.server`. You opt a module in with a side-effect import, and the
2693
+ client build fails loud (with the import chain) if it ever lands in a browser chunk:
2694
+
2695
+ [nifra/web] server-only module(s) in the client bundle — a module marked
2696
+ `import "@nifrajs/web/server-only"` reached the browser.
2697
+
2698
+ - server-only module reached the client bundle via routes/x.tsx → ./secrets.ts (marked
2699
+ server-only)
2700
+
2701
+ There are three markers; reach for them by intent:
2702
+
2703
+ marker
2704
+ enforcement
2705
+ use when
2706
+
2707
+ `*.server.ts` filename
2708
+
2709
+ client build auto-empties the module
2710
+
2711
+ a dedicated server module (a DB client, a `node:` helper) you can name
2712
+ `*.server`
2713
+
2714
+ `import "@nifrajs/web/server-only"`
2715
+
2716
+ client build fails loud, with the chain, if it leaks
2717
+
2718
+ pure server logic with **no `node:` import to catch** (a secret,
2719
+ a server-only call) that you can't / don't want to rename
2720
+
2721
+ `ServerOnly` type
2722
+
2723
+ type-level intent only — does NOT keep it out of the bundle
2724
+
2725
+ documenting that a value must not cross to the browser; pair it with one of the two
2726
+ runtime markers
2727
+
2728
+ A worked example — a secret with no `node:` import, marked so a leak fails the build
2729
+ rather than shipping the key to every visitor:
2730
+
2731
+ ```ts
2732
+ // secrets.ts — pure server logic: a secret constant, no `node:` import to catch.
2733
+ import "@nifrajs/web/server-only" // ← fails the CLIENT build (loud, with the import chain)
2734
+ import type { ServerOnly } from "@nifrajs/web" // ← type-level intent: this value is server-only
2735
+
2736
+ // `ServerOnly<string>` is structurally `string` (the brand is an optional phantom field), so it
2737
+ // stays assignment-compatible — it documents intent without obstructing real use.
2738
+ export const apiKey: ServerOnly<string> = process.env.SECRET_API_KEY!
2739
+
2740
+ // If this module ever reaches a browser chunk, buildClient fails with:
2741
+ // server-only module reached the client bundle via routes/x.tsx → ./secrets.ts (marked server-only)
2742
+ // Reach it from a loader/action (server-only) instead, and the secret never ships to the client.
2743
+ ```
2744
+
2745
+ **Fix:** reach the module from a loader/action (server-only), or rename it
2746
+ `*.server.ts` so the client build empties it. `nifra check` reports the
2747
+ same transitive chain pre-build. The `ServerOnly` brand on its own is purely
2748
+ type-level (it erases at build), so always back it with the import marker or the
2749
+ `.server` filename.
2750
+
2751
+ ### `resolveDispatcher` / `Invalid hook call` — duplicate React
2752
+
2753
+ If SSR throws `Cannot read properties of null (reading 'useState')` inside
2754
+ `resolveDispatcher`, or React logs **"Invalid hook call. Hooks can only be
2755
+ called inside the body of a function component"**, you almost certainly have
2756
+ **two copies of React** in one render. React's hook dispatcher is module-level
2757
+ global state; a second copy nulls it out and every hook throws.
2758
+
2759
+ Nifra **dedupes React** in both the production build and the Vite dev server, so
2760
+ the framework itself won't load two copies. The usual culprit is a
2761
+ **`file:`-linked package** (a local component library you
2762
+ `bun link` or reference with `file:../lib`) that bundles its own React in
2763
+ its own `node_modules`:
2764
+
2765
+ -
2766
+ Make React a **peer dependency** of the linked package (not a regular
2767
+ dependency), so it resolves to the app's single copy.
2768
+
2769
+ -
2770
+ Ensure one React version across the workspace — pin it in the root
2771
+ `package.json` `overrides` (Nifra's own repo pins
2772
+ `react` / `react-dom` this way) so every package resolves the same
2773
+ copy.
2774
+
2775
+ -
2776
+ Delete the linked package's nested `node_modules/react` after linking if your
2777
+ package manager duplicated it.
2778
+
2779
+ [!NOTE]
2780
+
2781
+ This applies to every framework with module-global render state, not just React (Preact,
2782
+ Vue, Solid, Svelte). Nifra dedupes the active adapter's runtime in build and dev; a
2783
+ `file:`-linked package shipping its own copy is the thing to fix. See
2784
+ [Dev & HMR](/docs/dev) and the `file:`-linked-package note in
2785
+ `AGENTS.md`.
2786
+
2787
+ ### Still stuck?
2788
+
2789
+ Run `nifra check --json` as the done-gate — it surfaces the import-chain leaks,
2790
+ typed-client drift, and raw-`Response`-from-a-route issues before you ship. The
2791
+ full machine-readable contract is at [/llms-full.txt](/llms-full.txt), and each
2792
+ package ships a tight `LLM.md` contract card.
2793
+
2794
+ ## Nifra — Backends & API (dev and prod)
2795
+
2796
+ > How a Nifra backend reaches your app: inProcessClient feeds ctx.api to loaders, and createWebApp auto-mounts the backend over HTTP at /api/* — in `nifra dev` and production alike, no hand-dispatch.
2797
+
2798
+ Your `@nifrajs/core` backend reaches a Nifra app two ways from a single wiring.
2799
+ `inProcessClient(backend)` is fed to every loader and action as
2800
+ `ctx.api` — and `createWebApp` now **auto-mounts** that
2801
+ backend over HTTP at `/api/*`, so the browser can call the very same routes. No
2802
+ hand-written `if (pathname.startsWith("/api/"))` branch in your server entry.
2803
+
2804
+ ### One backend, two call paths
2805
+
2806
+ Write the backend once. It defines its routes at the full `/api/…` path (the mount
2807
+ does no path stripping). Loaders call it _in-process_ during SSR; the browser calls it
2808
+ _over HTTP_. Both run the identical lifecycle — validation, middleware, contracts.
2809
+
2810
+ ```ts
2811
+ // backend.ts — a normal @nifrajs/core server. Routes live at the full /api/... path.
2812
+ import { server } from "@nifrajs/core"
2813
+ import { t } from "@nifrajs/schema"
2814
+
2815
+ export const backend = server()
2816
+ .post("/api/sync", { body: t.object({ cursor: t.string() }) }, async (c) => {
2817
+ // validated body; runs the same whether called in-process (a loader) or over HTTP (a client fetch)
2818
+ return { applied: 12, nextCursor: c.body.cursor }
2819
+ })
2820
+ .get("/api/me", (c) => ({ id: c.cookies.session ?? null }))
2821
+ ```
2822
+
2823
+ ### ctx.api — the in-process loader client
2824
+
2825
+ Pass `inProcessClient(backend)` as `createWebApp`'s `api`.
2826
+ Inside a loader or action, `ctx.api` is that typed client, and a call goes
2827
+ **straight to the backend's `fetch` in-process** — no network hop, no
2828
+ port, the full real lifecycle. This is the SSR data path; it never touches the HTTP mount.
2829
+
2830
+ ```ts
2831
+ // routes/index.tsx — a loader calls the backend IN-PROCESS via ctx.api (no network).
2832
+ import type { LoaderContext } from "@nifrajs/web"
2833
+
2834
+ export async function loader(ctx: LoaderContext) {
2835
+ const api = ctx.api as { me: { get(): Promise<{ data: { id: string | null } }> } }
2836
+ const res = await api.me.get() // in-process: full validation/middleware, no HTTP round-trip
2837
+ return { me: res.data }
2838
+ }
2839
+ ```
2840
+
2841
+ ### The auto-mounted /api/* (the new part)
2842
+
2843
+ Before, `inProcessClient` fed `ctx.api` but did _not_ serve the
2844
+ backend over HTTP — so a browser `POST /api/sync` hit the page router and 404/405'd
2845
+ until you hand-wrote a dispatch branch in `server-bun.ts`. Now
2846
+ `createWebApp` mounts it for you: a request whose pathname is exactly
2847
+ `apiPrefix` (default `/api`) or starts with
2848
+ `apiPrefix + "/"` is dispatched to `api.fetch(req.url, req)`
2849
+ **before** page routing, and the backend's `Response` is returned
2850
+ untouched (the request body is passed through, never pre-read).
2851
+
2852
+ ```ts
2853
+ // server.ts (prod) — createWebApp serves pages AND auto-mounts the backend at /api/*.
2854
+ import { inProcessClient } from "@nifrajs/client"
2855
+ import { createWebApp } from "@nifrajs/web"
2856
+ import { reactAdapter } from "@nifrajs/web-react"
2857
+ import { backend } from "./backend"
2858
+ import { clientEntry, manifest } from "./server-manifest"
2859
+
2860
+ export const app = createWebApp({
2861
+ adapter: reactAdapter,
2862
+ manifest,
2863
+ clientEntry,
2864
+ api: inProcessClient(backend), // → ctx.api in loaders/actions AND auto-mounted at /api/*
2865
+ // apiPrefix: "/api", // the default; pass "" to disable the HTTP mount (pages only)
2866
+ })
2867
+
2868
+ // Bun: Bun.serve({ fetch: app.fetch }). No `if (pathname.startsWith("/api/")) …` branch needed —
2869
+ // POST /api/sync, GET /api/me, etc. are dispatched to the backend BEFORE the page router sees them.
2870
+ ```
2871
+
2872
+ The dispatch runs in `createWebApp`'s request lifecycle, ahead of the page wildcard,
2873
+ for **every method** — so `GET`/`POST`/`PUT`/… all
2874
+ reach the backend, and an unknown `/api/…` path returns the _backend's_ 404,
2875
+ not the page's. A sibling path that merely shares the prefix string (e.g.
2876
+ `/apidocs`) is **not** captured — only the `/api` boundary
2877
+ is. Pass `apiPrefix: ""` to turn the mount off and keep `ctx.api` as a
2878
+ loader-only client.
2879
+
2880
+ [!NOTE] The mount lives in `createWebApp`, and `nifra dev` (the
2881
+ Vite-backed dev server) routes every request through that same app's `fetch`. So
2882
+ the `/api/*` routes are served identically in development and production — there is
2883
+ nothing extra to wire for the dev loop.
2884
+
2885
+ ### Calling /api/* from the browser
2886
+
2887
+ With the routes mounted, a client island or component hits them with the same typed
2888
+ `client<typeof backend>` — same-origin, no separate API server.
2889
+
2890
+ ```ts
2891
+ // A browser island / client component hits the mounted HTTP routes with the typed client.
2892
+ import { client } from "@nifrajs/client"
2893
+ import type { backend } from "./backend"
2894
+
2895
+ const api = client<typeof backend>("") // same-origin: /api/sync is served by createWebApp's mount
2896
+ export async function runSync(cursor: string) {
2897
+ const { data, error } = await api.api.sync.post({ cursor })
2898
+ return error ? { applied: 0 } : data
2899
+ }
2900
+ ```
2901
+
2902
+ ### Route actions vs the in-process backend (for mutations)
2903
+
2904
+ Two valid ways to mutate; pick by where the call originates.
2905
+
2906
+ -
2907
+ **Route `action`** — the form/SSR path. A
2908
+ `` (or the client submit) runs the route's
2909
+ `action(ctx)`, which typically calls `ctx.api` in-process and returns
2910
+ `actionData` (or a `redirect`). Progressive-enhancement: works with JS
2911
+ off, and the loader revalidates after. Reach for this for page-driven mutations tied to a
2912
+ route's UI.
2913
+
2914
+ -
2915
+ **The mounted `/api/*` backend** — the programmatic path. A browser
2916
+ island, a third party, a webhook, or a non-page client calls the HTTP route directly. Reach
2917
+ for this when the caller isn't a Nifra route's form — an RPC the page makes on an
2918
+ interaction, an external integration, a mobile client.
2919
+
2920
+ They share the backend: a route `action` and a browser `fetch` can both
2921
+ call `POST /api/sync` — one in-process, one over the mount — and get identical
2922
+ validation and behavior.
2923
+
2924
+ ## Nifra — Framework contract
2925
+
2926
+ > The one-page contract: LoaderContext shape, loader/action/meta/head signatures, the client↔server boundary, and the build-output contract — every signature verified against the source.
2927
+
2928
+ Everything you'd otherwise read the source to learn, on one page: the
2929
+ `LoaderContext` shape, the loader / action / meta / head signatures, what runs on
2930
+ the server vs the client, the two ways to handle a request, the dynamic-route 404 rule, and
2931
+ the build-output contract. Every signature here is verified against the
2932
+ `@nifrajs/web` source.
2933
+
2934
+ ### LoaderContext
2935
+
2936
+ One context object is passed to every `loader` and every `action`. The
2937
+ same five fields, always — `params`, `request`, `api`,
2938
+ `env`, and `draft`.
2939
+
2940
+ ```ts
2941
+ // The single context object passed to every loader AND action (@nifrajs/web).
2942
+ interface LoaderContext {
2943
+ params: Record<string, string> // matched route params (:id, *path)
2944
+ request: Request // the standard Web Request — read headers/body/URL off it
2945
+ api: unknown // the in-process backend client createWebApp was given
2946
+ env: unknown // platform bindings forwarded from c.env (Workers KV/D1/…)
2947
+ draft: boolean // true only when a valid draft cookie is present (draftSecret set)
2948
+ }
2949
+
2950
+ // api + env are typed per-route via @nifrajs/client's LoaderArgs<Api, Env>; the agnostic core
2951
+ // keeps them `unknown`. Branch on `draft` to load unpublished content for preview/editors.
2952
+ ```
2953
+
2954
+ ### Loader, action & meta signatures
2955
+
2956
+ A route module co-locates its data, mutation, head, and view. Only the
2957
+ `default` export (the component) is required; `loader`,
2958
+ `action`, `meta`, and `hydrate` are all optional. A
2959
+ `loader` runs on **GET** and feeds `props.data`; an
2960
+ `action` runs on **POST** and feeds `props.actionData`.
2961
+
2962
+ ```ts
2963
+ // routes/users/[id].tsx — a route module's full contract (every export optional but `default`).
2964
+ // doc-check: skip — full-contract sketch; ctx.api's routes and meta's loader-data shape come from
2965
+ // the reader's own backend type, so it can't compile standalone (see the typed examples elsewhere).
2966
+ import type { LoaderContext } from "@nifrajs/web"
2967
+
2968
+ // GET → data for the page. Runs in-process on the server during SSR (no HTTP hop).
2969
+ export async function loader(ctx: LoaderContext) {
2970
+ return { user: await ctx.api.users.get({ id: ctx.params.id }) }
2971
+ }
2972
+
2973
+ // POST → a mutation. Same context. Return a Response (e.g. a redirect — passed straight
2974
+ // through) OR data, which reaches the component as `actionData`.
2975
+ export async function action(ctx: LoaderContext) {
2976
+ const form = await ctx.request.formData()
2977
+ return { ok: true }
2978
+ }
2979
+
2980
+ // Static OR a function of { data, params, origin }. Static meta is serialized once; function meta
2981
+ // recomputes per request (its content can vary with loader data). `origin` is the request's
2982
+ // scheme+host (server-resolved, matches the client's location.origin) — use it for ABSOLUTE
2983
+ // canonical / og:url / og:image URLs without threading siteUrl through loader data.
2984
+ export const meta = ({ data, params, origin }) => ({
2985
+ title: `User ${data.user.name}`,
2986
+ meta: [{ name: "description", content: data.user.bio }],
2987
+ link: [{ rel: "canonical", href: `${origin}/users/${params.id}` }],
2988
+ })
2989
+
2990
+ export const hydrate = false // opt out of full-document hydration (static / island pages)
2991
+ export default function User(props) { /* props.data, props.actionData, props.params */ }
2992
+ ```
2993
+
2994
+ [!NOTE] An `action` may return a `Response` (a `redirect()`,
2995
+ say) — it's passed through untouched. Any other return is serialized as
2996
+ `actionData`.
2997
+
2998
+ ### Two ways to handle a request
2999
+
3000
+ A page's data comes from a `loader` calling the **in-process backend**
3001
+ (`ctx.api`) — but that backend is **loaders-only**: it registers
3002
+ `GET` handlers, so a `.post()` to `ctx.api` answers
3003
+ **405**. A **public POST** (a browser form or `fetch`) is
3004
+ a route **action** export, not an `inProcessClient(backend).post()`
3005
+ call.
3006
+
3007
+ ```ts
3008
+ // TWO ways to handle a request — pick by who is calling.
3009
+ import type { LoaderContext } from "@nifrajs/web"
3010
+
3011
+ // (1) A page's own data + mutations → route exports. A PUBLIC POST is a route ACTION
3012
+ // (routes/contact.tsx) — the browser POSTs the route's own URL, nifra runs `action`:
3013
+ export async function action(ctx: LoaderContext) {
3014
+ const form = await ctx.request.formData() // the browser POSTed this route
3015
+ return { ok: true, body: String(form.get("body")) } // → props.actionData on re-render
3016
+ }
3017
+
3018
+ // (2) ctx.api is the IN-PROCESS backend client (the inProcessClient(server) the app was given).
3019
+ // It is LOADERS-ONLY: the SSR backend registers GET handlers, so a .post() to ctx.api 405s.
3020
+ export async function loader(ctx: LoaderContext) {
3021
+ // ✅ a GET in-process — no HTTP hop. (A .post() to ctx.api would 405; write an `action` instead.)
3022
+ return { id: ctx.params.id }
3023
+ }
3024
+
3025
+ // Rule of thumb: data INTO a page → loader + ctx.api (GET). A mutation FROM the browser
3026
+ // (a form/fetch POST) → an `action` export on the route. Don't `inProcessClient(api).post()`.
3027
+ ```
3028
+
3029
+ ### Dynamic routes & the 404 / control-flow rule
3030
+
3031
+ A `[param]` route's loader runs for _any_ value on plain SSR —
3032
+ `getStaticPaths`'s `fallback: "404"` is enforced only by the prerender /
3033
+ CDN layer, never by the on-demand worker. So a loader that can't find its record
3034
+ **must** 404 itself. The mechanism is the throw contract:
3035
+
3036
+ ```ts
3037
+ // routes/users/[id].tsx — plain SSR runs the loader for ANY :id value. `fallback: "404"`
3038
+ // only takes effect under prerender/CDN, so on on-demand SSR you MUST guard and 404 yourself.
3039
+ import type { LoaderContext } from "@nifrajs/web"
3040
+
3041
+ declare function lookupUser(id: string): Promise<{ id: string } | null>
3042
+
3043
+ export async function loader(ctx: LoaderContext) {
3044
+ // ctx.params.id is `string | undefined` (an optional segment) — default it before the lookup.
3045
+ const user = await lookupUser(ctx.params.id ?? "")
3046
+ if (!user) {
3047
+ // CONTROL FLOW: a thrown Response is an INTENTIONAL HTTP response — it propagates untouched
3048
+ // (no _error boundary, no 500). This is how you 404 a missing record.
3049
+ throw new Response("Not Found", { status: 404 })
3050
+ }
3051
+ return { user }
3052
+ }
3053
+
3054
+ declare function lookupUser(id: string): Promise<{ name: string } | null>
3055
+
3056
+ // The throw contract, stated once:
3057
+ // • throw a Response → that exact HTTP response is sent (404 / redirect() / 403 / …). Control flow.
3058
+ // • throw an Error → renders the nearest _error boundary if one exists, else a 500. A bug.
3059
+ // So NEVER `throw new Error("not found")` to mean 404 — that is a 500. Throw a 404 Response.
3060
+ ```
3061
+
3062
+ **Throwing a `Response` is control flow** — that exact HTTP response
3063
+ (a 404, a `redirect()`, a 403) is sent untouched, bypassing the
3064
+ `_error` boundary. **Throwing an `Error` is a fault** — it
3065
+ renders the nearest `_error` boundary (or a 500 if there is none). Never throw an
3066
+ `Error` to signal a 404.
3067
+
3068
+ ### Meta & head rendering rules
3069
+
3070
+ A route's `<head>` is its **layout chain's** head merged with
3071
+ the page's — so a `_layout.tsx` can export `meta` to put sitewide tags
3072
+ (`hreflang`, `preconnect`, a section `<title>`) on every
3073
+ page below it.
3074
+
3075
+ ```ts
3076
+ // routes/_layout.tsx — a layout can export `meta` too. Its tags are SITEWIDE: they land in
3077
+ // the <head> of every page below it — the home for hreflang / preconnect / a section <title>.
3078
+ export const meta = {
3079
+ link: [
3080
+ { rel: "preconnect", href: "https://cdn.example.com", crossorigin: "anonymous" },
3081
+ { rel: "alternate", hreflang: "es", href: "https://example.com/es" },
3082
+ ],
3083
+ title: "Docs", // section default — a child page's title overrides it
3084
+ }
3085
+
3086
+ // The route's final <head> = its LAYOUT CHAIN's meta merged with the page's:
3087
+ // • title (and other scalars): NEAREST-WINS — page overrides inner layout overrides outer.
3088
+ // An undefined page title keeps the layout's.
3089
+ // • meta / link arrays: CONCATENATED outermost-layout → … → page (so the page's canonical
3090
+ // comes after the layout's links).
3091
+ // <link> attributes are typed (LinkDescriptor: a partial like { rel, href, hreflang } is assignable),
3092
+ // name-validated (any letter/digit/hyphen name) and value-escaped against XSS — so
3093
+ // rel/href/hreflang/crossorigin/media/sizes/as/integrity/fetchpriority/… all survive. A boolean
3094
+ // attribute (e.g. `disabled: true`) renders bare; `false`/`undefined` is omitted.
3095
+ ```
3096
+
3097
+ The merge is **nearest-wins** for scalars (the page's `title` beats an
3098
+ inner layout's, which beats an outer one) and **concatenated** for the
3099
+ `meta` / `link` arrays (outermost layout first, page last). A
3100
+ `link` entry is a typed `LinkDescriptor` — a partial like
3101
+ `{ rel, href, hreflang }` is assignable, custom / `data-*` attrs
3102
+ pass through, and every `<link>` attribute with a normal name —
3103
+ `rel`, `href`, `hreflang`, `crossorigin`,
3104
+ `media`, `sizes`, `type`, `as`,
3105
+ `integrity`, `referrerpolicy`, `fetchpriority`,
3106
+ `imagesrcset` — survives into the SSR'd tag; values are HTML-escaped against XSS.
3107
+
3108
+ ### SEO: Open Graph, Twitter cards & JSON-LD
3109
+
3110
+ A route's `meta` emits the full social / structured-data head with three public
3111
+ helpers — `canonical()`, `openGraph()`, and `jsonLd()` — and
3112
+ builds **absolute** URLs from the `origin` argument. Because
3113
+ `meta()` runs in **both** SSR and client navigation it has
3114
+ **no server env**: never read `process.env` or the request inside it.
3115
+ The framework injects `origin` (the site's scheme + host, e.g.
3116
+ `https://news.example.com`) from the request on the server and from
3117
+ `location.origin` on the client, so the two match and an absolute
3118
+ `og:url` / `canonical` / `og:image` never drifts between the
3119
+ server-rendered `<head>` and a soft-nav. Anything else server-only (an API
3120
+ base, a CDN host) belongs in the `loader`, not `meta()`.
3121
+
3122
+ ```ts
3123
+ // routes/articles/[slug].tsx — a complete SEO head: canonical + Open Graph + Twitter + JSON-LD,
3124
+ // with ABSOLUTE URLs built from `origin`. The three helpers (canonical/openGraph/jsonLd) are public
3125
+ // exports of @nifrajs/web; `origin` is the third MetaArgs field (after data + params).
3126
+ import { canonical, jsonLd, openGraph, type MetaArgs } from "@nifrajs/web"
3127
+
3128
+ // CAVEAT: meta() runs in BOTH SSR and client navigation, so it has NO server env — never read
3129
+ // `process.env` or the request here. The framework injects `origin` (the site's scheme+host, e.g.
3130
+ // "https://news.example.com") server-side from the request AND on the client from location.origin, so
3131
+ // the two match and an absolute og:url/canonical/og:image never drifts between SSR and a soft-nav. For
3132
+ // anything else server-only (an API base, a CDN host), thread it through the LOADER, not meta().
3133
+ export const meta = ({ data, params, origin }: MetaArgs) => {
3134
+ const article = data as { title: string; summary: string; image: string; published: string }
3135
+ const url = `${origin}/articles/${params.slug}` // absolute, from the injected origin
3136
+ const image = `${origin}${article.image}` // og:image MUST be absolute for crawlers
3137
+ return {
3138
+ title: article.title,
3139
+ meta: [
3140
+ { name: "description", content: article.summary },
3141
+ // og:* — openGraph emits only the props you pass (+ og:type, here "article").
3142
+ ...openGraph({
3143
+ title: article.title,
3144
+ description: article.summary,
3145
+ url,
3146
+ image,
3147
+ type: "article",
3148
+ }),
3149
+ // twitter:* — a large-image summary card sharing the same absolute image.
3150
+ { name: "twitter:card", content: "summary_large_image" },
3151
+ { name: "twitter:title", content: article.title },
3152
+ { name: "twitter:description", content: article.summary },
3153
+ { name: "twitter:image", content: image },
3154
+ ],
3155
+ link: [canonical(url)], // <link rel="canonical"> — the authoritative URL
3156
+ // JSON-LD structured data — escaped for safe <script> embedding by the head renderer.
3157
+ script: [
3158
+ jsonLd({
3159
+ "@context": "https://schema.org",
3160
+ "@type": "NewsArticle",
3161
+ headline: article.title,
3162
+ image,
3163
+ datePublished: article.published,
3164
+ mainEntityOfPage: url,
3165
+ }),
3166
+ ],
3167
+ }
3168
+ }
3169
+ ```
3170
+
3171
+ [!NOTE] `og:image` (and the Twitter image) **must be absolute** for
3172
+ crawlers — build it from `origin`. `openGraph()` emits only the
3173
+ properties you pass (plus `og:type`); `jsonLd()`'s payload is escaped for
3174
+ safe `<script>` embedding by the head renderer.
3175
+
3176
+ ### The client ↔ server boundary
3177
+
3178
+ Loaders, actions, and head resolution run on the server. The client receives the loader's
3179
+ _return value_ (serialized into the document), never the loader, the `api`,
3180
+ or `env`. Keep server-only imports out of a route file's top level.
3181
+
3182
+ ```ts
3183
+ // What runs WHERE:
3184
+ //
3185
+ // SERVER (Bun / edge) CLIENT (browser)
3186
+ // ───────────────────── ──────────────────────────
3187
+ // loader / action event handlers, useState/effects
3188
+ // meta / head resolution soft-nav re-fetch of loader data
3189
+ // SSR of the layout chain hydration + Fast Refresh (dev)
3190
+ // backend (api, env, draft) —
3191
+ //
3192
+ // The client gets the loader's RETURN VALUE (serialized into the document), never the loader
3193
+ // itself, the api, or env. So:
3194
+ // 1. Never import server-only modules (Bun, a DB client, secrets) at the top level of a route
3195
+ // file — the bundler pulls it into the client chunk and it crashes / leaks. (The build now
3196
+ // FAILS with a named error if a `node:` built-in reaches a client chunk — see below.)
3197
+ // 2. A client soft-nav re-runs the loader over the network (X-Nifra-Data) and re-merges the
3198
+ // same layout-chain head, so sitewide tags persist across navigation — no page-only flash.
3199
+ ```
3200
+
3201
+ ### The build-output contract
3202
+
3203
+ `nifra build` emits a content-hashed `dist/` plus a
3204
+ `manifest.json` you hand to `createWebApp` (`entry`,
3205
+ `routes` for per-route preload, `css` + `routeStyles` for
3206
+ `<link>` injection).
3207
+
3208
+ ```ts
3209
+ // `nifra build` → buildClient (Bun.build) writes dist/ + manifest.json. The contract:
3210
+ {
3211
+ "entry": "/assets/_nifra-entry-HASH.js", // the bootstrap module script
3212
+ "assets": ["/assets/…"], // every emitted chunk URL
3213
+ "routes": { "users/[id]": ["/assets/_layout-H.js", "/assets/_slug_-H.js"] },
3214
+ "css": ["/assets/app-H.css"], // aggregate stylesheets (fallback)
3215
+ "routeStyles": { "users/[id]": ["/assets/_layout-H.css"] } // per-route CSS (chain only)
3216
+ }
3217
+
3218
+ // Invariants worth knowing:
3219
+ // • Chunk names are URL-PATH-SAFE. A dynamic-route file [slug].tsx would emit `[slug]-H.js`,
3220
+ // whose [ ] make a static server 400 the request. The build renames it (`[slug]` → `_slug_`)
3221
+ // and rewrites every reference, so the lazy import resolves and the route hydrates.
3222
+ // • `process.env` is compiled away. `process.env` → `({})` and `process.env.NODE_ENV` → the
3223
+ // build mode; every other `process.env.X` becomes undefined — EXCEPT names you opt in with the
3224
+ // PUBLIC_ prefix (Vite/Next convention), which are baked in with their value (see below). No
3225
+ // `process is not defined` crash, and no unprefixed (secret) env leaking into the client bundle.
3226
+ // • A `node:` built-in in the CLIENT bundle FAILS the build with a named error (move it behind a
3227
+ // loader/action — server-only). It builds via a browser polyfill otherwise, then breaks at runtime.
3228
+ // • Assets are content-hashed + immutable. Serve /assets/* with a long-lived cache header.
3229
+ ```
3230
+
3231
+ ### Public env in the client bundle
3232
+
3233
+ `process.env` is compiled away in the browser bundle, so a bare read can't crash
3234
+ hydration and an unprefixed (secret) var resolves to `undefined` — it never reaches
3235
+ the client. To expose a value, give it the `PUBLIC_` prefix (the Vite / Next
3236
+ convention) in the build environment and it's baked in by value. The prefix is overridable via
3237
+ `buildClient`'s `publicEnvPrefix` (`""` disables it).
3238
+
3239
+ ```ts
3240
+ // process.env in the CLIENT bundle is compiled at build time:
3241
+ // process.env → ({}) (a bare read won't crash hydration)
3242
+ // process.env.NODE_ENV → "production" (the build mode — frameworks' prod/dev branch)
3243
+ // process.env.SECRET_KEY → undefined (unprefixed → never exposed; no secret leak)
3244
+ // process.env.PUBLIC_API_URL → "https://api.example.com" (PUBLIC_-prefixed → baked in by value)
3245
+ //
3246
+ // So to ship a value to the browser, give it a PUBLIC_ prefix in the BUILD environment:
3247
+ // PUBLIC_API_URL=https://api.example.com bun run build
3248
+ // then read process.env.PUBLIC_API_URL in app code — it becomes a string literal after the build.
3249
+ import { buildClient } from "@nifrajs/web/build"
3250
+
3251
+ // Override the prefix (or disable auto-exposure) via buildClient:
3252
+ await buildClient({
3253
+ routesDir: "./routes",
3254
+ outDir: "./dist",
3255
+ clientModule: "@nifrajs/web-react/client",
3256
+ publicEnvPrefix: "NIFRA_PUBLIC_", // default "PUBLIC_"; "" disables auto-exposure entirely
3257
+ })
3258
+ ```
3259
+
3260
+ ### Static emit (prerender)
3261
+
3262
+ Opt a static route into SSG with `export const prerender = true` (or a dynamic
3263
+ route with `export const getStaticPaths`), then call `prerenderRoutes` —
3264
+ a **public** export of `@nifrajs/web/build`. It drives the app's own
3265
+ `fetch` to write `index.html` + `_data.json` per route;
3266
+ `cloudflarePagesRoutes` emits the `_routes.json` for a hybrid CDN +
3267
+ worker deploy. No need to boot a server and curl it.
3268
+
3269
+ ```ts
3270
+ // build.ts — emit static HTML for opted-in routes. prerenderRoutes + cloudflarePagesRoutes
3271
+ // are PUBLIC (exported from @nifrajs/web/build) — no need to boot a server and curl it.
3272
+ import { buildClient, prerenderRoutes, cloudflarePagesRoutes } from "@nifrajs/web/build"
3273
+ import { discoverRoutes } from "@nifrajs/web/fs"
3274
+
3275
+ // 1. Build the client bundle first (so the app references the hashed entry).
3276
+ await buildClient({ routesDir: "./routes", outDir: "./dist", clientModule: "@nifrajs/web-react/client" })
3277
+
3278
+ // 2. Drive the app's own fetch to render each opted-in route → dist/<path>/index.html + _data.json.
3279
+ // Static routes opt in with `export const prerender = true`; dynamic routes enumerate concrete
3280
+ // params with `export const getStaticPaths` (+ a `fallback: "ssr" | "404"`).
3281
+ const { app } = await import("./server")
3282
+ const { prerendered } = await prerenderRoutes({
3283
+ app, // the built createWebApp (a { fetch } is enough)
3284
+ routes: discoverRoutes("./routes").routes,
3285
+ outDir: "./dist",
3286
+ })
3287
+
3288
+ // 3. Hybrid deploy: a Cloudflare Pages _routes.json that serves the prerendered HTML + _data.json
3289
+ // from the CDN and falls everything else through to the SSR worker.
3290
+ const paths = prerendered.map((p) => p.path)
3291
+ await Bun.write("./dist/_routes.json", JSON.stringify(cloudflarePagesRoutes({ prerendered: paths })))
3292
+ ```
3293
+
3294
+ See [SSG & ISR](/docs/rendering) for prerendering, [Dev & HMR](/docs/dev) for the
3295
+ two dev loops and the `--port` flag, and [Loaders & actions](/docs/data) for the
3296
+ data layer in depth.
3297
+
2587
3298
  ## Nifra — Databases
2588
3299
 
2589
3300
  > Nifra is database-agnostic: use SQLite, Postgres, or any ORM (Drizzle, Prisma, Kysely) from your handlers and loaders, on Bun, Node, Deno, and the edge.
@@ -2644,7 +3355,7 @@ import { Database } from "bun:sqlite" // Node: better-sqlite3 · edge: Tu
2644
3355
 
2645
3356
  // Open the connection ONCE at module scope (the driver pools internally).
2646
3357
  const db = new Database("app.db")
2647
- const byId = db.query<{ id: number; name: string }>("SELECT * FROM users WHERE id = ?")
3358
+ const byId = db.query<{ id: number; name: string }, [number]>("SELECT * FROM users WHERE id = ?")
2648
3359
 
2649
3360
  export const app = server().get("/users/:id", (c) =>
2650
3361
  // Parameterized (?) — never interpolate user input into SQL.
@@ -2772,6 +3483,7 @@ If you prefer parameterized queries over an ORM, use `postgres` or `pg`
2772
3483
  directly:
2773
3484
 
2774
3485
  ```ts
3486
+ // doc-check: skip — illustrates a third-party driver (postgres + drizzle); install those to run it.
2775
3487
  import { server } from "@nifrajs/core"
2776
3488
  import { drizzle } from "drizzle-orm/postgres-js" // or node-postgres / neon-http
2777
3489
  import postgres from "postgres"
@@ -3145,6 +3857,125 @@ backend you can also ship on its own. Start with [Getting started](/docs).
3145
3857
  Moving a backend (Express, Hono, Fastify, Elysia) instead? See
3146
3858
  [Migrating a backend](/docs/migrate-backend).
3147
3859
 
3860
+ ## Nifra — Types-first architecture
3861
+
3862
+ > One schema is the single source of truth: it drives runtime validation, inferred TypeScript types, the no-codegen typed client, an OpenAPI document, and the MCP contract your agents read.
3863
+
3864
+ In Nifra a single schema is the **source of truth**. The same definition drives runtime
3865
+ validation, inferred TypeScript types, the no-codegen typed client, an OpenAPI document, and
3866
+ the contract your coding agents read — so the five never drift apart.
3867
+
3868
+ ### One schema
3869
+
3870
+ Define request inputs and the response shape once with `t`. Nothing here is
3871
+ framework-specific; it's a plain object you attach to a route.
3872
+
3873
+ ```ts
3874
+ import { t } from "@nifrajs/schema"
3875
+
3876
+ // One contract, defined once. Everything below is derived from it.
3877
+ export const GetUser = {
3878
+ params: t.object({ id: t.string() }),
3879
+ response: t.object({
3880
+ id: t.string(),
3881
+ name: t.string(),
3882
+ role: t.union([t.literal("admin"), t.literal("user")]),
3883
+ }),
3884
+ }
3885
+ ```
3886
+
3887
+ ### Runtime validation
3888
+
3889
+ Attach the schema to a route. Path params, query, and body are validated at the runtime
3890
+ boundary _before_ your handler runs — invalid input is rejected with a 400, so the
3891
+ handler only ever sees well-formed data.
3892
+
3893
+ ```ts
3894
+ import { server } from "@nifrajs/core"
3895
+ import { GetUser } from "./schema"
3896
+
3897
+ export const app = server().get("/users/:id", GetUser, (c) => {
3898
+ // c.params.id is typed `string` — parsed from the path and validated at the boundary.
3899
+ return { id: c.params.id, name: "Ada", role: "admin" as const }
3900
+ // ^ the return is checked against GetUser.response — a wrong shape is a tsc error.
3901
+ })
3902
+ ```
3903
+
3904
+ ### Inferred types
3905
+
3906
+ The same schema types the handler: `c.params.id` is `string`, and the
3907
+ return value is checked against `response`. Change the schema and the handler
3908
+ stops compiling until it matches — types and validation can't disagree.
3909
+
3910
+ ### The typed client
3911
+
3912
+ The client is inferred from the server's _type_ — no generators, no build step, no
3913
+ SDK to regenerate. Paths and params autocomplete; the response is typed from the route. A
3914
+ backend change that breaks a call is a compile error on the frontend.
3915
+
3916
+ ```ts
3917
+ import { client } from "@nifrajs/client"
3918
+ import type { app } from "./server" // a TYPE import — server code never ships to the client
3919
+
3920
+ const api = client<typeof app>("https://api.example.com")
3921
+
3922
+ const res = await api.users({ id: "42" }).get() // path + params autocomplete, no codegen
3923
+ if (res.ok) {
3924
+ res.data.name // typed from the route's response schema
3925
+ } else {
3926
+ res.error // client-call failures are returned, never thrown
3927
+ }
3928
+ ```
3929
+
3930
+ ### OpenAPI
3931
+
3932
+ The `openapi()` middleware builds an OpenAPI 3.1 document from your registered
3933
+ routes and their schemas — generated lazily on first request, never hand-written. Pass
3934
+ `ui: true` to also serve a Scalar reference page.
3935
+
3936
+ ```ts
3937
+ import { server } from "@nifrajs/core"
3938
+ import { openapi } from "@nifrajs/middleware"
3939
+ import { GetUser } from "./schema" // the contract defined above
3940
+
3941
+ // Generates an OpenAPI 3.1 document from your registered routes — lazily, on first request.
3942
+ export const app = server()
3943
+ .use(openapi({ info: { title: "My API", version: "1.0.0" }, ui: true }))
3944
+ .get("/users/:id", GetUser, (c) => ({ id: c.params.id, name: "Ada", role: "admin" as const }))
3945
+
3946
+ // → GET /openapi.json (the spec, generated from your schemas)
3947
+ // → GET /reference (a Scalar API-reference page, because `ui: true`)
3948
+ ```
3949
+
3950
+ ### The MCP contract
3951
+
3952
+ The same routes and schemas feed coding agents. `nifra context` prints the live
3953
+ API surface as compact text, and `nifra mcp` serves it over the Model Context
3954
+ Protocol so Claude Code or Cursor read the real contract instead of guessing.
3955
+
3956
+ ```ts
3957
+ $ nifra context # the same contract as compact text — pipe into any agent prompt
3958
+ GET /users/:id → params { id: string } response { id, name, role }
3959
+
3960
+ $ nifra mcp # the same data over an MCP server — Claude Code & Cursor read it
3961
+ ```
3962
+
3963
+ ### Known limitations
3964
+
3965
+ -
3966
+ Validation only covers what you put in a schema. A raw-body, file-upload, or
3967
+ bring-your-own-validation route reads the body directly — cap and validate those yourself
3968
+ (see [Security](/docs/security)'s `c.boundedBody`).
3969
+
3970
+ -
3971
+ The typed client infers from the server _type_, so it needs an
3972
+ `import type` of your app and TypeScript on the frontend. There is no runtime
3973
+ coupling — server code never ships to the client.
3974
+
3975
+ -
3976
+ The generated OpenAPI document is a structural subset of 3.1 derived from your schemas;
3977
+ it reflects exactly what the routes declare, not hand-authored prose.
3978
+
3148
3979
  ---
3149
3980
 
3150
3981
  # Package reference (READMEs)
@@ -3168,6 +3999,14 @@ bun add @nifrajs/auth
3168
3999
 
3169
4000
  MIT
3170
4001
 
4002
+ ## For AI agents
4003
+
4004
+ Start with [`LLM.md`](./LLM.md) — this package's contract card (the exports you call + its footguns),
4005
+ one cheap read instead of the whole corpus. For the wider framework: the repo's
4006
+ [`AGENTS.md`](../../AGENTS.md) is the copy-paste quick reference, and
4007
+ [`llms-full.txt`](../../llms-full.txt) is the full machine-readable corpus. Run `nifra check` as the
4008
+ done-gate, or `nifra mcp` to give the agent live project tools.
4009
+
3171
4010
  ## @nifrajs/better-auth
3172
4011
 
3173
4012
  Mount [better-auth](https://better-auth.com) into a [nifra](https://github.com/nifra) app: one
@@ -3261,6 +4100,14 @@ export const loader = async ({ request }) => {
3261
4100
 
3262
4101
  MIT
3263
4102
 
4103
+ ## For AI agents
4104
+
4105
+ Start with [`LLM.md`](./LLM.md) — this package's contract card (the exports you call + its footguns),
4106
+ one cheap read instead of the whole corpus. For the wider framework: the repo's
4107
+ [`AGENTS.md`](../../AGENTS.md) is the copy-paste quick reference, and
4108
+ [`llms-full.txt`](../../llms-full.txt) is the full machine-readable corpus. Run `nifra check` as the
4109
+ done-gate, or `nifra mcp` to give the agent live project tools.
4110
+
3264
4111
  ## @nifrajs/cli
3265
4112
 
3266
4113
  The `nifra` CLI — zero-config dev/build/start for a nifra app, plus the agent surfaces (`nifra context`,
@@ -3361,6 +4208,14 @@ claude mcp add nifra -- nifra mcp
3361
4208
  Run the client from (or point it at) your project root. The protocol is hand-rolled
3362
4209
  (newline-delimited JSON-RPC 2.0) — no SDK dependency.
3363
4210
 
4211
+ ## For AI agents
4212
+
4213
+ Start with [`LLM.md`](./LLM.md) — this package's contract card (the exports you call + its footguns),
4214
+ one cheap read instead of the whole corpus. For the wider framework: the repo's
4215
+ [`AGENTS.md`](../../AGENTS.md) is the copy-paste quick reference, and
4216
+ [`llms-full.txt`](../../llms-full.txt) is the full machine-readable corpus. Run `nifra check` as the
4217
+ done-gate, or `nifra mcp` to give the agent live project tools.
4218
+
3364
4219
  ## @nifrajs/client
3365
4220
 
3366
4221
  A browser-safe, **end-to-end-typed** client for [nifra](../../README.md) servers — an
@@ -3392,6 +4247,14 @@ const { ok, status, data, error } = await api.users({ id: "42" }).get()
3392
4247
 
3393
4248
  ESM-only. MIT.
3394
4249
 
4250
+ ## For AI agents
4251
+
4252
+ Start with [`LLM.md`](./LLM.md) — this package's contract card (the exports you call + its footguns),
4253
+ one cheap read instead of the whole corpus. For the wider framework: the repo's
4254
+ [`AGENTS.md`](../../AGENTS.md) is the copy-paste quick reference, and
4255
+ [`llms-full.txt`](../../llms-full.txt) is the full machine-readable corpus. Run `nifra check` as the
4256
+ done-gate, or `nifra mcp` to give the agent live project tools.
4257
+
3395
4258
  ## @nifrajs/content
3396
4259
 
3397
4260
  Typed, schema-validated content collections for nifra — Markdown + frontmatter, framework-agnostic.
@@ -3411,6 +4274,14 @@ bun add @nifrajs/content
3411
4274
 
3412
4275
  MIT
3413
4276
 
4277
+ ## For AI agents
4278
+
4279
+ Start with [`LLM.md`](./LLM.md) — this package's contract card (the exports you call + its footguns),
4280
+ one cheap read instead of the whole corpus. For the wider framework: the repo's
4281
+ [`AGENTS.md`](../../AGENTS.md) is the copy-paste quick reference, and
4282
+ [`llms-full.txt`](../../llms-full.txt) is the full machine-readable corpus. Run `nifra check` as the
4283
+ done-gate, or `nifra mcp` to give the agent live project tools.
4284
+
3414
4285
  ## @nifrajs/core
3415
4286
 
3416
4287
  The Bun-native, contract-first HTTP framework at the heart of [nifra](../../README.md):
@@ -3447,6 +4318,14 @@ export type App = typeof app // hand this to @nifrajs/client for end-to-end type
3447
4318
 
3448
4319
  ESM-only; requires Bun at runtime. MIT.
3449
4320
 
4321
+ ## For AI agents
4322
+
4323
+ Start with [`LLM.md`](./LLM.md) — this package's contract card (the exports you call + its footguns),
4324
+ one cheap read instead of the whole corpus. For the wider framework: the repo's
4325
+ [`AGENTS.md`](../../AGENTS.md) is the copy-paste quick reference, and
4326
+ [`llms-full.txt`](../../llms-full.txt) is the full machine-readable corpus. Run `nifra check` as the
4327
+ done-gate, or `nifra mcp` to give the agent live project tools.
4328
+
3450
4329
  ## @nifrajs/cron
3451
4330
 
3452
4331
  Runtime-agnostic **in-process cron** for long-running nifra servers (Bun / Node / Deno). Standard
@@ -3508,6 +4387,14 @@ Workers has no long-lived process, so in-process cron doesn't apply. Use the pla
3508
4387
  trigger instead: `toFetchHandler(app, { scheduled })` from `@nifrajs/core` + a `[triggers]` cron in
3509
4388
  `wrangler.toml`. This package is for Bun/Node/Deno servers.
3510
4389
 
4390
+ ## For AI agents
4391
+
4392
+ Start with [`LLM.md`](./LLM.md) — this package's contract card (the exports you call + its footguns),
4393
+ one cheap read instead of the whole corpus. For the wider framework: the repo's
4394
+ [`AGENTS.md`](../../AGENTS.md) is the copy-paste quick reference, and
4395
+ [`llms-full.txt`](../../llms-full.txt) is the full machine-readable corpus. Run `nifra check` as the
4396
+ done-gate, or `nifra mcp` to give the agent live project tools.
4397
+
3511
4398
  ## @nifrajs/deno
3512
4399
 
3513
4400
  Run a [nifra](../../README.md) app on [Deno](https://deno.com) via `Deno.serve`.
@@ -3544,6 +4431,14 @@ cap live inside `app.fetch`, so they apply through this adapter automatically.
3544
4431
 
3545
4432
  Works with any `{ fetch(req): Promise<Response> }` handler, not just nifra. ESM/TS. MIT.
3546
4433
 
4434
+ ## For AI agents
4435
+
4436
+ Start with [`LLM.md`](./LLM.md) — this package's contract card (the exports you call + its footguns),
4437
+ one cheap read instead of the whole corpus. For the wider framework: the repo's
4438
+ [`AGENTS.md`](../../AGENTS.md) is the copy-paste quick reference, and
4439
+ [`llms-full.txt`](../../llms-full.txt) is the full machine-readable corpus. Run `nifra check` as the
4440
+ done-gate, or `nifra mcp` to give the agent live project tools.
4441
+
3547
4442
  ## @nifrajs/env
3548
4443
 
3549
4444
  Typed, validated environment variables. Define a schema once at startup; get a **frozen, typed**
@@ -3612,6 +4507,14 @@ pass the request's bindings: `defineEnv(shape, { source: env })`.
3612
4507
  Error messages name the offending variable and the reason — **never its value** (it may be a
3613
4508
  secret). `defineEnv` returns a frozen object, so a validated config can't be mutated downstream.
3614
4509
 
4510
+ ## For AI agents
4511
+
4512
+ Start with [`LLM.md`](./LLM.md) — this package's contract card (the exports you call + its footguns),
4513
+ one cheap read instead of the whole corpus. For the wider framework: the repo's
4514
+ [`AGENTS.md`](../../AGENTS.md) is the copy-paste quick reference, and
4515
+ [`llms-full.txt`](../../llms-full.txt) is the full machine-readable corpus. Run `nifra check` as the
4516
+ done-gate, or `nifra mcp` to give the agent live project tools.
4517
+
3615
4518
  ## @nifrajs/i18n
3616
4519
 
3617
4520
  Framework-agnostic i18n for nifra — locale negotiation + a tiny ICU message formatter on the platform Intl. Dependency-free.
@@ -3631,6 +4534,14 @@ bun add @nifrajs/i18n
3631
4534
 
3632
4535
  MIT
3633
4536
 
4537
+ ## For AI agents
4538
+
4539
+ Start with [`LLM.md`](./LLM.md) — this package's contract card (the exports you call + its footguns),
4540
+ one cheap read instead of the whole corpus. For the wider framework: the repo's
4541
+ [`AGENTS.md`](../../AGENTS.md) is the copy-paste quick reference, and
4542
+ [`llms-full.txt`](../../llms-full.txt) is the full machine-readable corpus. Run `nifra check` as the
4543
+ done-gate, or `nifra mcp` to give the agent live project tools.
4544
+
3634
4545
  ## @nifrajs/image
3635
4546
 
3636
4547
  Image optimization for nifra — a CLS-safe responsive <Image> + pluggable loaders (CDN or self-hosted) + pure-JS dimension reading. Zero-dependency core; optional Bun.Image-backed resize endpoint at @nifrajs/image/server.
@@ -3650,6 +4561,14 @@ bun add @nifrajs/image
3650
4561
 
3651
4562
  MIT
3652
4563
 
4564
+ ## For AI agents
4565
+
4566
+ Start with [`LLM.md`](./LLM.md) — this package's contract card (the exports you call + its footguns),
4567
+ one cheap read instead of the whole corpus. For the wider framework: the repo's
4568
+ [`AGENTS.md`](../../AGENTS.md) is the copy-paste quick reference, and
4569
+ [`llms-full.txt`](../../llms-full.txt) is the full machine-readable corpus. Run `nifra check` as the
4570
+ done-gate, or `nifra mcp` to give the agent live project tools.
4571
+
3653
4572
  ## @nifrajs/islets
3654
4573
 
3655
4574
  Fine-grained signals + declarative DOM bindings for islands — **interactivity in ~1.4 KB gz**, no
@@ -3712,6 +4631,14 @@ into one flush. Island-scale by design: if a widget outgrows this (lists, owners
3712
4631
  async orchestration), that's the signal to use the Solid adapter for that page — both tiers are
3713
4632
  first-class.
3714
4633
 
4634
+ ## For AI agents
4635
+
4636
+ Start with [`LLM.md`](./LLM.md) — this package's contract card (the exports you call + its footguns),
4637
+ one cheap read instead of the whole corpus. For the wider framework: the repo's
4638
+ [`AGENTS.md`](../../AGENTS.md) is the copy-paste quick reference, and
4639
+ [`llms-full.txt`](../../llms-full.txt) is the full machine-readable corpus. Run `nifra check` as the
4640
+ done-gate, or `nifra mcp` to give the agent live project tools.
4641
+
3715
4642
  ## @nifrajs/middleware
3716
4643
 
3717
4644
  Composable, dependency-light middleware for [nifra](../../README.md) — CORS, security headers, body
@@ -3792,6 +4719,14 @@ const app = server({ requestTimeoutMs: 5_000 })
3792
4719
 
3793
4720
  `@nifrajs/core` is a peer dependency. ESM-only. MIT.
3794
4721
 
4722
+ ## For AI agents
4723
+
4724
+ Start with [`LLM.md`](./LLM.md) — this package's contract card (the exports you call + its footguns),
4725
+ one cheap read instead of the whole corpus. For the wider framework: the repo's
4726
+ [`AGENTS.md`](../../AGENTS.md) is the copy-paste quick reference, and
4727
+ [`llms-full.txt`](../../llms-full.txt) is the full machine-readable corpus. Run `nifra check` as the
4728
+ done-gate, or `nifra mcp` to give the agent live project tools.
4729
+
3795
4730
  ## @nifrajs/node
3796
4731
 
3797
4732
  Run a [nifra](../../README.md) app on Node's `http` server — nifra's ergonomics, deployed
@@ -3855,6 +4790,14 @@ with no extra wiring. Slow-client / slow-loris protection is Node's built-in
3855
4790
 
3856
4791
  Works with any `{ fetch(req): Promise<Response> }` handler, not just nifra. ESM-only. MIT.
3857
4792
 
4793
+ ## For AI agents
4794
+
4795
+ Start with [`LLM.md`](./LLM.md) — this package's contract card (the exports you call + its footguns),
4796
+ one cheap read instead of the whole corpus. For the wider framework: the repo's
4797
+ [`AGENTS.md`](../../AGENTS.md) is the copy-paste quick reference, and
4798
+ [`llms-full.txt`](../../llms-full.txt) is the full machine-readable corpus. Run `nifra check` as the
4799
+ done-gate, or `nifra mcp` to give the agent live project tools.
4800
+
3858
4801
  ## @nifrajs/otel
3859
4802
 
3860
4803
  Distributed tracing for nifra. The `tracing()` plugin continues (or starts) a **W3C trace** per
@@ -3907,6 +4850,14 @@ Use `traceparent` and the built-in semantic attributes in every request span, th
3907
4850
  your own collector through an exporter. Keep the package edge-safe by installing the OpenTelemetry
3908
4851
  SDK only in apps that need that exporter.
3909
4852
 
4853
+ ## For AI agents
4854
+
4855
+ Start with [`LLM.md`](./LLM.md) — this package's contract card (the exports you call + its footguns),
4856
+ one cheap read instead of the whole corpus. For the wider framework: the repo's
4857
+ [`AGENTS.md`](../../AGENTS.md) is the copy-paste quick reference, and
4858
+ [`llms-full.txt`](../../llms-full.txt) is the full machine-readable corpus. Run `nifra check` as the
4859
+ done-gate, or `nifra mcp` to give the agent live project tools.
4860
+
3910
4861
  ## @nifrajs/runner
3911
4862
 
3912
4863
  Run requests through a nifra app and capture **structured results** — the shared engine behind the
@@ -3937,6 +4888,14 @@ It's a **runner, not a security sandbox**: it calls the app you hand it in-proce
3937
4888
  Isolation, when you need it, comes from the host — the browser tab for the playground, your own
3938
4889
  process/CI for the agent runner. Don't feed it code you wouldn't already run.
3939
4890
 
4891
+ ## For AI agents
4892
+
4893
+ Start with [`LLM.md`](./LLM.md) — this package's contract card (the exports you call + its footguns),
4894
+ one cheap read instead of the whole corpus. For the wider framework: the repo's
4895
+ [`AGENTS.md`](../../AGENTS.md) is the copy-paste quick reference, and
4896
+ [`llms-full.txt`](../../llms-full.txt) is the full machine-readable corpus. Run `nifra check` as the
4897
+ done-gate, or `nifra mcp` to give the agent live project tools.
4898
+
3940
4899
  ## @nifrajs/schema
3941
4900
 
3942
4901
  The optional, batteries-included schema builder for [nifra](../../README.md): `t` is
@@ -3988,6 +4947,14 @@ are still emitted without body/response detail (Standard Schema exposes no JSON
3988
4947
 
3989
4948
  `@nifrajs/core` is a peer dependency. ESM-only. MIT.
3990
4949
 
4950
+ ## For AI agents
4951
+
4952
+ Start with [`LLM.md`](./LLM.md) — this package's contract card (the exports you call + its footguns),
4953
+ one cheap read instead of the whole corpus. For the wider framework: the repo's
4954
+ [`AGENTS.md`](../../AGENTS.md) is the copy-paste quick reference, and
4955
+ [`llms-full.txt`](../../llms-full.txt) is the full machine-readable corpus. Run `nifra check` as the
4956
+ done-gate, or `nifra mcp` to give the agent live project tools.
4957
+
3991
4958
  ## @nifrajs/uploads
3992
4959
 
3993
4960
  File-upload hardening for nifra — magic-byte MIME detection, size + type validation, short-TTL signed download URLs, and EXIF/metadata stripping. Dependency-free, edge-safe.
@@ -4007,6 +4974,14 @@ bun add @nifrajs/uploads
4007
4974
 
4008
4975
  MIT
4009
4976
 
4977
+ ## For AI agents
4978
+
4979
+ Start with [`LLM.md`](./LLM.md) — this package's contract card (the exports you call + its footguns),
4980
+ one cheap read instead of the whole corpus. For the wider framework: the repo's
4981
+ [`AGENTS.md`](../../AGENTS.md) is the copy-paste quick reference, and
4982
+ [`llms-full.txt`](../../llms-full.txt) is the full machine-readable corpus. Run `nifra check` as the
4983
+ done-gate, or `nifra mcp` to give the agent live project tools.
4984
+
4010
4985
  ## @nifrajs/web
4011
4986
 
4012
4987
  Framework-agnostic SSR core for nifra — the render seam + HTML document orchestration.
@@ -4026,6 +5001,14 @@ bun add @nifrajs/web
4026
5001
 
4027
5002
  MIT
4028
5003
 
5004
+ ## For AI agents
5005
+
5006
+ Start with [`LLM.md`](./LLM.md) — this package's contract card (the exports you call + its footguns),
5007
+ one cheap read instead of the whole corpus. For the wider framework: the repo's
5008
+ [`AGENTS.md`](../../AGENTS.md) is the copy-paste quick reference, and
5009
+ [`llms-full.txt`](../../llms-full.txt) is the full machine-readable corpus. Run `nifra check` as the
5010
+ done-gate, or `nifra mcp` to give the agent live project tools.
5011
+
4029
5012
  ## @nifrajs/web-preact
4030
5013
 
4031
5014
  Preact render adapter for @nifrajs/web — streaming SSR + hydration (no build plugin).
@@ -4045,6 +5028,14 @@ bun add @nifrajs/web-preact
4045
5028
 
4046
5029
  MIT
4047
5030
 
5031
+ ## For AI agents
5032
+
5033
+ Start with [`LLM.md`](./LLM.md) — this package's contract card (the exports you call + its footguns),
5034
+ one cheap read instead of the whole corpus. For the wider framework: the repo's
5035
+ [`AGENTS.md`](../../AGENTS.md) is the copy-paste quick reference, and
5036
+ [`llms-full.txt`](../../llms-full.txt) is the full machine-readable corpus. Run `nifra check` as the
5037
+ done-gate, or `nifra mcp` to give the agent live project tools.
5038
+
4048
5039
  ## @nifrajs/web-react
4049
5040
 
4050
5041
  React render adapter for @nifrajs/web — SSR + hydration (Bun-native JSX, no Babel).
@@ -4064,6 +5055,14 @@ bun add @nifrajs/web-react
4064
5055
 
4065
5056
  MIT
4066
5057
 
5058
+ ## For AI agents
5059
+
5060
+ Start with [`LLM.md`](./LLM.md) — this package's contract card (the exports you call + its footguns),
5061
+ one cheap read instead of the whole corpus. For the wider framework: the repo's
5062
+ [`AGENTS.md`](../../AGENTS.md) is the copy-paste quick reference, and
5063
+ [`llms-full.txt`](../../llms-full.txt) is the full machine-readable corpus. Run `nifra check` as the
5064
+ done-gate, or `nifra mcp` to give the agent live project tools.
5065
+
4067
5066
  ## @nifrajs/web-solid
4068
5067
 
4069
5068
  Solid render adapter for @nifrajs/web — SSR + hydration + the Solid Babel build plugin.
@@ -4083,6 +5082,14 @@ bun add @nifrajs/web-solid
4083
5082
 
4084
5083
  MIT
4085
5084
 
5085
+ ## For AI agents
5086
+
5087
+ Start with [`LLM.md`](./LLM.md) — this package's contract card (the exports you call + its footguns),
5088
+ one cheap read instead of the whole corpus. For the wider framework: the repo's
5089
+ [`AGENTS.md`](../../AGENTS.md) is the copy-paste quick reference, and
5090
+ [`llms-full.txt`](../../llms-full.txt) is the full machine-readable corpus. Run `nifra check` as the
5091
+ done-gate, or `nifra mcp` to give the agent live project tools.
5092
+
4086
5093
  ## @nifrajs/web-svelte
4087
5094
 
4088
5095
  Svelte 5 render adapter for @nifrajs/web — SSR + hydration + the .svelte compiler Bun-plugin.
@@ -4102,6 +5109,14 @@ bun add @nifrajs/web-svelte
4102
5109
 
4103
5110
  MIT
4104
5111
 
5112
+ ## For AI agents
5113
+
5114
+ Start with [`LLM.md`](./LLM.md) — this package's contract card (the exports you call + its footguns),
5115
+ one cheap read instead of the whole corpus. For the wider framework: the repo's
5116
+ [`AGENTS.md`](../../AGENTS.md) is the copy-paste quick reference, and
5117
+ [`llms-full.txt`](../../llms-full.txt) is the full machine-readable corpus. Run `nifra check` as the
5118
+ done-gate, or `nifra mcp` to give the agent live project tools.
5119
+
4105
5120
  ## @nifrajs/web-vanilla
4106
5121
 
4107
5122
  Zero-framework render adapter for @nifrajs/web — auto-escaping tagged-template HTML, ~0 KB client JS, interactivity via @nifrajs/web/islands.
@@ -4121,6 +5136,14 @@ bun add @nifrajs/web-vanilla
4121
5136
 
4122
5137
  MIT
4123
5138
 
5139
+ ## For AI agents
5140
+
5141
+ Start with [`LLM.md`](./LLM.md) — this package's contract card (the exports you call + its footguns),
5142
+ one cheap read instead of the whole corpus. For the wider framework: the repo's
5143
+ [`AGENTS.md`](../../AGENTS.md) is the copy-paste quick reference, and
5144
+ [`llms-full.txt`](../../llms-full.txt) is the full machine-readable corpus. Run `nifra check` as the
5145
+ done-gate, or `nifra mcp` to give the agent live project tools.
5146
+
4124
5147
  ## @nifrajs/web-vue
4125
5148
 
4126
5149
  Vue render adapter for @nifrajs/web — streaming SSR + hydration, with a .vue SFC compiler Bun plugin (@nifrajs/web-vue/plugin).
@@ -4140,6 +5163,14 @@ bun add @nifrajs/web-vue
4140
5163
 
4141
5164
  MIT
4142
5165
 
5166
+ ## For AI agents
5167
+
5168
+ Start with [`LLM.md`](./LLM.md) — this package's contract card (the exports you call + its footguns),
5169
+ one cheap read instead of the whole corpus. For the wider framework: the repo's
5170
+ [`AGENTS.md`](../../AGENTS.md) is the copy-paste quick reference, and
5171
+ [`llms-full.txt`](../../llms-full.txt) is the full machine-readable corpus. Run `nifra check` as the
5172
+ done-gate, or `nifra mcp` to give the agent live project tools.
5173
+
4143
5174
  ## @nifrajs/workers
4144
5175
 
4145
5176
  Cloudflare Workers helpers for nifra — a Durable Object hub for cross-connection WebSocket broadcast.
@@ -4159,6 +5190,14 @@ bun add @nifrajs/workers
4159
5190
 
4160
5191
  MIT
4161
5192
 
5193
+ ## For AI agents
5194
+
5195
+ Start with [`LLM.md`](./LLM.md) — this package's contract card (the exports you call + its footguns),
5196
+ one cheap read instead of the whole corpus. For the wider framework: the repo's
5197
+ [`AGENTS.md`](../../AGENTS.md) is the copy-paste quick reference, and
5198
+ [`llms-full.txt`](../../llms-full.txt) is the full machine-readable corpus. Run `nifra check` as the
5199
+ done-gate, or `nifra mcp` to give the agent live project tools.
5200
+
4162
5201
  ## create-nifra
4163
5202
 
4164
5203
  Scaffold a new [nifra](../../README.md) app.
@@ -4193,6 +5232,14 @@ bun run dev
4193
5232
 
4194
5233
  MIT.
4195
5234
 
5235
+ ## For AI agents
5236
+
5237
+ Start with [`LLM.md`](./LLM.md) — this package's contract card (the exports you call + its footguns),
5238
+ one cheap read instead of the whole corpus. For the wider framework: the repo's
5239
+ [`AGENTS.md`](../../AGENTS.md) is the copy-paste quick reference, and
5240
+ [`llms-full.txt`](../../llms-full.txt) is the full machine-readable corpus. Run `nifra check` as the
5241
+ done-gate, or `nifra mcp` to give the agent live project tools.
5242
+
4196
5243
  ## nifra
4197
5244
 
4198
5245
  The unscoped meta-entry for the **nifra** full-stack framework. It re-exports [`@nifrajs/core`](https://www.npmjs.com/package/@nifrajs/core), so:
@@ -4220,6 +5267,14 @@ Docs + an AI-readable reference: [`/llms.txt`](https://nifra.dev/llms.txt) · [`
4220
5267
 
4221
5268
  MIT.
4222
5269
 
5270
+ ## For AI agents
5271
+
5272
+ Start with [`LLM.md`](./LLM.md) — this package's contract card (the exports you call + its footguns),
5273
+ one cheap read instead of the whole corpus. For the wider framework: the repo's
5274
+ [`AGENTS.md`](../../AGENTS.md) is the copy-paste quick reference, and
5275
+ [`llms-full.txt`](../../llms-full.txt) is the full machine-readable corpus. Run `nifra check` as the
5276
+ done-gate, or `nifra mcp` to give the agent live project tools.
5277
+
4223
5278
  ---
4224
5279
 
4225
5280
  # Complete export index (generated from each package's src/index.ts)
@@ -4294,7 +5349,7 @@ MIT.
4294
5349
 
4295
5350
  ## @nifrajs/web
4296
5351
 
4297
- `Action`, `ACTION_GLOBAL`, `buildManifest`, `CachedResponse`, `CacheStore`, `clientEntry`, `ClientRouter`, `ClientRouterOptions`, `createClientRouter`, `createMatcher`, `createQueryClient`, `createWebApp`, `CreateWebAppOptions`, `DATA_GLOBAL`, `DATA_HEADER`, `defer`, `Deferred`, `disableDraft`, `DRAFT_COOKIE`, `DraftCookieControls`, `enableDraft`, `EnableDraftOptions`, `enumeratePrerenderedPaths`, `enumerateStaticRoutes`, `Fetcher`, `FetcherState`, `FetchRouteData`, `filePathToPattern`, `filePathToPatterns`, `FontDisplay`, `fontFace`, `FontFace`, `fontPreload`, `FontPreloadInput`, `FontSource`, `generateClientEntry`, `GenerateClientEntryOptions`, `generateServerManifest`, `GenerateServerManifestOptions`, `GetStaticPaths`, `hashQueryKey`, `isDraftEnabled`, `ISR_REVALIDATE_HEADER`, `ISR_STATUS_HEADER`, `ISRApp`, `ISROptions`, `ISRPlatform`, `KVCacheStore`, `KVCacheStoreOptions`, `KVNamespaceLike`, `LayoutEntry`, `Loader`, `LoaderContext`, `manifest`, `Manifest`, `MemoryCacheStore`, `MemoryCacheStoreOptions`, `Meta`, `MetaArgs`, `MetaInput`, `MountRouterOptions`, `QueryClient`, `QueryClientOptions`, `QueryHandle`, `QueryState`, `QueryStatus`, `redirect`, `REDIRECT_HEADER`, `RedirectOptions`, `RenderAdapter`, `RenderedPage`, `renderPage`, `RenderPageOptions`, `renderPageResult`, `RenderProps`, `resolveMeta`, `revalidate`, `REVALIDATE_HEADER`, `revalidateEndpoint`, `RevalidateEndpointOptions`, `RevalidateResult`, `ROUTE_GLOBAL`, `RouteEntry`, `RouteMatch`, `RouteModule`, `RoutePattern`, `RouterState`, `serializeData`, `StaticPath`, `StaticPaths`, `StaticRoutes`, `Submission`, `SubmitOptions`, `withISR`
5352
+ `Action`, `ACTION_GLOBAL`, `apiKey`, `buildManifest`, `CachedResponse`, `CacheStore`, `canonical`, `clientEntry`, `ClientRouter`, `ClientRouterOptions`, `createClientRouter`, `createMatcher`, `createQueryClient`, `createWebApp`, `CreateWebAppOptions`, `DATA_GLOBAL`, `DATA_HEADER`, `DEFAULT_DEV_PORT`, `defer`, `Deferred`, `disableDraft`, `DRAFT_COOKIE`, `DraftCookieControls`, `enableDraft`, `EnableDraftOptions`, `enumeratePrerenderedPaths`, `enumerateStaticRoutes`, `Fetcher`, `FetcherState`, `FetchRouteData`, `filePathToPattern`, `filePathToPatterns`, `FontDisplay`, `fontFace`, `FontFace`, `fontPreload`, `FontPreloadInput`, `FontSource`, `generateClientEntry`, `GenerateClientEntryOptions`, `generateServerManifest`, `GenerateServerManifestOptions`, `GetStaticPaths`, `hashQueryKey`, `isDraftEnabled`, `ISR_REVALIDATE_HEADER`, `ISR_STATUS_HEADER`, `ISRApp`, `ISROptions`, `ISRPlatform`, `jsonLd`, `KVCacheStore`, `KVCacheStoreOptions`, `KVNamespaceLike`, `LayoutEntry`, `LinkDescriptor`, `Loader`, `LoaderContext`, `manifest`, `Manifest`, `MemoryCacheStore`, `MemoryCacheStoreOptions`, `mergeHeads`, `meta`, `Meta`, `MetaArgs`, `MetaInput`, `MountRouterOptions`, `openGraph`, `OpenGraphInput`, `QueryClient`, `QueryClientOptions`, `QueryHandle`, `QueryState`, `QueryStatus`, `redirect`, `REDIRECT_HEADER`, `RedirectOptions`, `RenderAdapter`, `RenderedPage`, `renderPage`, `RenderPageOptions`, `renderPageResult`, `RenderProps`, `resolveMeta`, `revalidate`, `REVALIDATE_HEADER`, `revalidateEndpoint`, `RevalidateEndpointOptions`, `RevalidateResult`, `ROUTE_GLOBAL`, `RouteEntry`, `RouteMatch`, `RouteModule`, `RoutePattern`, `RouterState`, `ScriptDescriptor`, `serializeData`, `ServerOnly`, `StaticPath`, `StaticPaths`, `StaticRoutes`, `Submission`, `SubmitOptions`, `withISR`
4298
5353
 
4299
5354
  ## @nifrajs/web-preact
4300
5355