@chidchanun/bcp 0.1.7 → 0.1.9

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.
package/CHANGELOG.md CHANGED
@@ -2,6 +2,77 @@
2
2
 
3
3
  All notable framework changes are tracked here before release.
4
4
 
5
+ ## 0.1.9 - Server data loaders
6
+
7
+ ### Server rendering and data
8
+
9
+ - Added route-level `loader.ts` / `loader.tsx` files next to `page.tsx` for server-only page data loading before SSR.
10
+ - Added public `useLoaderData<T>()` through `bcp` so the page consumes the same loader value during SSR, browser hydration and later client navigations.
11
+ - Loader context includes matched route `params` and a fresh `URLSearchParams` instance for the target request query string.
12
+ - Loaders run inside the normal request context, so `cookies()`, `headers()`, `requestUrl()`, request/auth helpers and JWT session APIs can be used directly.
13
+ - Loader modules may import application server-only/database modules without placing those dependencies in the browser bundle.
14
+ - Loaders may return either JSON-safe data or a Web `Response`, including `redirect()` responses.
15
+ - Response cookies created while a loader runs are merged into the final page, navigation or redirect response.
16
+ - Loader results are serialized into framework data with the existing script-safe serialization rules and are provided to SSR through an internal loader-data provider.
17
+ - Loader data rejects unsupported values such as functions, symbols, BigInt, non-finite numbers, class instances, Date/Map/Set objects and circular graphs.
18
+ - Having both `loader.ts` and `loader.tsx` next to one page is rejected as an ambiguous route configuration.
19
+
20
+ ### SPA navigation and cache safety
21
+
22
+ - Loader-backed routes now participate in BCP SPA navigation instead of requiring a full document reload.
23
+ - `/_bcp/navigation` executes the target route loader on the server and returns fresh loader data together with route metadata and client asset information.
24
+ - Navigation loaders receive a request context for the target page URL rather than the internal navigation transport URL, preserving target query parameters, cookies, sessions and request helpers.
25
+ - Loader redirects are converted to a navigation redirect payload while preserving all response cookies; same-origin redirects continue through the SPA router and cross-origin redirects use normal browser navigation.
26
+ - Non-navigation Web `Response` values continue to fall back to a document request so their original HTTP semantics remain authoritative.
27
+ - Client navigation explicitly sends same-origin credentials, aborts superseded requests and uses a monotonically increasing navigation sequence so stale responses cannot update router state after a newer navigation has started.
28
+ - Navigation redirect chains are capped at 10 redirects to prevent loops.
29
+ - `useNavigation()` becomes active before the server request starts and route loading markup can be displayed while the target client runtime/assets finish after the payload arrives.
30
+ - Standalone loader navigation is placed inside the existing middleware/security chain rather than bypassing application middleware or framework security gateways.
31
+ - Production response-cache manifests continue to exclude loader-backed pages automatically because loader output may be user/request specific.
32
+ - Navigation payload responses use `Cache-Control: no-store` and `X-Content-Type-Options: nosniff`.
33
+
34
+ ### Production and reliability
35
+
36
+ - Standalone production entries bundle each loader into the server artifact only and attach it to the corresponding page route definition.
37
+ - Added a standalone loader-navigation gateway only when an application actually has loader-backed routes; applications without loaders keep the existing production runtime path.
38
+ - Development loader imports use a server-side cache-busting version so edits are reflected without creating a client dependency edge.
39
+ - Added a `/loader-demo/[id]` fixture covering route params, query params, cookies, request method, request URL, SSR serialization and redirect response cookies.
40
+ - Added unit coverage for loader discovery, execution, response returns, serialization validation, duplicate-loader detection and SPA redirect/cookie transport.
41
+ - Added development integration coverage for SSR loader data plus SPA navigation data, target request context, sessions and redirect cookies.
42
+ - Added standalone production E2E coverage to verify the same SPA loader protocol after production bundling.
43
+ - Expanded package artifact smoke checks to require the client router, dev navigation target runtime, navigation response transport and standalone loader navigation runtime.
44
+ - Added dedicated server data loader documentation and updated the main README/application boundary guidance.
45
+
46
+ ## 0.1.8 - JWT cookie sessions and auth preset
47
+
48
+ ### Authentication
49
+
50
+ - Added `createSessionToken()` and `verifySessionToken()` to `bcp/server` for HS256 JWT session tokens.
51
+ - Added `createSession()` to sign a token and store it in an HttpOnly cookie with secure production defaults.
52
+ - Added `getSession()` to read and verify the active session cookie and return typed claims or `null` for invalid, expired or tampered tokens.
53
+ - Added `destroySession()` to expire the configured session cookie.
54
+ - Session tokens use `BCP_SESSION_SECRET` by default and reject secrets shorter than 32 bytes.
55
+ - Token verification validates HS256 signatures with constant-time comparison plus expiration, optional issuer and optional audience checks.
56
+ - JWT cookie defaults are `bcp_session`, 12-hour lifetime, `HttpOnly`, `SameSite=Lax`, `Path=/`, and `Secure` when `NODE_ENV=production`.
57
+ - Added low-level and high-level session option types through the existing server-only `bcp/server` entrypoint.
58
+
59
+ ### create-bcp-app
60
+
61
+ - Added interactive Authentication selection with `None` and `JWT Cookie` presets.
62
+ - Added the non-interactive `--auth <preset>` option with `none` and `jwt-cookie` values.
63
+ - JWT Cookie projects generate `lib/auth.ts` plus `/api/auth/login`, `/api/auth/logout` and `/api/auth/me` route handlers.
64
+ - Generated auth projects add `BCP_SESSION_SECRET=` to `.env.example`.
65
+ - Generated `authenticateCredentials(email, password)` returns `null` until the application connects its own database lookup and password-hash verification, keeping the starter secure by default.
66
+ - MySQL projects now generate a reusable `db` pool configured through `DB_HOST`, `DB_PORT`, `DB_USER`, `DB_PASSWORD` and `DB_NAME`, with development pool reuse through `globalThis`.
67
+
68
+ ### Reliability
69
+
70
+ - Added unit coverage for token round-trips, signature tampering, wrong secrets, issuer/audience mismatches, expiration, missing/short secrets and cookie lifecycle behavior.
71
+ - Added development integration coverage for login-style session creation, authenticated reads and logout cookie expiration.
72
+ - Added standalone production E2E coverage for the same JWT cookie session lifecycle after production bundling.
73
+ - Added create-app regression coverage for JWT Cookie scaffolding, secure credential-verification defaults and authentication-disabled projects.
74
+ - Added a basic application session API fixture and package-smoke checks for the session runtime and public helper surface.
75
+
5
76
  ## 0.1.7 - Server request context and cookies
