Embedding in your application
Add the Schema Viewer to an existing Express, Fastify, NestJS or Next.js application as a protected, development-only page. Every file on this page is a tested example in examples/embedding/.
Development only. Authentication is mandatory.
Never enable, bundle or serve the Schema Viewer or its snapshots in production. Require authentication even for local development, and protect the page, every viewer file and the schema JSON on the server with the same check. The one exception is serve: a loopback-only viewer with no authentication, for a single developer on their own machine. Never share it. The policy is Schema Viewer UI: Security; what this package enforces and what your application must do is in SECURITY.md.
This page covers what is specific to Node.js. The generic rules (the shape of a protected embedding, snapshots, React, Vue, Angular and FastAPI frontends, authentication) are in the canonical Embedding in client applications guide.
serve or an embedded viewer
node-schema-viewer serve | Embedded Schema Viewer | |
|---|---|---|
| What it is | A standalone local viewer: its own node:http server for one snapshot | The viewer as a page of your existing application |
| Where | http://127.0.0.1:8765/, loopback only by default | Your application's origin, under /dev/schema-viewer/ |
| Authentication | None. It relies on loopback and a Host check | Your application's sign-in, or HTTP Basic Auth at minimum |
| Production | A developer tool you run by hand | Never registered in production; the examples prove it |
| Configuration | --snapshot, --host, --port (CLI) | Environment variables read by your application |
serve has no authentication of its own. Keep it on loopback for one developer; to reach it from another machine, keep it on loopback and put an authenticated HTTPS reverse proxy or an access-controlled tunnel in front of it. Embed the viewer when it belongs in your application's development environment, behind your sign-in.
What the package provides
The programmatic exports below are what the examples use. There is no framework middleware, mount helper or authentication in the package; the examples' lib/ adds those in two small files you can copy.
| Export | What it does |
|---|---|
loadViewer(directory?) | Reads laranex-schema-viewer-ui's viewer/manifest.json and returns a Map from URL path (/index.html, /app.js, /assets/…, and / for index.html) to { body, type }, for exactly the files the manifest lists, each checked against its SHA-256. JSON files (the manifest) are left out. Throws ViewerError on a mismatch or a viewer that does not read laranex-schema 2.x. |
loadDocument(path) | Reads a snapshot (at most 32 MiB) and validates it as laranex-schema 2.x. Throws SnapshotError. |
POLICY | The Content-Security-Policy serve sends, with frame-ancestors 'none'. |
createViewerServer(options) | The serve server itself (node:http, no authentication). It is not middleware; do not expose it. |
canonicalJson comes from laranex-schema-viewer-ui/contract, the contract package this one depends on.
The examples
examples/embedding/
lib/dev-auth.mjs development switch and Basic Auth (node:crypto only)
lib/schema-viewer.mjs the viewer's files and snapshot as { status, headers, body }
express/server.mjs Express 5
fastify/server.mjs Fastify 5
nestjs/src/main.ts NestJS 12 (Express platform)
nextjs/ Next.js 16: proxy.ts, route handler, React page
test/embedding.test.mjs starts each example and checks every rule below over HTTPThey read the same environment variables:
| Variable | Meaning |
|---|---|
SCHEMA_VIEWER_ENABLED | 1 turns the viewer on. Anything else, or NODE_ENV=production: no viewer routes, even with 1. |
SCHEMA_VIEWER_USER, SCHEMA_VIEWER_PASSWORD | Basic Auth credentials. Required when enabled; passwords under 16 characters are refused. Errors name missing variables, never values. |
SCHEMA_VIEWER_SNAPSHOT | Path of the snapshot, outside public/, static/ and anything a bundler reads. |
Install node-schema-viewer and laranex-schema-viewer-ui as development dependencies. The server frameworks import lib/schema-viewer.mjs with a dynamic import() only after the switch is on, so a production install (npm ci --omit=dev) has neither package, and the application starts and serves no viewer route. Build the snapshot with build and keep it out of version control:
npx node-schema-viewer build --output ../.schema/schema-viewer.jsonShared code
isAuthorized compares SHA-256 digests of the user name and the password with timingSafeEqual, both every time. UNAUTHORIZED is the 401 every example sends: WWW-Authenticate: Basic realm="…", Cache-Control: no-store and a body without schema details. Nothing logs the Authorization header.
// Development-only switch and HTTP Basic Auth for the embedded Schema Viewer. No dependencies beyond node:crypto, so
// it can run in front of the viewer (Express, Fastify, NestJS) and in a Next.js proxy.
import { createHash, timingSafeEqual } from 'node:crypto';
export const PREFIX = '/dev/schema-viewer';
const MIN_PASSWORD_LENGTH = 16;
// The viewer is off unless SCHEMA_VIEWER_ENABLED=1, and always off when NODE_ENV=production. When it is on, the
// credentials and the snapshot path are required; the error names the missing variables, never their values.
export function devViewerConfig(env = process.env) {
if (env.NODE_ENV === 'production' || env.SCHEMA_VIEWER_ENABLED !== '1') {
return { enabled: false };
}
const missing = ['SCHEMA_VIEWER_USER', 'SCHEMA_VIEWER_PASSWORD', 'SCHEMA_VIEWER_SNAPSHOT'].filter((name) => !env[name]);
if (missing.length > 0) {
throw new Error(`The development schema viewer is enabled but ${missing.join(', ')} ${missing.length === 1 ? 'is' : 'are'} not set`);
}
if (env.SCHEMA_VIEWER_PASSWORD.length < MIN_PASSWORD_LENGTH) {
throw new Error(`SCHEMA_VIEWER_PASSWORD must be at least ${MIN_PASSWORD_LENGTH} characters`);
}
return { enabled: true, user: env.SCHEMA_VIEWER_USER, password: env.SCHEMA_VIEWER_PASSWORD, snapshot: env.SCHEMA_VIEWER_SNAPSHOT };
}
const digest = (value) => createHash('sha256').update(value, 'utf8').digest();
// Compares SHA-256 digests with timingSafeEqual, so neither the length nor the content of a guess changes the timing.
// Both comparisons always run.
export function isAuthorized(header, { user, password }) {
const match = /^Basic[ ]+([A-Za-z0-9+/=]+)$/i.exec(header ?? '');
if (!match) {
return false;
}
const decoded = Buffer.from(match[1], 'base64').toString('utf8');
const colon = decoded.indexOf(':');
if (colon === -1) {
return false;
}
const userOk = timingSafeEqual(digest(decoded.slice(0, colon)), digest(user));
const passwordOk = timingSafeEqual(digest(decoded.slice(colon + 1)), digest(password));
return userOk && passwordOk;
}
// The 401 every framework sends: a challenge and a generic body, nothing about the schema.
export const UNAUTHORIZED = {
status: 401,
headers: {
'WWW-Authenticate': 'Basic realm="Schema Viewer (development)", charset="UTF-8"',
'Cache-Control': 'no-store',
'Content-Type': 'text/plain; charset=utf-8',
},
body: 'Authentication required\n',
};createSchemaViewer answers one request below the prefix. It checks neither credentials nor the Host header, so call it only after your application's authentication has accepted the request. It serves exactly the manifest's files, re-reads and validates the snapshot on every request (503 while it is invalid), and sends Cache-Control: no-store and X-Content-Type-Options: nosniff on every response, errors included. Pass a policy with frame-ancestors 'self' if your own pages show the viewer in an <iframe>.
// The embedded viewer's files and snapshot as plain { status, headers, body } responses, so Express, Fastify, NestJS and
// Next.js route handlers send the same thing. Import it only after the development switch is on: node-schema-viewer is
// a development dependency and is not installed in production.
import { loadDocument, loadViewer, POLICY } from 'node-schema-viewer';
import { canonicalJson } from 'laranex-schema-viewer-ui/contract';
const COMMON = { 'Cache-Control': 'no-store', 'X-Content-Type-Options': 'nosniff' };
const text = (status, message, extra = {}) => ({
status,
headers: { ...COMMON, 'Content-Type': 'text/plain; charset=utf-8', ...extra },
body: Buffer.from(`${message}\n`),
});
// prefix: where the viewer is mounted, e.g. /dev/schema-viewer. policy: the Content-Security-Policy of every response;
// node-schema-viewer's POLICY forbids framing (frame-ancestors 'none'), so pass one with frame-ancestors 'self' to
// show the viewer in an iframe of the same application.
export function createSchemaViewer({ snapshot, prefix, policy = POLICY }) {
loadDocument(snapshot); // Fail at startup when the snapshot is missing or not a laranex-schema 2.x document.
const files = loadViewer(); // Only the files laranex-schema-viewer-ui's manifest lists, checked by SHA-256.
const headers = (type) => ({ ...COMMON, 'Content-Security-Policy': policy, 'Content-Type': type });
// path: the request path below the prefix ('' for the prefix itself, '/', '/app.js', '/snapshot.json', ...).
// It checks neither credentials nor the Host header: call it only after the application's authentication has
// accepted the request. Every response, errors included, carries Cache-Control: no-store.
return function respond(method, path) {
if (method !== 'GET' && method !== 'HEAD') {
return text(405, 'Method not allowed', { Allow: 'GET, HEAD' });
}
if (path === '') {
// index.html loads app.css, app.js and snapshot.json by relative URLs, so it needs the trailing slash.
return text(308, 'Permanent redirect', { Location: `${prefix}/` });
}
if (path === '/snapshot.json') {
try {
// Re-read and validated on every request, so a rebuilt snapshot shows up on reload.
return { status: 200, headers: headers('application/json; charset=utf-8'), body: Buffer.from(canonicalJson(loadDocument(snapshot))) };
} catch {
return text(503, 'Snapshot unavailable');
}
}
const file = files.get(path);
return file ? { status: 200, headers: headers(file.type), body: file.body } : text(404, 'Not found');
};
}Express
app.use(PREFIX, requireDeveloper, …) puts the check in front of every path under the prefix: the page, each viewer file and snapshot.json.
// Express 5: the application's own routes plus, in development only, the Schema Viewer at /dev/schema-viewer behind
// HTTP Basic Auth. Every viewer request (the page, each asset and snapshot.json) passes the same check first.
import express from 'express';
import { devViewerConfig, isAuthorized, PREFIX, UNAUTHORIZED } from '../lib/dev-auth.mjs';
const app = express();
app.disable('x-powered-by');
app.get('/', (request, response) => {
response.type('text').send('The application\n');
});
const viewer = devViewerConfig();
if (viewer.enabled) {
// Loaded only when enabled: production installs omit node-schema-viewer (a development dependency).
const { createSchemaViewer } = await import('../lib/schema-viewer.mjs');
const respond = createSchemaViewer({ snapshot: viewer.snapshot, prefix: PREFIX });
const requireDeveloper = (request, response, next) => {
// Replace this with your application's own developer or admin check where you have one, e.g.
// `if (request.user?.roles.includes('developer')) return next();` after your session middleware.
if (isAuthorized(request.get('authorization'), viewer)) {
next();
return;
}
response.status(UNAUTHORIZED.status).set(UNAUTHORIZED.headers).send(UNAUTHORIZED.body);
};
app.use(PREFIX, requireDeveloper, (request, response) => {
// Inside app.use, request.path is relative to the prefix; originalUrl tells /dev/schema-viewer from .../.
const bare = request.originalUrl.split('?')[0] === PREFIX;
const result = respond(request.method, bare ? '' : request.path);
response.status(result.status).set(result.headers).send(result.body);
});
}
const port = Number(process.env.PORT ?? 3000);
const host = process.env.HOST ?? '127.0.0.1';
app.listen(port, host, () => {
console.log(`Listening on http://${host}:${port}${viewer.enabled ? ` (schema viewer at ${PREFIX}/)` : ''}`);
});Fastify
An encapsulated plugin with the prefix; its onRequest hook runs before each of its routes.
// Fastify 5: the application's own routes plus, in development only, the Schema Viewer at /dev/schema-viewer behind
// HTTP Basic Auth. The plugin's onRequest hook runs before every route it registers: the page, each asset and
// snapshot.json.
import Fastify from 'fastify';
import { devViewerConfig, isAuthorized, PREFIX, UNAUTHORIZED } from '../lib/dev-auth.mjs';
const app = Fastify({ logger: false });
app.get('/', async () => 'The application\n');
const viewer = devViewerConfig();
if (viewer.enabled) {
// Loaded only when enabled: production installs omit node-schema-viewer (a development dependency).
const { createSchemaViewer } = await import('../lib/schema-viewer.mjs');
const respond = createSchemaViewer({ snapshot: viewer.snapshot, prefix: PREFIX });
await app.register(
async (scope) => {
scope.addHook('onRequest', async (request, reply) => {
// Or your application's own authentication (a session, @fastify/jwt, ...) with a developer role check.
if (!isAuthorized(request.headers.authorization, viewer)) {
return reply.code(UNAUTHORIZED.status).headers(UNAUTHORIZED.headers).send(UNAUTHORIZED.body);
}
});
const send = (request, reply, path) => {
const result = respond(request.method, path);
return reply.code(result.status).headers(result.headers).send(result.body);
};
// With a prefix, '/' answers both /dev/schema-viewer and /dev/schema-viewer/; GET routes also answer HEAD.
scope.get('/', (request, reply) => send(request, reply, request.url.split('?')[0].endsWith('/') ? '/' : ''));
scope.get('/*', (request, reply) => send(request, reply, `/${request.params['*']}`));
},
{ prefix: PREFIX },
);
}
const port = Number(process.env.PORT ?? 3000);
const host = process.env.HOST ?? '127.0.0.1';
await app.listen({ port, host });
console.log(`Listening on http://${host}:${port}${viewer.enabled ? ` (schema viewer at ${PREFIX}/)` : ''}`);NestJS
Nest guards run only for controllers, so they cannot protect these files. The viewer is functional middleware on the Express instance, registered in main.ts before Nest's routes.
// NestJS 12 (Express platform): the application's module plus, in development only, the Schema Viewer at
// /dev/schema-viewer behind HTTP Basic Auth. The viewer is functional middleware on the underlying Express instance,
// registered before Nest's routes, so every viewer request (the page, each asset and snapshot.json) passes the same
// check first. Nest guards only run for controllers, so they cannot protect these files.
import 'reflect-metadata';
import { NestFactory } from '@nestjs/core';
import type { NestExpressApplication } from '@nestjs/platform-express';
import type { NextFunction, Request, Response } from 'express';
import { devViewerConfig, isAuthorized, PREFIX, UNAUTHORIZED } from '../../lib/dev-auth.mjs';
import { AppModule } from './app.module.js';
const app = await NestFactory.create<NestExpressApplication>(AppModule, { logger: ['error', 'warn'] });
app.disable('x-powered-by');
const viewer = devViewerConfig();
if (viewer.enabled) {
// Loaded only when enabled: production installs omit node-schema-viewer (a development dependency).
const { createSchemaViewer } = await import('../../lib/schema-viewer.mjs');
const respond = createSchemaViewer({ snapshot: viewer.snapshot, prefix: PREFIX });
const requireDeveloper = (request: Request, response: Response, next: NextFunction): void => {
// Or your application's own authentication middleware (a session or passport strategy) with a role check.
if (isAuthorized(request.get('authorization'), viewer)) {
next();
return;
}
response.status(UNAUTHORIZED.status).set(UNAUTHORIZED.headers).send(UNAUTHORIZED.body);
};
app.use(PREFIX, requireDeveloper, (request: Request, response: Response) => {
const bare = request.originalUrl.split('?')[0] === PREFIX;
const result = respond(request.method, bare ? '' : request.path);
response.status(result.status).set(result.headers).send(result.body);
});
}
const port = Number(process.env.PORT ?? 3000);
const host = process.env.HOST ?? '127.0.0.1';
await app.listen(port, host);
console.log(`Listening on http://${host}:${port}${viewer.enabled ? ` (schema viewer at ${PREFIX}/)` : ''}`);Next.js
Next.js needs server-side protection and client-side rendering kept apart, and the viewer kept out of next build:
proxy.tsis the first gate for every path under/dev/schema-viewer:404unless the viewer is enabled,401without valid credentials.- The route handler serves the standalone viewer and
snapshot.jsonunder the prefix and checks again; the React page checks again on the server. The proxy is not the only control. - The viewer's routes are
*.dev.ts/*.dev.tsxfiles, whichnext.config.mjstreats as routes only innext devwithSCHEMA_VIEWER_ENABLED=1.next builddoes not see them: the production build has no viewer page, handler, viewer code or snapshot.
import { PHASE_DEVELOPMENT_SERVER } from 'next/constants.js';
// The viewer's routes are *.dev.ts(x) files. They are routes only in `next dev` with SCHEMA_VIEWER_ENABLED=1; every
// other phase, `next build` included, does not treat them as routes, so the production build has no viewer page, no
// viewer route handler and no viewer code.
export default function config(phase) {
const devViewer = phase === PHASE_DEVELOPMENT_SERVER && process.env.SCHEMA_VIEWER_ENABLED === '1';
return {
pageExtensions: devViewer ? ['dev.tsx', 'dev.ts', 'tsx', 'ts'] : ['tsx', 'ts'],
// node-schema-viewer reads the viewer files beside its own module at run time; let Node.js load it unbundled.
serverExternalPackages: ['node-schema-viewer'],
poweredByHeader: false,
agentRules: false,
};
}// The first gate: every request under /dev/schema-viewer (the standalone page, each of its files, snapshot.json and the
// React page) is answered 404 unless the development viewer is enabled, and 401 without valid credentials. The route
// handler and the page check again, so the proxy is not the only control.
import { NextResponse, type NextRequest } from 'next/server';
import { devViewerConfig, isAuthorized, UNAUTHORIZED } from '../lib/dev-auth.mjs';
export function proxy(request: NextRequest): NextResponse {
const viewer = devViewerConfig();
if (!viewer.enabled) {
return new NextResponse('Not found\n', { status: 404, headers: { 'Content-Type': 'text/plain; charset=utf-8', 'Cache-Control': 'no-store' } });
}
if (!isAuthorized(request.headers.get('authorization'), viewer)) {
return new NextResponse(UNAUTHORIZED.body, { status: UNAUTHORIZED.status, headers: UNAUTHORIZED.headers });
}
return NextResponse.next();
}
export const config = { matcher: ['/dev/schema-viewer', '/dev/schema-viewer/:path*'] };The standalone viewer at /dev/schema-viewer/index.html. Next.js removes trailing slashes, so the prefix itself redirects to index.html, where the page's relative URLs (app.js, app.css, snapshot.json) resolve:
// The standalone viewer under /dev/schema-viewer/: index.html, app.js, app.css, assets/* and snapshot.json, all
// answered here and all behind the same check. Exists only in `next dev` with SCHEMA_VIEWER_ENABLED=1 (next.config.mjs).
import { devViewerConfig, isAuthorized, PREFIX, UNAUTHORIZED } from '../../../../../lib/dev-auth.mjs';
import { createSchemaViewer } from '../../../../../lib/schema-viewer.mjs';
export const dynamic = 'force-dynamic';
let respond: ReturnType<typeof createSchemaViewer> | undefined;
async function handle(request: Request, { params }: { params: Promise<{ path?: string[] }> }): Promise<Response> {
const viewer = devViewerConfig();
if (!viewer.enabled) {
return new Response('Not found\n', { status: 404, headers: { 'Content-Type': 'text/plain; charset=utf-8', 'Cache-Control': 'no-store' } });
}
if (!isAuthorized(request.headers.get('authorization'), viewer)) {
return new Response(UNAUTHORIZED.body, { status: UNAUTHORIZED.status, headers: UNAUTHORIZED.headers });
}
const { path = [] } = await params;
if (path.length === 0) {
// Next.js removes trailing slashes, so the page lives at index.html, where its relative URLs resolve.
return new Response(null, { status: 308, headers: { Location: `${PREFIX}/index.html`, 'Cache-Control': 'no-store' } });
}
respond ??= createSchemaViewer({ snapshot: viewer.snapshot, prefix: PREFIX });
const result = respond(request.method, `/${path.join('/')}`);
return new Response(request.method === 'HEAD' ? null : new Uint8Array(result.body), { status: result.status, headers: result.headers });
}
export { handle as GET, handle as HEAD };The React component at /dev/schema-viewer/react: a server component that checks the credentials and renders a client component, which fetches the snapshot from the protected route after the page loads, so the schema is never in a JavaScript bundle or in the server-rendered HTML.
// The React component in an application page at /dev/schema-viewer/react. The page checks the credentials on the
// server before rendering (the proxy already did); the browser then fetches the snapshot from the protected
// /dev/schema-viewer/snapshot.json, so the schema is never part of a JavaScript bundle.
import { headers } from 'next/headers';
import { notFound } from 'next/navigation';
import { devViewerConfig, isAuthorized, PREFIX } from '../../../../../lib/dev-auth.mjs';
import { SchemaViewerClient } from './schema-viewer-client.dev';
export const dynamic = 'force-dynamic';
export default async function SchemaViewerPage() {
const viewer = devViewerConfig();
if (!viewer.enabled || !isAuthorized((await headers()).get('authorization'), viewer)) {
notFound();
}
return <SchemaViewerClient snapshotUrl={`${PREFIX}/snapshot.json`} />;
}'use client';
import { useEffect, useState } from 'react';
import { SchemaViewer, validateDocument, type SchemaDocument } from 'laranex-schema-viewer-ui';
import 'laranex-schema-viewer-ui/styles.css';
type State = { document: SchemaDocument } | { error: string } | null;
export function SchemaViewerClient({ snapshotUrl }: { snapshotUrl: string }) {
const [state, setState] = useState<State>(null);
useEffect(() => {
let active = true;
fetch(snapshotUrl, { credentials: 'same-origin', cache: 'no-store', headers: { Accept: 'application/json' } })
.then((response) => (response.ok ? response.json() : Promise.reject(new Error(`The snapshot request failed (${response.status}).`))))
.then((data: unknown) => {
const result = validateDocument(data);
if (active) {
setState(result.ok ? { document: result.document } : { error: result.errors.join('\n') });
}
})
.catch((error: unknown) => {
if (active) {
setState({ error: error instanceof Error ? error.message : String(error) });
}
});
return () => {
active = false;
};
}, [snapshotUrl]);
if (state === null) {
return <p>Loading the schema…</p>;
}
if ('error' in state) {
return <pre>{state.error}</pre>;
}
return (
<div style={{ height: '100dvh' }}>
<SchemaViewer document={state.document} downloadHref={snapshotUrl} />
</div>
);
}What the proxy does not cover
The React page's JavaScript chunks come from /_next/static/, outside the prefix. In next dev they hold the viewer's library code, never the schema. If your policy requires the viewer's code itself to be unreachable without credentials, use the standalone route, which serves every viewer file under the prefix, or put the whole development server behind authentication.
Use your application's authentication
Basic Auth is the minimum. Where your application already signs developers in, put that check in the same place: before createSchemaViewer, on the server, for every path under the prefix. For example, in Express after your session middleware, with your own user and role names:
const requireDeveloper = (request, response, next) => {
if (request.user?.roles?.includes('developer')) {
next();
return;
}
response.status(request.user ? 403 : 401).set('Cache-Control', 'no-store').send('Not allowed\n');
};In Fastify, use the same condition in the onRequest hook after your authentication plugin; in NestJS, register your session or passport middleware before the viewer middleware; in Next.js, check your session in proxy.ts, the route handler and the page. Client-side route guards, hidden links and obscure URLs are not access controls.
Static assets
Serve the viewer only through loadViewer's map, as createSchemaViewer does. Do not point express.static, @fastify/static or Next.js public/ at node_modules/laranex-schema-viewer-ui/dist/viewer: they would serve the files without your check (or under a second, unprotected path) and include files the manifest does not list. The files are held in memory, so serving them costs no disk access.
Frontend dev servers in a monorepo
When a Vite (React or Vue) or Angular frontend runs its own dev server in front of a Node.js backend, the backend serves and protects /dev/schema-viewer/, and the dev server proxies that prefix to it with the Authorization header intact. The proxy configuration and the frontend side are in the canonical guide's FastAPI and Angular sections, which apply unchanged to these backends.
Run the tests
From the repository root, after npm ci:
npm run test:examplesIt installs the examples (examples/embedding/, with this package built and packed from the checkout) and runs test/embedding.test.mjs with credentials generated for the run, never printed, and a synthetic fixture of laranex-schema-viewer-ui as the snapshot. For Express, Fastify, NestJS and next dev it checks that:
- without credentials, or with a wrong user name, a wrong password, a malformed header or another scheme, the page, every file in the viewer's manifest,
snapshot.jsonand unknown paths under the prefix answer401withWWW-Authenticate: BasicandCache-Control: no-store, forGETandHEAD, with no schema or viewer content; - with credentials, every viewer file matches its manifest SHA-256,
snapshot.jsonis the fixture, and the manifest and unknown files are404withno-store; - with
NODE_ENV=production(even withSCHEMA_VIEWER_ENABLED=1) or without the opt-in, every viewer path is404while the application still answers; - the application refuses to start when the viewer is enabled without a password or with a short one, without echoing any value;
next buildwith the opt-in set produces no viewer route, no viewer file, no viewer code and no snapshot, andnext startanswers404under the prefix.
Next.js answers /dev/schema-viewer/ (with a trailing slash) itself with a redirect to /dev/schema-viewer, before the proxy runs; the redirect carries no content.
Limitations
- The package has no middleware, mount helper or authentication for frameworks; copy the examples'
lib/files. They are examples: authentication, and keeping the viewer out of production, remain your application's responsibility. serveandcreateViewerServerhave no authentication and are not meant to be mounted inside an application.POLICYforbids framing; pass your own policy tocreateSchemaViewerto show the viewer in an<iframe>.- Basic Auth has no logout, no per-user audit and no rate limiting of its own; see Security.