# Stopnetic setup for JavaScript applications

Angular, React, Vue, Svelte, Solid and plain JavaScript use the same server guard
and browser access check. React is optional. Install `@stopnetic/js` in the
website project. The CLI chooses hosting files; choose your actual SSR framework
in the instructions below to see where its server guard belongs.

Start with [SSR framework instructions](#ssr-framework-instructions) if the app
renders on the server. Optional browser-only demos: [React Router](#react-router),
[Angular Router](#angular-router), [Vue Router](#vue-router),
[Svelte](#svelte-navigation), [Solid Router](#solid-router),
[plain JavaScript](#plain-javascript-navigation). Each section includes the
layout, navigation and protected API example, not just the startup function.

The generated control endpoint is only part of installation. It does not
automatically intercept pages, edit your router or protect existing APIs.

## Integration contract — read before editing an existing app

1. Inspect the existing server entry, rendering modes, hosting adapter and router.
   Preserve SSR, hydration, public pages, routes and application features.
   Installing Stopnetic is not authorization to convert SSR to CSR, delete server
   files, replace the application's router or copy the demo over an existing app.
2. Choose ONE of the two rendering paths below. Keep the app's current rendering
   by default. A hosting/runtime incompatibility must be explained to the owner;
   do not resolve it by silently removing SSR or changing hosting.
3. Install the guard before the existing server renders or serves HTML. Keep the
   existing browser bootstrap unchanged for the default document-navigation mode.
   Do not add a global `Checking access...` screen or a browser startup gate.
4. Save protected paths in Stopnetic Policies. Do not add the demo paths, protect
   every page, copy dashboard rules into a frontend path list, or change defaults
   without the owner's instruction. Guards evaluate requests; they do not make
   every evaluated request require verification.
5. Use normal document navigation for the simplest server-enforced integration.
   If retaining client-side SPA navigation is required, use the OPTIONAL router
   examples and explain their asynchronous checks before adding them.
6. Independently guard private API handlers. Validate public, protected and denied
   pages plus SSR output before declaring integration complete.

## What visitors should see

For example, suppose only `/members` is Page / Automatic browser flow:

| Request | Normal behavior |
| --- | --- |
| `/`, `/about`, `/contact` with no matching rule | Public page; normal layout, no Stopnetic checking screen or verification |
| `/members` without a valid session | Server verification flow before returning the page |
| `/members` with a valid session | Normal page content |
| A Page / Block route | Empty HTTP 404 before any site layout renders |
| A protected API without a valid session | HTTP 404; no JSON data and no browser challenge |

Generated site mode leaves unmatched paths public; explicit broader rules can
change that. Public requests still need a valid signed policy response. A service
failure returns 503 rather than bypassing policy. Rules belong in the dashboard;
the browser cannot know whether a target is public until the server checks it.

## 1. Choose the actual rendering and server runtime

**Option A — preserve SSR.** Keep the existing renderer, server entry and
hydration. Add the Node guard before the renderer (section 3). Use server
document navigation (section 4). Public pages render normally; protected pages
render only after the dashboard decision permits them.

**Option B — retain a static/CSR frontend.** Keep the existing browser bootstrap
and build. Add a Node hosting page guard before HTML (section 3), then use server
document navigation (section 4). A Node function serving built `index.html` is
static page handling, not SSR. Choose this option for an existing static app or
only after an explicit decision to convert rendering.

Neither option requires a global browser boundary. Optional SPA navigation is
a separate choice, not a third rendering mode.

| Application | Where the shared guard belongs |
| --- | --- |
| Node/Express SSR or static-file server | Before body parsers, static files, API handlers and rendering |
| Node server with a Fetch `Request` → `Response` handler | Wrap the complete application handler before it renders or returns API data |
| Static Angular, React, Vue, Svelte, Solid or plain HTML | A Node hosting function/middleware before HTML, plus the shared browser check below |
| Angular SSR, Nuxt, SvelteKit, Astro, SolidStart, React Router server mode | The deployed Node request entry, before the framework renderer; preserve framework context |
| Next.js | Use `@stopnetic/next` and its Next.js installation guide |
| Edge/Deno/Cloudflare Workers, PHP, Python, Ruby, C# or Java | This Node SDK cannot be imported there; use a Node gateway or an adapter for that runtime |

Node.js 22 or newer is required. SSR support alone does not establish runtime
compatibility. A Fetch API on an Edge runtime is not a Node server.
Prerendering runs at build time: it is not a request-time access check. Do not
publish protected prerendered pages or private data to a public static directory.

## 2. Save the six server environment settings

Copy actual values from Stopnetic Integration into your hosting SERVER
environment. Use `https://www.example.com` only as an example; replace it with
your website's canonical origin.

```dotenv
STOPNETIC_WEBSITE_ORIGIN=https://www.example.com
STOPNETIC_SITE_ID=your_site_id
STOPNETIC_API_URL=https://your-stopnetic-service/api/integration/decision
STOPNETIC_API_TOKEN=your_private_installation_key
STOPNETIC_DECISION_PUBLIC_KEY=your_complete_public_PEM
STOPNETIC_COOKIE_SECRET=your_separate_random_cookie_secret
```

The installer creates a private cookie secret in `.env.local` when missing.
Reuse it across deployments. Do not commit `.env.local`. Copy complete PEM
header/body/footer with real newlines or literal `\n`. The dashboard's generated
local configuration uses an unquoted single-line key with literal `\n`; the SDK
restores its line breaks. Hosting fields take only the value, with no variable
name, equals sign or surrounding quotes. Manually written actual multiline PEMs
still need quotes in env files. Previously generated quoted env files remain valid.
Never put private settings in frontend environment files or prefix them with
`VITE_`, `REACT_APP_` or `NEXT_PUBLIC_`. Angular's `environment.ts` is browser
configuration, not a private server environment.

## 3. Protect the server before rendering

### Node/Express: every frontend uses the same mount

```js
import { installStopnetic } from './stopnetic.server.mjs';
installStopnetic(app);
// Existing body parsers, static files, APIs and framework renderer follow.
```

Generate that helper with `stopnetic-js init --platform node`. Only expose the
guarded server. Keep the control endpoints `/api/stopnetic/access` and
`/api/stopnetic/verify` reachable before SPA fallbacks. Preserve the canonical
Host at your proxy. This works without a React dependency or browser boundary.

### Node Fetch application: one wrapper for pages and APIs

In SDK 0.0.3-pilot or newer:

```js
import { createStopneticAppHandler } from '@stopnetic/js/server';

let protectedApp;
export async function handleRequest(request, context) {
  protectedApp ??= createStopneticAppHandler(async (checked, frameworkContext) => {
    // Your existing Node framework handler. Return a Response.
    return renderApplication(checked, frameworkContext);
  });
  return protectedApp(request, context);
}
```

Lazy initialization reads environment settings at runtime, not during a
prerender build. Control requests are handled by Stopnetic. Other requests are
checked before the application handler runs; denied responses do not render
the site's navigation or private API data. Context and unread request bodies
are preserved. Continue enforcing your application's own user/data permissions.

For a framework hook, keep its existing request/context conventions and call
the shared guard before its `resolve`, `next` or renderer. A Fetch wrapper is
for a complete handler returning a Response, not every framework middleware
signature. Node/Express can mount the generated helper around any renderer.

<!-- BEGIN SSR EXAMPLES -->

### SSR framework instructions

Choose the actual SSR framework, not just the browser UI library. Angular
instructions apply ONLY to Angular. Each section below identifies the server
file, integration steps, prerender constraints and runtime. The same sections
appear in the dashboard SSR framework selector. Merge examples into existing
entries; do not replace SSR, hydration, context or application features.

Use your selected framework hook OR the shared Node wrapper above. Do not
stack both or mount the generated Express helper as an additional guard.

| Your app | Choose |
| --- | --- |
| Angular with SSR | Angular SSR |
| React Router framework/server mode | React Router SSR |
| React plus your own Node renderer | React custom SSR |
| Nuxt | Nuxt (Vue SSR) |
| Vue plus your own Node renderer | Vue custom SSR |
| SvelteKit | SvelteKit SSR |
| Astro on-demand rendering | Astro SSR |
| SolidStart | SolidStart SSR; match the installed major version |
| Other Node renderer / plain JavaScript server | Other Node SSR |
| Next.js | Dedicated @stopnetic/next guide; do not install the JS control files |
| React/Vue/Svelte/Solid UI without an SSR server | Option B: static/browser app |
| PHP, Python, Ruby, C#, Java or Edge/Deno | No native adapter in this Node npm SDK; a separate runtime adapter/gateway is needed |

All snippets require SDK 0.0.3-pilot or newer and six SERVER settings. They
are manual integration examples, not automatic installers for each framework.
Validate the actual deployed adapter, versions, internal data endpoints and
cache/static aliases. Hook-contract checks do not prove provider deployment.

### Angular SSR on Node

**Where:** src/server.ts (Fetch entry), or your existing Express server.ts before AngularNodeAppEngine.handle.

1. Keep server.ts, main.server.ts, server providers, host configuration and hydration. Merge the guard into the existing request entry; preserve extra framework context.
2. Use the Fetch example only for an AngularAppEngine Request/Response entry. An Express entry instead mounts the Node helper before express.static and AngularNodeAppEngine.handle.
3. Protected paths use RenderMode.Server. The wildcard below is an example, not a replacement for existing public/hybrid route configuration.

**Prerendering and caches:** Do not publish protected RenderMode.Prerender output to a public CDN. Retain public rendering modes only where they cannot bypass future protection. Any path the dashboard may protect must reach the server guard before HTML.

**Hosting runtime:** Node.js 22+ only. Keep the existing Node hosting adapter; check the deployed runtime, not just the hosting brand. Edge/Deno deployments need a separate supported adapter or a gateway decision. Do not convert SSR to static rendering automatically.

**Netlify Angular only:** Netlify’s automatic Angular SSR uses Edge Functions. The current Node SDK has no drop-in integration for that combination. Keep SSR; obtain a supported Node deployment or gateway decision. A static index.html function is not an Angular SSR adapter.

#### src/server.ts

```ts
// Merge into your existing Node SSR entry; retain host settings and context.
import { AngularAppEngine, createRequestHandler } from '@angular/ssr';
import { createStopneticAppHandler } from '@stopnetic/js/server';

const engine = new AngularAppEngine();
let guarded: ReturnType<typeof createStopneticAppHandler> | undefined;
async function handle(request: Request): Promise<Response> {
  guarded ??= createStopneticAppHandler(async checked =>
    await engine.handle(checked) ?? new Response(null, { status: 404 }));
  return guarded(request);
}
export const reqHandler = createRequestHandler(handle);
```

#### src/app/app.routes.server.ts

```ts
import { RenderMode, ServerRoute } from '@angular/ssr';
export const serverRoutes: ServerRoute[] = [
  // Demo only: merge with existing hybrid modes rather than replacing them.
  { path: '**', renderMode: RenderMode.Server },
];
```

Framework references: [Angular SSR](https://angular.dev/guide/ssr), [Netlify Angular runtime](https://docs.netlify.com/build/frameworks/framework-setup-guides/angular/).

### React Router framework mode on Node

**Where:** Existing Express entry before @react-router/express createRequestHandler.

1. React Router framework mode supplies the SSR server. Keep its server build, loaders/actions, getLoadContext and hydration; BrowserRouter alone is not an SSR backend.
2. Copy the helper, then import and call it at the application root before parsers/assets and the existing createRequestHandler. Do not place it after the renderer or mount it at a stripped URL prefix.
3. The helper checks React Router 7 .data requests against their page path, including /_root.data. The actual router still receives its original URL and unread body. This example uses the default / basename; custom basenames and other router versions need alias mapping review.

**Prerendering and caches:** Keep ssr enabled in react-router.config.ts. Exclude protected paths from prerender output and CDN caches. Direct .data requests must not expose private loaders merely because the document path is guarded. Independently authorize resource routes, actions and server functions.

**Hosting runtime:** Node.js 22+ only. Keep the existing Node hosting adapter; check the deployed runtime, not just the hosting brand. Edge/Deno deployments need a separate supported adapter or a gateway decision. Do not convert SSR to static rendering automatically.

#### stopnetic-react-router.mjs

```js
// Call installStopneticReactRouter(app) before parsers, assets and the existing
// @react-router/express createRequestHandler. Keep that handler/context intact.
import { createStopneticServer } from '@stopnetic/js/server';

export function installStopneticReactRouter(app) {
  let server;
  app.use((req, res, next) => {
    server ??= createStopneticServer();
    return server.access(req, res, () => {
      // React Router 7 single-fetch data URLs must use the page's policy.
      // The renderer still receives the original request and unread body.
      const url = new URL(req.originalUrl || req.url, 'https://example.invalid');
      let checked = req;
      if (url.pathname.endsWith('.data')) {
        url.pathname = url.pathname === '/_root.data' ? '/' : url.pathname.slice(0, -5);
        checked = Object.create(req);
        checked.originalUrl = url.pathname + url.search;
      }
      return server.guard(checked, res, next);
    });
  });
}
```

#### Existing Express entry (merge fragment)

```js
// In your EXISTING Express entry, before parsers, static files and rendering:
import { installStopneticReactRouter } from './stopnetic-react-router.mjs';
installStopneticReactRouter(app);
// Keep the existing renderer, application routes and server startup below.
```

Framework references: [React Router server adapters](https://reactrouter.com/api/other-api/adapter), [React Router prerendering](https://reactrouter.com/how-to/pre-rendering).

### React with an existing custom SSR server

**Where:** Existing Node/Express server before React rendering and any private data handler.

1. React is a UI library; this option assumes you already have a Node SSR server. Keep renderToPipeableStream/renderToReadableStream, request context and hydrateRoot.
2. Copy the helper and call it before the existing assets/API/renderer middleware. For a Fetch-style server use the custom Node Fetch wrapper instead.
3. A Vite React SPA without a server uses Option B: static/browser app. Next.js and React Router SSR have their own selector entries. Do not add a second renderer or replace the app with the browser demo.

**Prerendering and caches:** Do not statically publish protected HTML or embed private data in public bundles. Keep guarded rendering at request time, retaining public prerendering only with correct server/CDN coverage.

**Hosting runtime:** Node.js 22+ only. Keep the existing Node hosting adapter; check the deployed runtime, not just the hosting brand. Edge/Deno deployments need a separate supported adapter or a gateway decision. Do not convert SSR to static rendering automatically.

#### stopnetic-react-ssr.mjs

```js
// Existing custom React SSR server: call before assets, APIs and React rendering.
// Keep renderToPipeableStream/renderToReadableStream and hydration unchanged.
import { createStopneticServer } from '@stopnetic/js/server';
export function installStopneticReactSSR(app) {
  let server;
  app.use((req, res, next) => {
    server ??= createStopneticServer();
    return server.access(req, res, () => server.guard(req, res, next));
  });
}
```

#### Existing Express entry (merge fragment)

```js
// In your EXISTING Express entry, before parsers, static files and rendering:
import { installStopneticReactSSR } from './stopnetic-react-ssr.mjs';
installStopneticReactSSR(app);
// Keep the existing renderer, application routes and server startup below.
```

Framework references: [React Node SSR](https://react.dev/reference/react-dom/server/renderToPipeableStream).

### Nuxt 3/4 with Nitro 2 and H3 1 on Node

**Where:** server/middleware/00-stopnetic.mjs; runs before Nitro renders pages and serves API handlers.

1. Keep Nuxt SSR, its app, pages, hydration and existing Node Nitro preset. Check the installed Nitro/H3 versions; this example uses H3 1 fromNodeMiddleware, not the H3 2 API.
2. Add this server middleware before other body readers and handlers. app/middleware and defineNuxtRouteMiddleware are navigation UI hooks and cannot replace this server guard.
3. Keep the private environment settings in the deployed Node process. Do not use runtimeConfig.public. Test the built server and separate provider functions, not only Nuxt dev.

**Prerendering and caches:** Do not use nuxt generate or prerender/SWR/ISR/cached route rules for protected paths. Protected _payload.json files must not be generated into public output. Keep public route rules only where they cannot expose protected content through CDN/cache aliases.

**Hosting runtime:** Node.js 22+ only. Keep the existing Node hosting adapter; check the deployed runtime, not just the hosting brand. Edge/Deno deployments need a separate supported adapter or a gateway decision. Do not convert SSR to static rendering automatically.

#### server/middleware/00-stopnetic.mjs

```js
// server/middleware/00-stopnetic.mjs. Nuxt 3/4, Nitro 2, H3 1, NODE only.
// Use server middleware, not app/middleware or defineNuxtRouteMiddleware.
import { fromNodeMiddleware } from 'h3';
import { createStopneticServer } from '@stopnetic/js/server';

let server;
export default fromNodeMiddleware((req, res, next) => {
  server ??= createStopneticServer();
  return server.access(req, res, () => server.guard(req, res, next));
});
```

Framework references: [Nuxt server middleware](https://nuxt.com/docs/4.x/directory-structure/server), [H3 1 Node middleware adapter](https://v1.h3.dev/examples/from-expressjs-to-h3).

### Vue with an existing custom SSR server

**Where:** Existing Node/Express server before Vue rendering and private APIs.

1. Vue alone is a UI library. Keep the existing createSSRApp/renderToString server, request context and client hydration.
2. Copy and mount the helper before the existing parsers/assets/API/renderer middleware. For a Request/Response server use the custom Node Fetch wrapper.
3. A Vue SPA uses Option B. A Nuxt app uses the Nuxt entry; do not replace either with a newly invented Express renderer.

**Prerendering and caches:** Protected HTML and private serialized state must be produced only after the server check. Avoid publicly cached/prerendered output for protected paths.

**Hosting runtime:** Node.js 22+ only. Keep the existing Node hosting adapter; check the deployed runtime, not just the hosting brand. Edge/Deno deployments need a separate supported adapter or a gateway decision. Do not convert SSR to static rendering automatically.

#### stopnetic-vue-ssr.mjs

```js
// Existing custom Vue SSR server: call before assets, APIs and Vue rendering.
// Keep createSSRApp/renderToString and client hydration unchanged.
import { createStopneticServer } from '@stopnetic/js/server';
export function installStopneticVueSSR(app) {
  let server;
  app.use((req, res, next) => {
    server ??= createStopneticServer();
    return server.access(req, res, () => server.guard(req, res, next));
  });
}
```

#### Existing Express entry (merge fragment)

```js
// In your EXISTING Express entry, before parsers, static files and rendering:
import { installStopneticVueSSR } from './stopnetic-vue-ssr.mjs';
installStopneticVueSSR(app);
// Keep the existing renderer, application routes and server startup below.
```

Framework references: [Vue SSR](https://vuejs.org/guide/scaling-up/ssr.html).

### SvelteKit server hooks on Node

**Where:** src/hooks.server.js, before resolve(event) and any existing application hooks.

1. Keep the existing Node adapter, SSR pages, loads/actions and hydration. Merge this handle into existing hooks; put Stopnetic first and retain other hooks in its continuation.
2. The guard receives the original request and per-request continuation. Data requests are checked against event.url, the canonical page path, then resolve receives the original event and body.
3. Merge prerender=false into each protected +page.server.js or a covering server layout. building skips runtime enforcement only during build; no protected path may be prerendered. Test with the full adapter server.

**Prerendering and caches:** Keep SSR enabled. The runtime handle hook does not protect files already served statically. Exclude protected routes from prerender output, including __data.json. Do not rely on a page rule alone for separately exposed endpoints or remote functions.

**Hosting runtime:** Node.js 22+ only. Keep the existing Node hosting adapter; check the deployed runtime, not just the hosting brand. Edge/Deno deployments need a separate supported adapter or a gateway decision. Do not convert SSR to static rendering automatically.

#### src/hooks.server.js

```js
import { building } from '$app/environment';
import { createStopneticAppHandler } from '@stopnetic/js/server';

let guarded;
// When merging existing hooks, run this first and put them in the continuation.
export async function handle({ event, resolve }) {
  // Build-time public prerendering only. Protected routes must have prerender=false.
  if (building) return resolve(event);
  guarded ??= createStopneticAppHandler((_request, run) => run());
  // SvelteKit exposes the canonical page URL on data requests. Check that page
  // rather than allowing an unmatched /members/__data.json alias through.
  const checked = event.isDataRequest
    ? new Request(event.url, { method: event.request.method, headers: event.request.headers })
    : event.request;
  return guarded(checked, () => resolve(event));
}
```

#### src/routes/members/+page.server.js (merge example)

```js
// Merge into the PROTECTED route's +page.server.js, retaining loads/actions.
export const prerender = false;
export const ssr = true;
```

Framework references: [SvelteKit server hooks](https://svelte.dev/docs/kit/hooks), [SvelteKit page options](https://svelte.dev/docs/kit/page-options).

### Astro request middleware on Node

**Where:** src/middleware.js, before next() renders a page or executes an endpoint.

1. Keep the existing compatible Node adapter and Astro renderer. If middleware exists, merge/sequence this guard first, preserving locals and the existing continuation.
2. Use context.request and next(), not a browser startup gate. The isPrerendered branch is build-only public rendering; protected pages must never enter it.
3. Set prerender=false on protected .astro pages/endpoints. Keep their existing content. For @astrojs/node middleware mode, an outer Express guard before static files is also available; do not install both patterns unnecessarily.

**Prerendering and caches:** Protected pages need on-demand rendering with a Node adapter. Public prerendered pages can remain public, but files bypassing request middleware cannot acquire protection through a dashboard rule alone.

**Hosting runtime:** Node.js 22+ only. Keep the existing Node hosting adapter; check the deployed runtime, not just the hosting brand. Edge/Deno deployments need a separate supported adapter or a gateway decision. Do not convert SSR to static rendering automatically.

#### src/middleware.js

```js
// src/middleware.js. Keep the existing Node deployment adapter.
import { defineMiddleware } from 'astro:middleware';
import { createStopneticAppHandler } from '@stopnetic/js/server';

let guarded;
export const onRequest = defineMiddleware(async (context, next) => {
  // Public prerender builds only; protected pages must export prerender=false.
  if (context.isPrerendered) return next();
  guarded ??= createStopneticAppHandler((_request, run) => run());
  return guarded(context.request, () => next());
});
```

#### src/pages/members.astro (frontmatter flag)

```astro
---
// Merge this flag into a protected page, preserving its existing content.
export const prerender = false;
---
```

Framework references: [Astro middleware](https://docs.astro.build/en/guides/middleware/), [Astro Node adapter](https://docs.astro.build/en/guides/integrations-guide/node/).

### SolidStart request middleware on Node

**Where:** src/middleware.js, registered in the existing SolidStart config before rendering and API execution.

1. Choose the snippet matching your installed major version. SolidStart 2 uses createMiddleware([...]) and H3 event.req/event.res; SolidStart 1 uses onRequest and event.request/event.response.
2. For v2 register solidStart({ middleware: "./src/middleware.js" }) in existing vite.config.ts. For v1 register middleware in existing defineConfig in app.config.ts. Keep all other settings and the SSR entry-server.tsx.
3. A per-request allowed marker distinguishes an authorized continuation from a verification 204. The framework continues only when the marker is set; no renderer runs on challenge/block/outage. Server functions still require their own permissions and policy checks for their actual endpoint paths.

**Prerendering and caches:** Keep SSR enabled and protected routes out of prerender/static/ISR output. Use the existing Node server plugin/preset; do not assume a middleware export changes an Edge deployment into Node.

**Hosting runtime:** Node.js 22+ only. Keep the existing Node hosting adapter; check the deployed runtime, not just the hosting brand. Edge/Deno deployments need a separate supported adapter or a gateway decision. Do not convert SSR to static rendering automatically.

#### src/middleware.js — SolidStart 2

```js
// SolidStart 2: register this module using solidStart({ middleware: ... }).
import { createMiddleware } from '@solidjs/start/middleware';
import { createStopneticAppHandler } from '@stopnetic/js/server';

let check;
export default createMiddleware([
  async (event, next) => {
    check ??= createStopneticAppHandler((_request, state) => {
      state.allowed = true;
      return new Response(null, { status: 204 });
    });
    // Per-request state: a verification 204 must NEVER be mistaken for permission.
    const state = { allowed: false };
    const response = await check(event.req, state);
    if (!state.allowed) return response;
    event.res.headers.set('cache-control', 'private, no-store');
    const result = await next(); // Preserve H3/SolidStart context and raw results.
    if (!(result instanceof Response)) return result;
    const headers = new Headers(result.headers);
    headers.set('cache-control', 'private, no-store');
    return new Response(result.body, {
      status: result.status, statusText: result.statusText, headers,
    });
  },
]);
```

#### src/middleware.js — SolidStart 1 alternative

```js
// SolidStart 1: register using defineConfig({ middleware: './src/middleware.js' }).
// Keep the existing entry-server.tsx renderer. This is NOT SolidStart 2 config.
import { createMiddleware } from '@solidjs/start/middleware';
import { createStopneticAppHandler } from '@stopnetic/js/server';

let check;
export default createMiddleware({
  onRequest: async event => {
    check ??= createStopneticAppHandler((_request, state) => {
      state.allowed = true;
      return new Response(null, { status: 204 });
    });
    const state = { allowed: false };
    const response = await check(event.request, state);
    if (!state.allowed) return response;
    event.response.headers.set('cache-control', 'private, no-store');
    // undefined continues to SolidStart; preserve its rendering and API handling.
  },
});
```

Framework references: [SolidStart 2 middleware](https://docs.solidjs.com/solid-start/v2/reference/server/create-middleware), [SolidStart 1 middleware](https://docs.solidjs.com/solid-start/v1/reference/server/create-middleware).

### Custom Node SSR or plain JavaScript server

**Where:** Existing root request entry, before static HTML, API data or rendering.

1. Use the Express helper OR the Fetch wrapper according to the existing server signature. Do not replace the renderer or introduce a second server.
2. Call installStopneticSSR(app) first for Express. For Fetch, export the protected version of the existing handler using protectSSR(renderApplication); all context arguments are retained.
3. Keep the canonical Host and private environment in the Node service. Only expose the guarded entry. Independently guard separately deployed API functions.

**Prerendering and caches:** Static HTML/cache aliases must not bypass the guard. Preserve existing public routes and apply protection only through dashboard policies.

**Hosting runtime:** Node.js 22+ only. Keep the existing Node hosting adapter; check the deployed runtime, not just the hosting brand. Edge/Deno deployments need a separate supported adapter or a gateway decision. Do not convert SSR to static rendering automatically.

#### stopnetic-ssr.mjs

```js
// Call before parsers, static files, APIs and your existing Node SSR renderer.
import { createStopneticServer } from '@stopnetic/js/server';
export function installStopneticSSR(app) {
  let server;
  app.use((req, res, next) => {
    server ??= createStopneticServer();
    return server.access(req, res, () => server.guard(req, res, next));
  });
}
```

#### stopnetic-fetch-ssr.mjs

```js
// Wrap your existing Node Request -> Response renderer; retain all context.
import { createStopneticAppHandler } from '@stopnetic/js/server';
export function protectSSR(renderApplication) {
  let guarded;
  return (request, ...context) => {
    guarded ??= createStopneticAppHandler(renderApplication);
    return guarded(request, ...context);
  };
}
```

<!-- END SSR EXAMPLES -->

### Static apps on Netlify / Vercel

The generated Netlify control file serves `/api/stopnetic/access` and
`/api/stopnetic/verify`; Vercel uses `api/stopnetic/[operation].mjs`. They do not
guard the original page request by themselves. Add a Node page function or
Node routing middleware before serving HTML if you need real page HTTP 404s.
The full hosting guide is at your Stopnetic dashboard's `/docs#javascript`.

Use your actual build output. Vite often uses `dist`; Angular commonly uses
`dist/<project>/browser`. Angular scripts/styles may be at that directory's
root rather than `/assets`. Bundle the correct HTML and assets, preserve their
MIME types, and do not use the Vite-only file paths unchanged for Angular.
Reject direct entry-HTML and function aliases that would bypass protected paths.

## 4. Navigation — server enforcement by default

For BOTH rendering options, leave the existing browser entry and app layout
alone. Use document links for page navigation so every target reaches the
guarded server before rendering. No global `checkAccess`, `StopneticBoundary`,
startup import gate, root access service or loading message is required.

| Frontend | Document navigation |
| --- | --- |
| Angular | `<a href="/members">Members</a>` instead of `routerLink` for this navigation |
| React | `<a href="/members">Members</a>` instead of router `<Link>` |
| Vue | `<a href="/members">Members</a>` instead of `<RouterLink>` |
| Svelte / SvelteKit | `<a href="/members" data-sveltekit-reload>Members</a>`; disable any custom interception |
| Solid | `<a href="/members" target="_self">Members</a>` instead of router `<A>` |
| Plain JavaScript | `<a href="/members">Members</a>`; do not intercept it with `pushState` |

Replace programmatic SPA transitions with `location.assign('/members')` when
using this mode. Turn off router prefetch/preload of protected page data that
would bypass the guarded document request. Existing routers may still resolve
the current URL after loading. Normal pages keep their usual loading behavior;
Stopnetic's verification HTML is sent only when the server decision requires it.

For SSR, hydrate the HTML already authorized by the server. Do not replace it
with a browser-only loading screen or run a redundant startup check. Private API
requests remain independently checked by their server handler.

### Optional SPA navigation — explicit asynchronous tradeoff

Retain SPA navigation only when needed. SPA transitions do not make a new
document request, so check each target in the router before showing its content.
The current browser API returns access after a server request; it does not
provide a synchronous dashboard route manifest. Public transitions also need
that check to discover the current policy. A brief empty pending state is
possible; it is not a challenge and is not advertised as instant public rendering.
Use document navigation above if this delay is unacceptable. Do not bypass
checks with a hardcoded public/protected list, cached `allowed` flag, or client
user-agent detection. Keep full target content hidden until the check succeeds.

The six optional demos below intentionally check their sample router targets.
They are not instructions to protect all those paths in Policies. Their pending
state contains no `Checking access...` text. They do not provide server SSR
integration or HTTP status changes. Do not install both a startup check and a
router gate; do not apply these browser demos to an existing hydrated SSR root.

### Optional browser startup gate for a browser-only app

This alternative is useful only if browser mounting must wait for an access
check. Skip it when the server already guards HTML and document navigation is
used. It is not required by either rendering option above.

For a browser-only/static app, move the existing startup code from `src/main.ts`
or `src/main.js` into `src/bootstrap.ts` or `src/bootstrap.js`. Keep its original
framework imports and bootstrap code. Replace the browser entry with:

```js
import { checkAccess } from '@stopnetic/js/browser';

async function start() {
  try {
    const result = await checkAccess(location.pathname + location.search);
    if (result.allowed) {
      await import('./bootstrap'); // Your existing app startup, unchanged.
    } else if (result.reason === 'verification_required' && result.verificationUrl) {
      location.replace(result.verificationUrl);
    } else {
      document.body.replaceChildren(document.createTextNode(
        result.reason === 'blocked' ? 'Not found.' : 'Access unavailable.'));
    }
  } catch {
    document.body.replaceChildren(document.createTextNode('Access unavailable.'));
  }
}
void start();
```

This is browser code, not a server hook. The startup file can use any framework:

| Frontend | Existing startup retained in `bootstrap` |
| --- | --- |
| Angular | `bootstrapApplication(App, appConfig)` or your existing NgModule bootstrap |
| React | `createRoot(...).render(...)` |
| Vue | `createApp(...).use(router).mount(...)` |
| Svelte | Your existing `mount(...)` or older constructor |
| Solid | Your existing `render(...)` |
| Plain JavaScript | Your existing DOM rendering code |

Client router navigation does not rerun this optional startup file. A UI check
cannot set the original HTTP status or make public bundles private.

<!-- BEGIN FRONTEND EXAMPLES -->

### Optional SPA demos — choose your frontend implementation

The package includes copyable source files in `examples/frontend`. The same
examples appear below and in the dashboard frontend selector. Skip this
section for the default server/document-navigation integration. These are
optional browser-only demos, not replacements for an existing app or SSR
entry. Retaining SPA navigation requires asynchronous target checks, even
for public targets. Pending content is empty; no global checking text is
required. Keep the target layout hidden until allowed to avoid exposing it
on denial. Only dashboard rules decide which paths require verification.

If explicitly choosing this mode, merge the relevant hooks into your own
browser router and adapt the sample paths. Do not add every demo path to
Policies. Do not combine these gates with the optional startup gate or
apply them to a hydrated SSR root. SSR uses section 3 and document navigation.

Copy the selected folder under `src/<frontend>/` and the shared data helper
to `src/load-private-data.js` (plus its `.d.ts` file for TypeScript). This keeps
the relative imports below valid. Point your browser entry at the example
`main` file, or import that file from your existing entry when trying the demo.
React/Vue/Svelte/Solid/plain JS entries expect `<div id="app"></div>`; Angular
uses `<app-root></app-root>`. Use your own mount target when adapting the code.
Do not mount your old app outside the gate or mount two apps at once.

- React Router
- Angular Router
- Vue Router
- Svelte navigation
- Solid Router
- Plain JavaScript navigation

Shared browser API helper: `src/load-private-data.js`. `/api/private` must
be guarded on its own server. Angular demonstrates HttpClient instead.


```js
// Browser example. The /api/private SERVER handler must also be guarded.
export async function loadPrivateData(signal) {
  const response = await fetch('/api/private', {
    credentials: 'same-origin', cache: 'no-store', signal,
    headers: { Accept: 'application/json' },
  });
  if (!response.ok) throw new Error(`Request failed (HTTP ${response.status}).`);
  return response.json();
}
```

TypeScript declaration: `src/load-private-data.d.ts`.


```ts
export function loadPrivateData(signal?: AbortSignal): Promise<unknown>;
```

### React Router

Use BrowserRouter and StopneticBoundary around the full header, routes and footer. Client Link navigation triggers another access check. React Router 6+ style; adapt your existing routes rather than replacing them. Add react-router-dom if your app does not have it.

#### src/react/App.tsx


```tsx
import { useEffect, useState, type ReactNode } from 'react';
import { BrowserRouter, Link, Route, Routes, useLocation } from 'react-router-dom';
import { StopneticBoundary } from '@stopnetic/js/react';
import { loadPrivateData } from '../load-private-data.js';

function RouteGuard({ children }: { children: ReactNode }) {
  const location = useLocation();
  return <StopneticBoundary path={location.pathname + location.search}
    pending={null}
    blocked={<p>Access unavailable.</p>}>
    {children}
  </StopneticBoundary>;
}

function Members() {
  const [data, setData] = useState('Loading data...');
  useEffect(() => {
    const controller = new AbortController();
    loadPrivateData(controller.signal)
      .then(value => { if (!controller.signal.aborted) setData(JSON.stringify(value) ?? 'No data.'); })
      .catch(() => { if (!controller.signal.aborted) setData('API access unavailable.'); });
    return () => controller.abort();
  }, []);
  return <main><h1>Members</h1><p>{data}</p></main>;
}

function Layout() {
  return <>
    <header><nav>
      <Link to="/">Home</Link>{' '}<Link to="/members">Members</Link>{' '}
      <Link to="/slow">Slow</Link>{' '}<Link to="/blocked">Blocked</Link>{' '}
      <Link to="/offline">Offline</Link>
    </nav></header>
    <Routes>
      <Route caseSensitive path="/" element={<main><h1>Home</h1></main>}/>
      <Route caseSensitive path="/members" element={<Members/>}/>
      <Route caseSensitive path="/slow" element={<main><h1>Slow</h1></main>}/>
      <Route caseSensitive path="/blocked" element={<main><h1>Blocked</h1></main>}/>
      <Route caseSensitive path="/offline" element={<main><h1>Offline</h1></main>}/>
      <Route path="*" element={<main><h1>Not found</h1></main>}/>
    </Routes>
    <footer>Example site</footer>
  </>;
}

export default function App() {
  return <BrowserRouter><RouteGuard><Layout/></RouteGuard></BrowserRouter>;
}
```

#### src/react/main.tsx


```tsx
import { createRoot } from 'react-dom/client';
import App from './App';
createRoot(document.getElementById('app')!).render(<App/>);
```

### Angular Router

Optional browser-only SPA demo: the service records checking/allowed/denied state. Checks evaluate policy; only dashboard rules require verification. Members uses HttpClient after route access succeeds. Merge providers into your existing browser appConfig. Preserve SSR/hydration with the server/document-navigation option instead of applying this demo to a hydrated root. Modern standalone Angular syntax.

#### src/angular/app/app.config.ts


```ts
import { ApplicationConfig } from '@angular/core';
import { provideHttpClient } from '@angular/common/http';
import { provideRouter } from '@angular/router';
import { routes } from './app.routes';
export const appConfig: ApplicationConfig = {
  providers: [provideRouter(routes), provideHttpClient()],
};
// Merge these providers into your existing appConfig; retain its other settings.
```

#### src/angular/app/app.routes.ts


```ts
import { Component } from '@angular/core';
import { Routes } from '@angular/router';
import { MembersPage } from './members.page';
import { stopneticRouteGuard } from './stopnetic-access.service';

@Component({ template: '<main><h1>Home</h1></main>' })
class HomePage {}
@Component({ template: '<main><h1>Slow</h1></main>' })
class OtherPage {}

const pages: Routes = [
  { path: '', pathMatch: 'full', component: HomePage },
  { path: 'members', component: MembersPage },
  { path: 'slow', component: OtherPage },
  { path: 'blocked', component: OtherPage },
  { path: 'offline', component: OtherPage },
];
export const routes: Routes = pages.map(route => ({
  ...route, canActivate: [stopneticRouteGuard], runGuardsAndResolvers: 'always' as const,
}));
```

#### src/angular/app/app.ts


```ts
import { Component, inject } from '@angular/core';
import { RouterLink, RouterOutlet } from '@angular/router';
import { StopneticAccess } from './stopnetic-access.service';

@Component({
  selector: 'app-root', imports: [RouterLink, RouterOutlet],
  template: `
    @if (access.state() === 'allowed') {
      <header><nav>
        <a routerLink="/">Home</a> <a routerLink="/members">Members</a>
        <a routerLink="/slow">Slow</a> <a routerLink="/blocked">Blocked</a>
        <a routerLink="/offline">Offline</a>
      </nav></header>
      <router-outlet/>
      <footer>Example site</footer>
    } @else if (access.state() === 'denied') {
      <p role="alert">Access unavailable.</p>
    }
  `,
})
export class App { readonly access = inject(StopneticAccess); }
```

#### src/angular/app/members.page.ts


```ts
import { isPlatformBrowser } from '@angular/common';
import { Component, DestroyRef, inject, OnInit, PLATFORM_ID, signal } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';

@Component({ template: '<main><h1>Members</h1><p>{{ data() }}</p></main>' })
export class MembersPage implements OnInit {
  readonly data = signal('Loading data...');
  private readonly http = inject(HttpClient);
  private readonly destroyRef = inject(DestroyRef);
  private readonly platform = inject(PLATFORM_ID);
  ngOnInit() {
    // This example loads data in the browser. SSR fetches need request cookies
    // forwarded explicitly, plus server authorization; do not bypass the guard.
    if (!isPlatformBrowser(this.platform)) return;
    this.http.get('/api/private', { withCredentials: true })
      .pipe(takeUntilDestroyed(this.destroyRef))
      .subscribe({ next: value => this.data.set(JSON.stringify(value)),
        error: () => this.data.set('API access unavailable.') });
  }
}
```

#### src/angular/app/stopnetic-access.service.ts


```ts
import { isPlatformBrowser } from '@angular/common';
import { inject, Injectable, PLATFORM_ID, signal } from '@angular/core';
import { CanActivateFn } from '@angular/router';
import { checkAccess } from '@stopnetic/js/browser';

@Injectable({ providedIn: 'root' })
export class StopneticAccess {
  readonly state = signal<'checking' | 'allowed' | 'denied'>('checking');
  private readonly platform = inject(PLATFORM_ID);
  private current?: AbortController;
  async check(path: string): Promise<boolean> {
    // Node SSR must be authorized by the SERVER guard before Angular renders.
    if (!isPlatformBrowser(this.platform)) { this.state.set('allowed'); return true; }
    this.current?.abort();
    const controller = new AbortController(); this.current = controller;
    this.state.set('checking');
    try {
      const result = await checkAccess(path.split('#')[0], { signal: controller.signal });
      if (controller.signal.aborted) return false;
      if (result.allowed) { this.state.set('allowed'); return true; }
      if (result.reason === 'verification_required' && result.verificationUrl) location.assign(result.verificationUrl);
    } catch { if (controller.signal.aborted) return false; }
    this.state.set('denied'); return false;
  }
}
export const stopneticRouteGuard: CanActivateFn = (_route, state) =>
  inject(StopneticAccess).check(state.url);
```

#### src/angular/main.ts


```ts
import { bootstrapApplication } from '@angular/platform-browser';
import { App } from './app/app';
import { appConfig } from './app/app.config';
bootstrapApplication(App, appConfig).catch(error => console.error(error));
```

### Vue Router

Vue 3 and Vue Router 4: beforeEach checks the target and cancels stale checks. App.vue hides the entire layout while checking or denied. afterEach shows it only after a successful navigation. Add vue-router if needed.

#### src/vue/App.vue


```vue
<script setup>
import { RouterLink, RouterView } from 'vue-router';
import { accessState } from './router.js';
</script>

<template>
  <template v-if="accessState === 'allowed'">
    <header><nav>
      <RouterLink to="/">Home</RouterLink> <RouterLink to="/members">Members</RouterLink>
      <RouterLink to="/slow">Slow</RouterLink> <RouterLink to="/blocked">Blocked</RouterLink>
      <RouterLink to="/offline">Offline</RouterLink>
    </nav></header>
    <RouterView/>
    <footer>Example site</footer>
  </template>
  <p v-else-if="accessState === 'denied'" role="alert">Access unavailable.</p>
</template>
```

#### src/vue/main.js


```js
import { createApp } from 'vue';
import App from './App.vue';
import { router } from './router.js';
createApp(App).use(router).mount('#app');
```

#### src/vue/Members.vue


```vue
<script setup>
import { onMounted, onUnmounted, ref } from 'vue';
import { loadPrivateData } from '../load-private-data.js';
const data = ref('Loading data...');
const controller = new AbortController();
onMounted(() => {
  loadPrivateData(controller.signal)
    .then(value => { if (!controller.signal.aborted) data.value = JSON.stringify(value); })
    .catch(() => { if (!controller.signal.aborted) data.value = 'API access unavailable.'; });
});
onUnmounted(() => controller.abort());
</script>

<template><main><h1>Members</h1><p>{{ data }}</p></main></template>
```

#### src/vue/router.js


```js
import { h, ref } from 'vue';
import { createRouter, createWebHistory } from 'vue-router';
import { checkAccess } from '@stopnetic/js/browser';
import Members from './Members.vue';

export const accessState = ref('checking');
const page = title => ({ render: () => h('main', h('h1', title)) });
export const router = createRouter({
  history: createWebHistory(),
  routes: [
    { path: '/', component: page('Home') },
    { path: '/members', component: Members },
    { path: '/slow', component: page('Slow') },
    { path: '/blocked', component: page('Blocked') },
    { path: '/offline', component: page('Offline') },
  ],
});

let current;
let approvedPath = '';
router.beforeEach(async to => {
  current?.abort();
  const controller = new AbortController();
  current = controller;
  approvedPath = '';
  accessState.value = 'checking';
  try {
    const result = await checkAccess(to.fullPath.split('#')[0], { signal: controller.signal });
    if (controller.signal.aborted) return false;
    if (result.allowed) { approvedPath = to.fullPath; return true; }
    if (result.reason === 'verification_required' && result.verificationUrl) {
      location.assign(result.verificationUrl);
    }
    accessState.value = 'denied';
  } catch { if (!controller.signal.aborted) accessState.value = 'denied'; }
  return false;
});
router.afterEach((to, _from, failure) => {
  if (!failure && approvedPath === to.fullPath) accessState.value = 'allowed';
});
```

### Svelte navigation

Svelte 5 example with onMount, History API navigation, popstate and abort cleanup. The full layout stays hidden until access succeeds. It does not require a third-party router. For SvelteKit SSR, retain server-side enforcement and integrate its own navigation lifecycle; this is a standalone Svelte app.

#### src/svelte/App.svelte


```svelte
<script>
  import { onMount } from 'svelte';
  import { checkAccess } from '@stopnetic/js/browser';
  import { loadPrivateData } from '../load-private-data.js';

  let path = '';
  let state = 'checking';
  let data = '';
  let current;
  async function showPage() {
    current?.abort();
    const controller = new AbortController();
    current = controller;
    path = location.pathname;
    state = 'checking'; data = '';
    try {
      const result = await checkAccess(path + location.search, { signal: controller.signal });
      if (controller.signal.aborted) return;
      if (!result.allowed) {
        if (result.reason === 'verification_required' && result.verificationUrl) location.assign(result.verificationUrl);
        state = 'denied'; return;
      }
      state = 'allowed';
      if (path === '/members') {
        data = 'Loading data...';
        try {
          const value = await loadPrivateData(controller.signal);
          if (!controller.signal.aborted) data = JSON.stringify(value);
        } catch { if (!controller.signal.aborted) data = 'API access unavailable.'; }
      }
    } catch { if (!controller.signal.aborted) state = 'denied'; }
  }
  function navigate(event) {
    if (event.button !== 0 || event.metaKey || event.ctrlKey || event.shiftKey || event.altKey) return;
    event.preventDefault();
    history.pushState(null, '', event.currentTarget.href);
    void showPage();
  }
  onMount(() => {
    void showPage();
    addEventListener('popstate', showPage);
    return () => { current?.abort(); removeEventListener('popstate', showPage); };
  });
</script>

{#if state === 'allowed'}
  <header><nav>
    <a href="/" onclick={navigate}>Home</a> <a href="/members" onclick={navigate}>Members</a>
    <a href="/slow" onclick={navigate}>Slow</a> <a href="/blocked" onclick={navigate}>Blocked</a>
    <a href="/offline" onclick={navigate}>Offline</a>
  </nav></header>
  <main><h1>{path === '/members' ? 'Members' : path === '/' ? 'Home' : path === '/slow' ? 'Slow' : 'Not found'}</h1><p>{data}</p></main>
  <footer>Example site</footer>
{:else if state === 'denied'}
  <p role="alert">Access unavailable.</p>
{/if}
```

#### src/svelte/main.js


```js
// Svelte 5 entry. Svelte 4 apps retain their existing `new App(...)` entry.
import { mount } from 'svelte';
import App from './App.svelte';
mount(App, { target: document.getElementById('app') });
```

### Solid Router

Solid Router root layout uses useLocation and reactive checks. The Show boundary creates the full layout only for the checked path, and cleanup aborts stale checks. Add @solidjs/router if needed. This browser example does not replace SolidStart server authorization.

#### src/solid/App.tsx


```tsx
import { createEffect, createSignal, onCleanup, Show, type ParentComponent } from 'solid-js';
import { A, Route, Router, useLocation } from '@solidjs/router';
import { checkAccess } from '@stopnetic/js/browser';
import { loadPrivateData } from '../load-private-data.js';

const GuardedLayout: ParentComponent = props => {
  const location = useLocation();
  const path = () => location.pathname + location.search;
  const [result, setResult] = createSignal({ path: '', allowed: false, pending: true });
  createEffect(() => {
    const target = path();
    const controller = new AbortController();
    setResult({ path: target, allowed: false, pending: true });
    checkAccess(target, { signal: controller.signal }).then(value => {
      if (controller.signal.aborted) return;
      if (!value.allowed && value.reason === 'verification_required' && value.verificationUrl) locationRedirect(value.verificationUrl);
      setResult({ path: target, allowed: value.allowed, pending: false });
    }).catch(() => {
      if (!controller.signal.aborted) setResult({ path: target, allowed: false, pending: false });
    });
    onCleanup(() => controller.abort());
  });
  return <Show when={result().allowed && result().path === path() ? result() : undefined}
    fallback={<Show when={!result().pending && result().path === path()}>
      <p role="alert">Access unavailable.</p></Show>}>
    {_checked => <>
      <header><nav><A href="/">Home</A>{' '}<A href="/members">Members</A>{' '}
        <A href="/slow">Slow</A>{' '}<A href="/blocked">Blocked</A>{' '}<A href="/offline">Offline</A>
      </nav></header>
      {props.children}
      <footer>Example site</footer>
    </>}
  </Show>;
};
const locationRedirect = (url: string) => window.location.assign(url);

function Members() {
  const [data, setData] = createSignal('Loading data...');
  const controller = new AbortController();
  loadPrivateData(controller.signal)
    .then(value => { if (!controller.signal.aborted) setData(JSON.stringify(value) ?? 'No data.'); })
    .catch(() => { if (!controller.signal.aborted) setData('API access unavailable.'); });
  onCleanup(() => controller.abort());
  return <main><h1>Members</h1><p>{data()}</p></main>;
}
export default function App() {
  return <Router root={GuardedLayout}>
    <Route path="/" component={() => <main><h1>Home</h1></main>}/>
    <Route path="/members" component={Members}/>
    <Route path="/slow" component={() => <main><h1>Slow</h1></main>}/>
    <Route path="/blocked" component={() => <main><h1>Blocked</h1></main>}/>
    <Route path="/offline" component={() => <main><h1>Offline</h1></main>}/>
  </Router>;
}
```

#### src/solid/main.tsx


```tsx
import { render } from 'solid-js/web';
import App from './App';
render(() => <App/>, document.getElementById('app')!);
```

### Plain JavaScript navigation

Create the full layout only after checkAccess allows the target. Clicks and browser back/forward rerun the check. Abort old checks and data loads. Bundle this module with your browser build; native browsers need bundled imports or an import map.

#### src/vanilla/main.js


```js
import { checkAccess } from '@stopnetic/js/browser';
import { loadPrivateData } from '../load-private-data.js';
const root = document.getElementById('app');
let current;
async function showPage() {
  current?.abort();
  const controller = new AbortController(); current = controller;
  root.replaceChildren();
  try {
    const path = location.pathname;
    const result = await checkAccess(path + location.search, { signal: controller.signal });
    if (controller.signal.aborted) return;
    if (!result.allowed) {
      if (result.reason === 'verification_required' && result.verificationUrl) location.assign(result.verificationUrl);
      const alert = document.createElement('p'); alert.setAttribute('role', 'alert');
      alert.textContent = 'Access unavailable.'; root.replaceChildren(alert); return;
    }
    const header = document.createElement('header');
    const nav = document.createElement('nav'); header.append(nav);
    for (const [url, name] of [['/', 'Home'], ['/members', 'Members'], ['/slow', 'Slow'], ['/blocked', 'Blocked'], ['/offline', 'Offline']]) {
      const link = document.createElement('a'); link.href = url; link.textContent = name;
      link.addEventListener('click', event => {
        if (event.button !== 0 || event.metaKey || event.ctrlKey || event.shiftKey || event.altKey) return;
        event.preventDefault(); history.pushState(null, '', url); void showPage();
      });
      nav.append(link, document.createTextNode(' '));
    }
    const main = document.createElement('main'); const heading = document.createElement('h1');
    heading.textContent = path === '/members' ? 'Members' : path === '/' ? 'Home' : path === '/slow' ? 'Slow' : 'Not found';
    const data = document.createElement('p'); main.append(heading, data);
    const footer = document.createElement('footer'); footer.textContent = 'Example site';
    root.replaceChildren(header, main, footer);
    if (path === '/members') {
      data.textContent = 'Loading data...';
      try {
        const value = await loadPrivateData(controller.signal);
        if (!controller.signal.aborted) data.textContent = JSON.stringify(value);
      } catch { if (!controller.signal.aborted) data.textContent = 'API access unavailable.'; }
    }
  } catch {
    if (!controller.signal.aborted) root.replaceChildren(document.createTextNode('Access unavailable.'));
  }
}
addEventListener('popstate', showPage);
addEventListener('pagehide', () => current?.abort());
void showPage();
```

<!-- END FRONTEND EXAMPLES -->

## 5. Protect APIs independently

If the full application wrapper is in front of an API, it checks that API too.
Separate Netlify/Vercel API functions need their own wrapper:

```js
import { withStopnetic } from '@stopnetic/js/server';
const handler = withStopnetic(async (request, context) => {
  // Also enforce your application's login/data permissions.
  return Response.json({ example: true });
});
// Netlify: export default handler;
// export const config = { path: '/api/private', preferStatic: false };
// Vercel: export default { fetch: handler };
```

Use the public custom `/api/private` path in all clients and Policies, not
Netlify's default `/.netlify/functions/` alias. Fetch same-origin data with
`credentials: 'same-origin'`. API requests cannot run a verification page;
verify on an HTML page first. An API on another origin needs its own server
protection and session, or a guarded same-origin server proxy. CORS alone does
not authorize data, and the current SDK does not share sessions across hosts.

## 6. Rules, deployment and testing

Save the actual page as Page / Automatic browser flow and its APIs as API /
Require verified session in Policies. First matching rule wins. Unmatched paths
are public in the generated site's whole-site mode. Public and block rules still
require a valid signed policy response. Verification can be completed by capable
automated browsers; retain your own authentication and authorization.

Run `stopnetic-js check`, build and deploy. Configure server env in each hosted
context and redeploy after changing it. For local Netlify/Vercel functions use
the full local server, not just `ng serve` or Vite. For Node use
`node --env-file=.env.local your-server.mjs` or your server environment loader.

Test raw document requests, verified browser requests, direct private API calls,
denial and service failure. Check status/body, session cookies, JS/CSS MIME types,
client navigation, entry HTML, aliases, prerender outputs and caches. A denied
request must not execute the renderer or data handler. The offline CLI check
validates configuration and generated files, not deployed coverage.

With only `/members` protected, confirm `/about` and `/contact` render without
a Stopnetic checking screen. Change a dashboard rule and make a new document
request: the change must apply without editing a client route list. For SSR,
confirm permitted HTML contains server-rendered page content before JavaScript
runs, hydration still works, and the renderer is never called on denial. Verify
the deployed request actually reaches Node; an SSR build alone proves nothing
about a provider's deployed runtime. Optional SPA checks may have a pending delay.

Ordinary policy changes apply on subsequent guarded requests without reinstalling
or redeploying. Already loaded content is not recalled. Public HTML/JS/assets and
content bundled into frontend code remain public; keep private data in guarded
server endpoints.

## Updating an existing generated guide

After installing the new SDK, rerun `stopnetic-js init --platform <same-platform>`
(preview with `--dry-run`). An unchanged 0.0.2/0.0.3/0.0.4/0.0.5/0.0.6 generated guide is upgraded
with its setup manifest. Your key, cookie secret and server files are preserved.
Customized guides or handlers require manual merging; switching platforms also
requires review. This command does not publish, deploy or contact Stopnetic.

References: https://angular.dev/guide/ssr,
https://angular.dev/guide/routing/route-guards,
https://docs.netlify.com/build/frameworks/framework-setup-guides/angular/.