6
77
 
7
78
  ### Server APIs
package/README.md CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  BCP Framework is a React full-stack framework with file-based routing, SSR, client navigation, API routes, middleware, metadata, client islands, cache/revalidation, security defaults and standalone production builds.
4
4
 
5
- > Current development version: `0.1.7`. BCP is still pre-1.0 and validates each release candidate before the manual npm publish step.
5
+ > Current development version: `0.1.9`. BCP is still pre-1.0 and validates each release candidate before the manual npm publish step.
6
6
 
7
7
  ## Quick start
8
8
 
@@ -35,6 +35,7 @@ app/
35
35
  ├─ not-found.tsx
36
36
  ├─ users/
37
37
  │ └─ [id]/
38
+ │ ├─ loader.ts
38
39
  │ └─ page.tsx
39
40
  └─ api/
40
41
  └─ hello/
@@ -73,7 +74,7 @@ Server-only modules can declare:
73
74
  import "bcp/server-only";
74
75
  ```
75
76
 
76
- Database helpers generated by `create-bcp-app` include the server-only marker automatically. A server-only helper must not be imported from a page/client graph; put database work behind an API route instead.
77
+ Database helpers generated by `create-bcp-app` include the server-only marker automatically. A server-only helper must not be imported from a hydrated page/client graph. Use an API route or, for page data in BCP 0.1.9+, a route-level `loader.ts` that runs only on the server.
77
78
 
78
79
  See [Application Modules](docs/application-modules.md) for the complete boundary model and examples.
79
80
 
@@ -123,7 +124,7 @@ Supported methods include GET, POST, PUT, PATCH, DELETE, HEAD and OPTIONS. HEAD
123
124
 
124
125
  ## Server request APIs
125
126
 
126
- BCP 0.1.7 adds request-scoped server helpers through `bcp/server`. In the current 0.1.x client pipeline these helpers are intended for API routes and are rejected from page/client module graphs.
127
+ BCP 0.1.7 adds request-scoped server helpers through `bcp/server`. These helpers are server-only and are available from API handlers and server data loaders.
127
128
 
128
129
  ```ts
129
130
  import {
@@ -196,6 +197,189 @@ export async function POST() {
196
197
 
197
198
  `redirect()` defaults to status `307`; supported statuses are `301`, `302`, `303`, `307` and `308`. Use `cookieStore.delete("session")` to expire a cookie. See [Server Request APIs](docs/server-request-apis.md) for proxy trust, URL handling, response helpers, request isolation, cookie options and boundary details.
198
199
 
200
+ ## Server data loaders
201
+
202
+ BCP 0.1.9 adds a server-only `loader.ts` convention next to `page.tsx`. The loader runs before SSR, receives route `params` plus `searchParams`, and can use `bcp/server` APIs, sessions and server-only database modules.
203
+
204
+ ```text
205
+ app/users/[id]/
206
+ ├─ loader.ts
207
+ └─ page.tsx
208
+ ```
209
+
210
+ ```ts
211
+ // app/users/[id]/loader.ts
212
+ import {
213
+ getSession,
214
+ redirect,
215
+ } from "bcp/server";
216
+
217
+ export async function loader({
218
+ params,
219
+ searchParams,
220
+ }: {
221
+ params: {
222
+ id: string;
223
+ };
224
+ searchParams: URLSearchParams;
225
+ }) {
226
+ const session =
227
+ await getSession();
228
+
229
+ if (!session) {
230
+ return redirect(
231
+ "/login",
232
+ 303
233
+ );
234
+ }
235
+
236
+ return {
237
+ id:
238
+ params.id,
239
+ query:
240
+ searchParams.get("q"),
241
+ session,
242
+ };
243
+ }
244
+ ```
245
+
246
+ Pages consume the serializable result with `useLoaderData<T>()`:
247
+
248
+ ```tsx
249
+ "use client";
250
+
251
+ import {
252
+ useLoaderData,
253
+ } from "bcp";
254
+
255
+ export default function UserPage() {
256
+ const data =
257
+ useLoaderData<{
258
+ id: string;
259
+ query: string | null;
260
+ }>();
261
+
262
+ return (
263
+ <main>
264
+ User {data.id}
265
+ </main>
266
+ );
267
+ }
268
+ ```
269
+
270
+ Loader data is validated as JSON-safe before SSR and is serialized into framework data so hydration reads the same value without importing `loader.ts` into the browser graph. A loader may also return a Web `Response`; response cookies are preserved on redirects.
271
+
272
+ Loader-backed pages now use the same SPA navigation model as normal BCP routes. `<Link>`, `navigate()`, `router.push()`, `router.replace()`, history navigation and `router.refresh()` request `/_bcp/navigation`; the server executes the target loader inside a fresh target-page request context and returns current loader data with the route payload. Same-origin loader redirects continue through the router while preserving `Set-Cookie` headers. Superseded navigations are aborted and sequence-checked so stale responses cannot update the active route. Loader pages remain excluded from the production response-cache manifest by default because their output may depend on session, cookie, request identity or user-specific database state.
273
+
274
+ See [Server Data Loaders](docs/server-data-loaders.md) for serialization rules, authentication patterns, redirect behavior, SPA navigation, cache safety and request-context semantics.
275
+
276
+ ## JWT cookie sessions
277
+
278
+ BCP 0.1.8 adds HS256 JWT cookie sessions directly to `bcp/server`. Configure a server-only secret of at least 32 bytes:
279
+
280
+ ```env
281
+ BCP_SESSION_SECRET=replace-this-with-a-long-random-secret-at-least-32-bytes
282
+ ```
283
+
284
+ Login-style APIs can create an HttpOnly session cookie without manually signing or serializing the JWT:
285
+
286
+ ```ts
287
+ import {
288
+ createSession,
289
+ json,
290
+ } from "bcp/server";
291
+
292
+ export async function POST() {
293
+ await createSession(
294
+ {
295
+ userId: 42,
296
+ email: "user@example.com",
297
+ role: "admin",
298
+ },
299
+ {
300
+ issuer: "my-app",
301
+ audience: "my-app-users",
302
+ }
303
+ );
304
+
305
+ return json({
306
+ success: true,
307
+ });
308
+ }
309
+ ```
310
+
311
+ Protected API routes can verify and read the session:
312
+
313
+ ```ts
314
+ import {
315
+ getSession,
316
+ json,
317
+ } from "bcp/server";
318
+
319
+ export async function GET() {
320
+ const session =
321
+ await getSession<{
322
+ userId: number;
323
+ email: string;
324
+ role: string;
325
+ }>({
326
+ issuer: "my-app",
327
+ audience: "my-app-users",
328
+ });
329
+
330
+ if (!session) {
331
+ return json(
332
+ {
333
+ error: "Unauthorized",
334
+ },
335
+ {
336
+ status: 401,
337
+ }
338
+ );
339
+ }
340
+
341
+ return json({
342
+ userId:
343
+ session.userId,
344
+ email:
345
+ session.email,
346
+ role:
347
+ session.role,
348
+ });
349
+ }
350
+ ```
351
+
352
+ Use `destroySession()` to expire the cookie. Low-level `createSessionToken()` and `verifySessionToken()` helpers are also available when an application needs to manage token storage itself. JWT payloads are signed, not encrypted, so sensitive secrets must not be stored inside them. See [JWT Cookie Sessions](docs/session-auth.md) for options and the security model.
353
+
354
+ ## create-bcp-app auth preset
355
+
356
+ BCP 0.1.8 can scaffold the JWT cookie foundation automatically:
357
+
358
+ ```bash
359
+ npx create-bcp-app my-app --database mysql --auth jwt-cookie
360
+ ```
361
+
362
+ Interactive setup offers `None` and `JWT Cookie`. Selecting JWT Cookie creates:
363
+
364
+ ```text
365
+ lib/auth.ts
366
+ app/api/auth/login/route.ts
367
+ app/api/auth/logout/route.ts
368
+ app/api/auth/me/route.ts
369
+ ```
370
+
371
+ and adds:
372
+
373
+ ```env
374
+ BCP_SESSION_SECRET=
375
+ ```
376
+
377
+ to `.env.example`.
378
+
379
+ The generated `authenticateCredentials(email, password)` returns `null` by default. Applications must connect it to their own database lookup and password-hash verification before login can succeed. This is intentional so a newly generated project does not trust user identity supplied directly by the browser.
380
+
381
+ The generated MySQL preset uses a reusable `db` pool and separate `DB_HOST`, `DB_PORT`, `DB_USER`, `DB_PASSWORD` and `DB_NAME` settings.
382
+
199
383
  ## Metadata
200
384
 
201
385
  ```ts
@@ -228,7 +412,7 @@ import {
228
412
  } from "bcp/cache";
229
413
  ```
230
414
 
231
- The current cache implementation is process-local and intentionally does not provide distributed invalidation across multiple Node.js instances.
415
+ The current cache implementation is process-local and intentionally does not provide distributed invalidation across multiple Node.js instances. Loader-backed pages are excluded from the automatic production response-cache manifest in 0.1.9; cache loader data explicitly only when the application has a safe user-aware cache key and invalidation strategy.
232
416
 
233
417
  ## Middleware
234
418
 
@@ -338,6 +522,8 @@ No real npm publish command is run automatically by the repository.
338
522
  - [Getting Started](docs/getting-started.md)
339
523
  - [Application Modules](docs/application-modules.md)
340
524
  - [Server Request APIs](docs/server-request-apis.md)
525
+ - [Server Data Loaders](docs/server-data-loaders.md)
526
+ - [JWT Cookie Sessions](docs/session-auth.md)
341
527
  - [Routing](docs/routing.md)
342
528
  - [Configuration](docs/configuration.md)
343
529
  - [Caching](docs/caching.md)
@@ -8,7 +8,7 @@ import {
8
8
  } from "@/lib/database";
9
9
  ```
10
10
 
11
- The generated `tsconfig.json` maps `@/*` to the project root. BCP also installs a Node.js resolver during development so the same alias works in SSR and API route modules. Production builds resolve the alias through the application bundler.
11
+ The generated `tsconfig.json` maps `@/*` to the project root. BCP also installs a Node.js resolver during development so the same alias works in SSR, loader and API route modules. Production builds resolve the alias through the application bundler.
12
12
 
13
13
  ## Client modules
14
14
 
@@ -79,7 +79,7 @@ import {
79
79
  } from "@/lib/database";
80
80
  ```
81
81
 
82
- Put the database operation behind an API route instead:
82
+ For browser-driven data operations, put the database operation behind an API route:
83
83
 
84
84
  ```ts
85
85
  // app/api/users/route.ts
@@ -111,6 +111,66 @@ const result =
111
111
  await response.json();
112
112
  ```
113
113
 
114
+ ## Server data loaders
115
+
116
+ BCP 0.1.9 adds a server-only page data path. A `loader.ts` next to `page.tsx` is not part of the hydrated page graph and may import server-only database helpers directly.
117
+
118
+ ```text
119
+ app/users/
120
+ ├─ loader.ts
121
+ └─ page.tsx
122
+ ```
123
+
124
+ ```ts
125
+ // app/users/loader.ts
126
+ import {
127
+ db,
128
+ } from "@/lib/database";
129
+
130
+ export async function loader() {
131
+ const [rows] =
132
+ await db.query(
133
+ "SELECT id, name FROM users"
134
+ );
135
+
136
+ return {
137
+ users: rows,
138
+ };
139
+ }
140
+ ```
141
+
142
+ The page consumes only the serializable result:
143
+
144
+ ```tsx
145
+ "use client";
146
+
147
+ import {
148
+ useLoaderData,
149
+ } from "bcp";
150
+
151
+ export default function UsersPage() {
152
+ const data =
153
+ useLoaderData<{
154
+ users: Array<{
155
+ id: number;
156
+ name: string;
157
+ }>;
158
+ }>();
159
+
160
+ return (
161
+ <pre>
162
+ {JSON.stringify(
163
+ data.users,
164
+ null,
165
+ 2
166
+ )}
167
+ </pre>
168
+ );
169
+ }
170
+ ```
171
+
172
+ The loader itself stays server-only; only its validated JSON-safe result is serialized for hydration. See [Server Data Loaders](server-data-loaders.md) for request context, authentication, redirects and cache behavior.
173
+
114
174
  ## Current boundary model
115
175
 
116
- BCP 0.1.x is not a React Server Components implementation. Pages and layouts can participate in browser hydration, so server-only dependencies must remain behind API routes or other server execution paths. The boundary checks are designed to make that rule explicit and prevent accidental database or secret-bearing code from entering client bundles.
176
+ BCP 0.1.x is not a React Server Components implementation. Pages and layouts can participate in browser hydration, so server-only dependencies must remain behind explicit server execution paths: API routes, route-level server data loaders, middleware, or other framework server runtimes. The boundary checks are designed to prevent accidental database or secret-bearing code from entering client bundles.
@@ -0,0 +1,240 @@
1
+ # Server data loaders
2
+
3
+ BCP 0.1.9 adds route-level server data loading for pages. A loader lives next to a page and runs on the server before that page is rendered. Loader-backed routes also participate in BCP client navigation: a navigation request executes the target loader on the server, returns fresh loader data, and renders the next route without requiring a full document reload.
4
+
5
+ ## File convention
6
+
7
+ ```text
8
+ app/
9
+ └─ users/
10
+ └─ [id]/
11
+ ├─ loader.ts
12
+ └─ page.tsx
13
+ ```
14
+
15
+ `loader.ts` must export a named `loader()` function:
16
+
17
+ ```ts
18
+ import {
19
+ getSession,
20
+ redirect,
21
+ } from "bcp/server";
22
+
23
+ export async function loader({
24
+ params,
25
+ searchParams,
26
+ }: {
27
+ params: {
28
+ id: string;
29
+ };
30
+ searchParams: URLSearchParams;
31
+ }) {
32
+ const session =
33
+ await getSession();
34
+
35
+ if (!session) {
36
+ return redirect(
37
+ "/login",
38
+ 303
39
+ );
40
+ }
41
+
42
+ return {
43
+ id:
44
+ params.id,
45
+ filter:
46
+ searchParams.get(
47
+ "filter"
48
+ ),
49
+ user:
50
+ session,
51
+ };
52
+ }
53
+ ```
54
+
55
+ The matching page reads the loader result with `useLoaderData<T>()`:
56
+
57
+ ```tsx
58
+ "use client";
59
+
60
+ import {
61
+ useLoaderData,
62
+ } from "bcp";
63
+
64
+ interface UserLoaderData {
65
+ id: string;
66
+ filter: string | null;
67
+ user: {
68
+ sub: string;
69
+ };
70
+ }
71
+
72
+ export default function UserPage() {
73
+ const data =
74
+ useLoaderData<
75
+ UserLoaderData
76
+ >();
77
+
78
+ return (
79
+ <main>
80
+ <h1>
81
+ User {data.id}
82
+ </h1>
83
+ </main>
84
+ );
85
+ }
86
+ ```
87
+
88
+ ## Loader context
89
+
90
+ A loader receives:
91
+
92
+ - `params` — the matched file-route parameters, including dynamic segments.
93
+ - `searchParams` — a fresh `URLSearchParams` instance for the target request URL.
94
+
95
+ Loaders run inside the normal BCP request context. Server-only helpers from `bcp/server`, including `headers()`, `cookies()`, `requestUrl()`, `requestMethod()`, `requestId()`, `clientIp()`, `bearerToken()`, `getSession()` and `redirect()`, are available while the loader is running.
96
+
97
+ For both initial document requests and SPA navigations, the loader request context is created for the target page URL rather than the internal `/_bcp/navigation` transport URL. For example, navigating to `/users/42?filter=active` makes `requestUrl()` observe `/users/42?filter=active` and gives the loader the matching query string.
98
+
99
+ This makes the recommended authenticated page flow:
100
+
101
+ ```text
102
+ Initial document request
103
+
104
+ loader.ts
105
+
106
+ Session / database / authorization
107
+
108
+ serializable loader data
109
+
110
+ SSR page
111
+
112
+ useLoaderData() during hydration
113
+
114
+ Later <Link> / router navigation
115
+
116
+ /_bcp/navigation
117
+
118
+ loader.ts runs for the target URL
119
+
120
+ fresh session / cookie / database state
121
+
122
+ navigation payload with loader data
123
+
124
+ client renders the target route
125
+ ```
126
+
127
+ Database modules should remain server-only and can be imported directly by `loader.ts`.
128
+
129
+ ## Return values
130
+
131
+ A loader can return either serializable data or a Web `Response`.
132
+
133
+ ### Data
134
+
135
+ Loader data must contain only JSON-compatible values:
136
+
137
+ - `null`
138
+ - strings
139
+ - booleans
140
+ - finite numbers
141
+ - arrays of supported values
142
+ - plain objects containing supported values
143
+
144
+ Values such as `Date`, `Map`, `Set`, functions, symbols, `BigInt`, class instances, `NaN`, `Infinity` and circular object graphs are rejected. Convert database-specific values to JSON-safe values before returning them.
145
+
146
+ ```ts
147
+ export async function loader() {
148
+ const user =
149
+ await findUser();
150
+
151
+ return {
152
+ id:
153
+ user.id,
154
+ createdAt:
155
+ user.createdAt.toISOString(),
156
+ };
157
+ }
158
+ ```
159
+
160
+ The result is rendered through a server-side loader-data provider and serialized into BCP framework data. During SPA navigation the same data is carried in the navigation payload and written into the framework data before the target client bundle renders. `loader.ts` itself is never imported by the browser entry.
161
+
162
+ ### Redirects and other responses
163
+
164
+ A loader can return a standard `Response`, including the BCP `redirect()` helper:
165
+
166
+ ```ts
167
+ import {
168
+ cookies,
169
+ redirect,
170
+ } from "bcp/server";
171
+
172
+ export async function loader() {
173
+ const cookieStore =
174
+ await cookies();
175
+
176
+ cookieStore.set(
177
+ "last_guard",
178
+ "private-page",
179
+ {
180
+ httpOnly: true,
181
+ sameSite: "lax",
182
+ path: "/",
183
+ }
184
+ );
185
+
186
+ return redirect(
187
+ "/login",
188
+ 303
189
+ );
190
+ }
191
+ ```
192
+
193
+ Response cookies created during the loader are merged into the returned response, including redirects.
194
+
195
+ For an SPA navigation, BCP converts an HTTP redirect into a navigation redirect payload while preserving all `Set-Cookie` values. Same-origin redirects continue through the BCP router without a full page reload. Cross-origin redirects are handed to normal browser navigation. The client router limits a single navigation chain to 10 redirects to prevent redirect loops.
196
+
197
+ A loader may also return another Web `Response`. If that response is not a navigation payload or supported redirect response, the client router falls back to a normal document request so the original HTTP semantics are preserved.
198
+
199
+ ## Client navigation behavior in 0.1.9
200
+
201
+ Routes with and without loaders both support BCP SPA navigation.
202
+
203
+ When `<Link>`, `navigate()`, `router.push()`, `router.replace()`, browser history navigation, or `router.refresh()` targets a loader-backed page, BCP performs the following work:
204
+
205
+ 1. Starts navigation state immediately and aborts the previous in-flight navigation request.
206
+ 2. Requests `/_bcp/navigation` with the target pathname and query string.
207
+ 3. Runs the target route loader on the server inside a fresh request context.
208
+ 4. Returns route metadata, client asset information and the fresh loader data in one navigation payload.
209
+ 5. Writes the payload into `__BCP_DATA__` before importing/rendering the target client bundle.
210
+ 6. Reuses the existing React root so persistent layouts can keep client state where React reconciliation allows it.
211
+
212
+ `AbortController` cancels superseded network requests. BCP also assigns each navigation a monotonically increasing sequence so a stale response that completes after a newer navigation is not allowed to update router state. The route bundle import receives the navigation identifier as its cache-busting query parameter as well.
213
+
214
+ `loading.tsx` remains part of the navigation payload. `useNavigation()` is set to navigating before the server request starts; once the payload arrives, the matching loading markup can be shown while target assets/runtime work completes.
215
+
216
+ ## Middleware and security
217
+
218
+ SPA loader navigation still travels through the normal BCP middleware and security gateway chain before reaching the loader navigation runtime. Applications should still enforce page authorization inside the loader itself because the loader owns the target page data and can make the authorization decision using `getSession()`, cookies, headers or database state.
219
+
220
+ The internal `/_bcp/navigation` endpoint is transport infrastructure and is not a substitute for loader-level authorization checks.
221
+
222
+ ## Response cache behavior
223
+
224
+ Loader-backed pages are excluded from the production response-cache manifest by default. A loader can depend on cookies, sessions, authorization headers, request identity or user-specific database state, so automatically placing the resulting HTML in a public route cache would be unsafe.
225
+
226
+ Caching inside a loader can still be implemented explicitly with application-aware data caching where the cache key and invalidation model are known.
227
+
228
+ Navigation payloads are sent with `Cache-Control: no-store` and `X-Content-Type-Options: nosniff`.
229
+
230
+ ## Error and not-found behavior
231
+
232
+ Errors thrown by a loader enter the same page error handling path as SSR errors. For navigation requests, server errors or unsupported response shapes cause the router to fall back to a normal document request so the page-level error/not-found rendering path remains authoritative.
233
+
234
+ A loader can also call existing server/runtime primitives that throw the framework not-found signal; the matching `not-found.tsx` boundary remains responsible for rendering the 404 page.
235
+
236
+ ## Development reloads
237
+
238
+ `loader.ts` and `loader.tsx` are watched as server files during development. Loader modules use a server-side cache-busting version during document rendering and navigation, so edits are reflected without putting loader code in the browser dependency graph.
239
+
240
+ Only one loader file may exist next to a page. Having both `loader.ts` and `loader.tsx` is treated as a framework configuration error.