import { FastifyRequest, FastifyReply, FastifyPluginAsync, FastifyPluginCallback, FastifyInstance, preHandlerHookHandler, FastifySchema, FastifyBaseLogger, FastifyLoggerOptions } from 'fastify'; export { FastifyReply, FastifyRequest, FastifyReply as ServerReply, FastifyRequest as ServerRequest } from 'fastify'; import { CookieSerializeOptions } from '@fastify/cookie'; import { SecureContext } from 'tls'; import { APIResponseHelpers } from 'unirend/api-envelope'; import { WebSocket } from 'ws'; import { ReactNode } from 'react'; import { RouteObject } from 'react-router'; import { Logger, LoggerService } from 'lifecycleion/logger'; interface PageMetadata { title: string; description: string; keywords?: string; canonical?: string; og?: { title?: string; description?: string; image?: string; }; } /** * Base meta structure with required page metadata */ interface BaseMeta { page?: PageMetadata; } interface ErrorDetails { [key: string]: unknown; } /** * Error details value - supports both object and array formats * * @example Object format (structured errors) * { field: 'email', reason: 'invalid format', code: 'VALIDATION_ERROR' } * * @example Array format (multiple validation errors with type field) * [ * { field: 'email', type: 'invalid_email', message: 'Must be a valid email address' }, * { field: 'password', type: 'invalid_length', message: 'Must be at least 8 characters long' } * ] * * @example Array format (error trace) * ['Step 1 failed', 'Rollback initiated', 'Cleanup completed'] */ type ErrorDetailsValue = ErrorDetails | unknown[]; /** * Error object structure for API error responses */ interface ErrorObject { code: string; message: string; details?: ErrorDetailsValue; } interface RedirectInfo { target: string; permanent: boolean; preserve_query?: boolean; } /** * API Success Response with extensible meta * * @template T - The data type * @template M - Additional meta properties (extends BaseMeta) * * @example * // Basic usage (no extra meta) * type BasicResponse = APISuccessResponse; * * @example * // With required extra meta fields * interface CustomMeta extends BaseMeta { * pagination: { page: number; total: number }; * cache: { expires: string }; * } * type PaginatedResponse = APISuccessResponse; */ interface APISuccessResponse { status: 'success'; status_code: number; request_id: string; request_timestamp?: string; type: 'api'; data: T; meta: M; error?: null; } /** * API Error Response with extensible meta * * @template M - Additional meta properties (extends BaseMeta) */ interface APIErrorResponse { status: 'error'; status_code: number; request_id: string; request_timestamp?: string; type: 'api'; data: null; meta: M; error: ErrorObject; } /** * API response envelope as a discriminated union * * @template T - The data type for success responses * @template M - Additional meta properties (extends BaseMeta) */ type APIResponseEnvelope = APISuccessResponse | APIErrorResponse; /** * Page Success Response with extensible meta * * @template T - The data type * @template M - Additional meta properties (extends BaseMeta) */ interface PageSuccessResponse { status: 'success'; status_code: number; request_id: string; request_timestamp?: string; type: 'page'; data: T; meta: M; error?: null; ssr_request_context?: Record; } /** * Page Error Response with extensible meta * * @template M - Additional meta properties (extends BaseMeta) */ interface PageErrorResponse { status: 'error'; status_code: number; request_id: string; request_timestamp?: string; type: 'page'; data: null; meta: M; error: ErrorObject; ssr_request_context?: Record; } /** * Page Redirect Response with extensible meta * * @template M - Additional meta properties (extends BaseMeta) */ interface PageRedirectResponse { status: 'redirect'; status_code: 200; request_id: string; request_timestamp?: string; type: 'page'; data: null; meta: M; error?: null; redirect: RedirectInfo; ssr_request_context?: Record; } /** * Page response envelope as a discriminated union * * @template T - The data type for success responses * @template M - Additional meta properties (extends BaseMeta) */ type PageResponseEnvelope = PageSuccessResponse | PageErrorResponse | PageRedirectResponse; /** * Parameters passed to page data loader handlers with shortcuts to common fields * * Handlers should treat these params as the authoritative routing context * (routeParams, queryParams, requestPath, originalURL) produced by the * page data loader. Do not reconstruct routing info from the Fastify request. * * The Fastify request represents the original HTTP request and should be used * only for transport/ambient data (cookies, headers, IP, auth tokens, etc.). * During SSR, this is the same request that initiated the page render; after * hydration, client-side page data loader calls will include their own * transport context as appropriate. */ interface PageDataHandlerParams { pageType: string; version?: number; /** Indicates how the handler was invoked: via HTTP route or internal short-circuit */ invocationOrigin: 'http' | 'internal'; /** Route params (from React Router via POST body) */ routeParams: Record; /** Query params (from React Router via POST body) */ queryParams: Record; /** Request path (from React Router via POST body) */ requestPath: string; /** Original URL (from React Router via POST body) */ originalURL: string; /** The APIResponseHelpers class configured on this server (use this instead of importing directly) */ APIResponseHelpers: APIResponseHelpersClass; } /** * Handler function type for page data endpoints * * @param request - The Fastify request object (original request). Use for cookies, headers, IP, etc. * @param params - Page data context (preferred for page routing: path, query, route params) * @returns A PageResponseEnvelope (recommended), APIResponseEnvelope (will be converted), or false if response already sent * * **Recommendation**: Return PageResponseEnvelope for optimal performance and control. * APIResponseEnvelope is supported but will be converted by the pageDataLoader, which * adds overhead and may not preserve all metadata as intended. * * **Return false** when you've sent a custom response (e.g., using * APIResponseHelpers.sendErrorEnvelope() or validation helpers like ensureJSONBody). * This signals that the handler has already sent the response and the framework * should not attempt to send anything. */ type PageDataHandler = ( /** Original HTTP request (for cookies/headers/IP/auth) */ originalRequest: FastifyRequest, reply: ControlledReply, params: PageDataHandlerParams) => Promise | APIResponseEnvelope | false> | PageResponseEnvelope | APIResponseEnvelope | false; /** * Parsed domain information derived from a request hostname. * * Used both on the Fastify request object (`request.domainInfo`) and in the * Unirend React context (`useDomainInfo()`). */ interface DomainInfo { /** Bare hostname with port stripped (IPv6-safe, e.g. `'app.example.com'` or `'::1'`). */ hostname: string; /** * Apex domain without a leading dot (e.g. `'example.com'`). * Empty string for localhost and raw IP addresses where no root domain can be resolved. * * When empty, omit the `domain` attribute entirely — `domain=.localhost` is invalid * per RFC 6265 and browsers reject it. A cookie without a `domain` attribute becomes * a host-only cookie scoped to the exact hostname, which is correct for localhost and IPs. * * Prepend `.` when using as a cookie `domain` attribute to span subdomains: * ```ts * document.cookie = [ * 'theme=dark', * 'path=/', * 'max-age=31536000', * domainInfo.rootDomain ? `domain=.${domainInfo.rootDomain}` : null, * ].filter(Boolean).join('; '); * ``` */ rootDomain: string; } /** * Render mode type - SSR, SSG, or Client * - "ssr": Server-Side Rendering (runtime server rendering) * - "ssg": Static Site Generation (build-time server rendering) * - "client": Client-side execution (SPA or after a SSG build/SSR page hydration occurs) */ type UnirendRenderMode = 'ssr' | 'ssg' | 'client'; /** * Unirend context value type */ interface UnirendContextValue { /** * The render mode: * - 'ssr': Server-Side Rendering * - 'ssg': Static Site Generation * - 'client': Client-side execution (SPA or after a SSG build/SSR page hydration occurs) */ renderMode: UnirendRenderMode; /** * Whether the app is running in development mode */ isDevelopment: boolean; /** * The Fetch API Request object (available during SSR/SSG rendering) * Undefined on client-side after hydration */ fetchRequest?: Request; /** * Public application configuration * This is a frozen (immutable) copy of the config passed to the server * Available on both server and client (injected into HTML during SSR/SSG) */ publicAppConfig?: Record; /** * CDN base URL for asset serving (e.g. 'https://cdn.example.com') * Available on both server (from app config or per-request override) and client * (read from window.__CDN_BASE_URL__ injected into HTML by the server) * Empty string when no CDN is configured */ cdnBaseURL?: string; /** * Domain information computed server-side from the request hostname. * Available during SSR (computed per-request) and SSG when a `hostname` option is * provided at build time. `null` when hostname is not known (SSG without hostname * configured, or pure SPA — no server to compute it via the public suffix list). */ domainInfo?: DomainInfo | null; /** * Request context revision counter for reactivity * Format: `${timestamp}-${counter}` (e.g., "1729123456789-0", "1729123456789-1") * Increments whenever request context is modified to trigger re-renders * @internal */ requestContextRevision?: string; } type RenderType = 'ssg' | 'ssr'; type APIResponseHelpersClass = typeof APIResponseHelpers; type FastifyTrustProxyFunction = (address: string, hop: number) => boolean; interface RenderRequest { type: RenderType; fetchRequest: Request; /** * Unirend context value to provide to the app * Contains render mode, development status, and server request info * Always provided by SSRServer or SSG */ unirendContext: UnirendContextValue; } /** * Base interface for render results with a discriminated union type */ interface RenderResultBase { resultType: 'page' | 'response' | 'render-error'; } /** * Page result containing HTML content */ interface RenderPageResult extends RenderResultBase { resultType: 'page'; html: string; preloadLinks: string; head?: { title: string; meta: string; link: string; }; statusCode?: number; errorDetails?: Error; ssOnlyData?: Record; } /** * Response result wrapping a standard Response object * Used for redirects, errors, or any other non-HTML responses */ interface RenderResponseResult extends RenderResultBase { resultType: 'response'; response: Response; } /** * Error result containing error information * Used when rendering fails with an exception */ interface RenderErrorResult extends RenderResultBase { resultType: 'render-error'; error: Error; } /** * Union type for all possible render results */ type RenderResult = RenderPageResult | RenderResponseResult | RenderErrorResult; /** * Required paths for SSR development server */ interface SSRDevPaths { /** Path to the server entry file (e.g. "./src/EntrySSR.tsx") */ serverEntry: string; /** Path to the HTML template file (e.g. "./index.html") */ template: string; /** Path to the Vite config file (e.g. "./vite.config.ts") */ viteConfig: string; } /** * Plugin metadata returned by plugins for dependency tracking and cleanup */ interface PluginMetadata { /** Unique name for this plugin */ name: string; /** Plugin dependencies - other plugin names that must be registered first */ dependsOn?: string | string[]; } /** * Plugin registration function type * Plugins get access to a controlled subset of Fastify functionality * Can optionally return metadata for dependency tracking */ type ServerPlugin = (pluginHost: PluginHostInstance, options: PluginOptions) => Promise | PluginMetadata | void; /** * Fastify hook names that plugins can register * Includes common lifecycle hooks plus string for custom hooks */ type FastifyHookName = Parameters[0]; /** * Controlled Fastify instance interface for plugins * Exposes safe methods while preventing access to destructive operations */ interface PluginHostInstance { /** Register plugins and middleware */ register: = Record>(plugin: FastifyPluginAsync | FastifyPluginCallback, opts?: Options) => Promise; /** Add custom hooks */ addHook: (hookName: FastifyHookName, handler: (request: FastifyRequest, reply: FastifyReply, ...args: unknown[]) => unknown) => void; /** Add decorators to request/reply objects */ decorate: (property: string, value: unknown) => void; decorateRequest: (property: string, value: unknown) => void; decorateReply: (property: string, value: unknown) => void; /** Read-only accessors for server-level decorations */ hasDecoration: (property: string) => boolean; getDecoration: (property: string) => T | undefined; /** Access to route registration with constraints */ route: (opts: SafeRouteOptions) => void; get: (path: string, handler: RouteHandler) => void; post: (path: string, handler: RouteHandler) => void; put: (path: string, handler: RouteHandler) => void; delete: (path: string, handler: RouteHandler) => void; patch: (path: string, handler: RouteHandler) => void; /** Server-level logger (pino). Use `(obj, msg)` argument order. Useful for logging during plugin setup, before any request exists. */ log: FastifyBaseLogger; /** API route registration shortcuts method for versioned endpoints */ api?: unknown; /** Page data loader handler registration method for page data endpoints */ pageDataHandler?: unknown; /** The APIResponseHelpers class configured on this server — use this to build envelopes so custom subclasses are respected */ APIResponseHelpers: APIResponseHelpersClass; } /** * Controlled reply surface available to handlers. * Allows setting headers and cookies without giving full reply control. * * Used by page data loader handlers, API route handlers, and processFileUpload(). * Provides limited access to prevent handlers from prematurely sending responses * or bypassing the framework's envelope pattern. */ interface ControlledReply { /** Set a response header (content-type may be enforced by framework) */ header: (name: string, value: string) => void; /** Set a cookie if @fastify/cookie is registered */ setCookie?: (name: string, value: string, options?: CookieSerializeOptions) => void; /** Alias for setCookie if @fastify/cookie is registered */ cookie?: (name: string, value: string, options?: CookieSerializeOptions) => void; /** Clear a cookie if @fastify/cookie is registered */ clearCookie?: (name: string, options?: CookieSerializeOptions) => void; /** Verify and unsign a cookie value if @fastify/cookie is registered */ unsignCookie?: (value: string) => { valid: true; renew: boolean; value: string; } | { valid: false; renew: false; value: null; }; /** Sign a cookie value if @fastify/cookie is registered */ signCookie?: (value: string) => string; /** Read a response header value (if available) */ getHeader: (name: string) => string | number | string[] | undefined; /** Read all response headers as a plain object */ getHeaders: () => Record; /** Remove a response header by name */ removeHeader: (name: string) => void; /** Check if a response header has been set */ hasHeader: (name: string) => boolean; /** Whether the reply has already been sent */ sent: boolean; /** * Access to the underlying response stream (for connection monitoring) * * Limited scope: Only used internally by processFileUpload() for detecting * broken connections during file uploads. Most handlers won't need this */ raw: { /** Whether the underlying connection has been destroyed */ destroyed: boolean; }; /** * Internal: send an error envelope and finish the response immediately. * * This is installed by the framework's controlled-reply wrapper so helpers * can terminate with the same raw/hijacked path while still reusing shared * header logic such as CORS application. * * ControlledReply intentionally does not expose general-purpose send/write * methods to user handlers. This internal escape hatch exists only so * framework-owned helpers like APIResponseHelpers can terminate early in a * controlled way without reopening arbitrary reply access. */ _sendErrorEnvelope: (statusCode: number, errorEnvelope: APIErrorResponse | PageErrorResponse) => Promise; } /** * Safe route options that prevent catch-all conflicts */ interface SafeRouteOptions { method: string | string[]; url: string; handler: RouteHandler; preHandler?: preHandlerHookHandler | preHandlerHookHandler[]; schema?: FastifySchema; config?: unknown; constraints?: { /** Only allow specific hosts, no wildcards that could conflict with SSR */ host?: string; /** Only allow specific versions */ version?: string; }; } type RouteHandler = (request: FastifyRequest, reply: FastifyReply) => void | Promise; /** * WebSocket server configuration options */ interface WebSocketOptions { /** * Enable/disable permessage-deflate compression * @default false */ perMessageDeflate?: boolean; /** * The maximum allowed message size in bytes * @default 100 * 1024 * 1024 (100MB) */ maxPayload?: number; /** * Custom handler called when the WebSocket server is closing * Provides access to all connected clients for graceful shutdown * @param clients Set of all connected WebSocket clients * @returns Promise that resolves when cleanup is complete */ preClose?: (clients: Set) => Promise; } /** * HTTPS server configuration options * Provides first-class HTTPS support with certificate files and SNI callback */ interface HTTPSOptions { /** * Private key in PEM format * Can be a string, Buffer, or array of strings/Buffers for multiple keys */ key: string | Buffer | Array; /** * Certificate chain in PEM format * Can be a string, Buffer, or array of strings/Buffers for multiple certificates */ cert: string | Buffer | Array; /** * Optional CA certificates in PEM format * Used for client certificate verification */ ca?: string | Buffer | Array; /** * Optional passphrase for the private key */ passphrase?: string; /** * Optional SNI (Server Name Indication) callback for dynamic certificate selection * Useful for multi-tenant SaaS applications serving multiple domains * * The callback receives the server name (domain) and should return a SecureContext * with the appropriate certificate for that domain. Can be async. * * @param servername - The domain name from the TLS handshake * @returns SecureContext with the appropriate certificate, or a Promise resolving to one * * @example * ```ts * sni: async (servername) => { * const ctx = tls.createSecureContext({ * key: await loadKeyForDomain(servername), * cert: await loadCertForDomain(servername), * }); * * return ctx; * } * ``` */ sni?: (servername: string) => SecureContext | Promise; } /** * Shared configuration for versioned API endpoint groups * Used by helpers that register versioned endpoints (page data, generic API routes, etc.) */ interface APIEndpointConfig { /** * Endpoint prefix that comes before version/endpoint (default: "/api") * Set to `false` to disable API handling (server becomes a plain web server) */ apiEndpointPrefix?: string | false; /** Whether to enable versioning (default: true) */ versioned?: boolean; /** Base endpoint name for page data loader handlers (default: "page_data"). Used by SSR/APIServer's page-data registration only. */ pageDataEndpoint?: string; } /** * Plugin options passed to each plugin * * Environment information available at plugin registration time. * * Notes: * - Use these fields inside your plugin setup to decide what to REGISTER * (e.g., which routes, which hooks). This is registration-time context. * - For per-request branching inside handlers/middleware, read * `request.isDevelopment` (decorated by the servers). Both reflect the same * underlying mode; they serve different scopes. */ interface PluginOptions { /** Type of server the plugin is running on */ serverType: 'ssr' | 'api'; /** Server mode (development or production) */ mode: 'development' | 'production'; /** Whether running in development mode */ isDevelopment: boolean; /** API endpoints configuration from the server */ apiEndpoints?: APIEndpointConfig; } /** * Log levels supported by the Unirend logger adapter. */ type UnirendLoggerLevel = 'trace' | 'debug' | 'info' | 'warn' | 'error' | 'fatal'; /** * Logger function signature used by Unirend logger object methods. */ type UnirendLoggerFunction = (message: string, context?: Record) => void; /** * Framework-level logger object that Unirend adapts to Fastify's logger interface. */ interface UnirendLoggerObject { trace: UnirendLoggerFunction; debug: UnirendLoggerFunction; info: UnirendLoggerFunction; warn: UnirendLoggerFunction; error: UnirendLoggerFunction; fatal: UnirendLoggerFunction; } /** * High-level logging options that Unirend can adapt to Fastify. */ interface UnirendLoggingOptions { /** * Logger object used by Unirend and adapted to Fastify under the hood. */ logger: UnirendLoggerObject; /** * Initial minimum level used by the adapter. * @default "info" */ level?: UnirendLoggerLevel; } /** * Subset of Fastify server options safe for use with Unirend servers. * Excludes options that would conflict with server setup. * * These options are supported by SSRServer, APIServer, and StaticWebServer. */ interface FastifyServerOptions { /** * Enable/configure Fastify logging * @example true | false | { level: 'info' } | { level: 'warn', prettyPrint: true } */ logger?: boolean | FastifyLoggerOptions; /** * Custom Fastify logger instance (e.g. pino-compatible logger). * When provided, this is passed to Fastify as `loggerInstance`. */ loggerInstance?: FastifyBaseLogger; /** * Trust proxy headers (useful for deployment behind load balancers) * Passed directly to Fastify. Supports `true`, IP/CIDR strings like * `'127.0.0.1'` or `'127.0.0.1,192.168.1.1/24'`, IP/CIDR lists like * `['127.0.0.1', '10.0.0.0/8']`, hop counts, or a custom trust function * `(address, hop) => boolean`. * @default false */ trustProxy?: boolean | string | string[] | number | FastifyTrustProxyFunction; /** * Maximum request body size in bytes for non-multipart requests (JSON, text, * URL-encoded forms). Does NOT apply to multipart file uploads — those are * controlled by `fileUploads.limits.fileSize` (the multipart plugin registers * its own streaming content-type parser, bypassing this limit). * @default 1048576 (1MB) */ bodyLimit?: number; /** * Keep-alive timeout in milliseconds * @default 72000 (72 seconds) */ keepAliveTimeout?: number; /** * Request idle timeout in milliseconds. The timer resets on every data chunk * received, so large file uploads are not affected as long as data keeps * flowing. A stalled or hung request (no new data) is closed with 408 once * the timeout elapses. Set to `0` to disable. When running without a reverse * proxy, `30000` (30 s) is a reasonable starting point. * @default 0 (disabled) */ requestTimeout?: number; /** * TCP connection timeout in milliseconds. Closes connections that have been * idle (no data in either direction) for longer than this value. Protects * against clients that open a socket but never send an HTTP request. Set to * `0` to disable. * * When running without a reverse proxy, `10000` (10 s) is a reasonable * starting point. **WebSocket caveat:** this timeout applies to the * underlying socket, so idle WebSocket connections (e.g. a notification * channel waiting for events) will be closed if no frames are exchanged * within the timeout window. Only set this when WebSockets are disabled, or * ensure your clients send regular ping/pong heartbeats to keep the socket * active. * @default 0 (disabled) */ connectionTimeout?: number; } /** * Log level config for access logging. * Can be a single level applied to all requests, or per-status-range. */ type AccessLogLevelConfig = UnirendLoggerLevel | { /** Level for 2xx–3xx responses. @default 'info' */ success?: UnirendLoggerLevel; /** Level for 4xx responses. @default 'warn' */ clientError?: UnirendLoggerLevel; /** Level for 5xx responses. @default 'error' */ serverError?: UnirendLoggerLevel; }; /** * Context passed to onRequest access log hook (request start). */ interface AccessLogRequestContext { /** Stable source identifier for access log entries and hooks. */ logSource: 'unirend.accessLog'; reqID: string | number; method: string; url: string; ip: string; userAgent: string | undefined; /** Server label string (e.g. `'SSR'`, `'API'`). Set via the `serverLabel` server option. */ serverLabel: string; /** Whether the request is being handled as a static asset response. */ isStaticAsset: boolean; /** Raw Fastify request — same access pattern as data loaders. */ request: FastifyRequest; } /** * Read-only snapshot of reply state at response time. * Extracted to prevent accidental mutation of the response. */ interface AccessLogReplyInfo { statusCode: number; headers: Record; } /** * Context passed to onResponse access log hook (request finish or abort). */ interface AccessLogResponseContext extends AccessLogRequestContext { statusCode: number; /** Elapsed time in milliseconds. */ responseTime: number; /** Whether the request completed normally or the client disconnected early. */ finishType: 'completed' | 'aborted'; /** Read-only reply snapshot. */ replyInfo: AccessLogReplyInfo; } /** * First-party access logging configuration. * Applies to all server types (SSRServer, APIServer, StaticWebServer, RedirectServer). * * Fastify's built-in request lifecycle logs are always suppressed internally, * this config controls what Unirend logs instead. */ interface AccessLogConfig { /** * Which lifecycle events to log. * @default 'finish' */ events?: 'start' | 'finish' | 'both' | 'none'; /** * Template for finish/response log lines. Supports {{variable}} placeholders. * Available variables: logSource, method, url, statusCode, responseTime, finishType, reqID, ip, userAgent, serverLabel, isStaticAsset * @default 'Request finished {{method}} {{url}} {{statusCode}} ({{responseTime}}ms)' */ responseTemplate?: string; /** * Template for start/request log lines. Supports {{variable}} placeholders. * Available variables: logSource, method, url, reqID, ip, userAgent, serverLabel, isStaticAsset * @default 'Request started {{method}} {{url}}' */ requestTemplate?: string; /** * Log level for printed lines. Defaults to status-code-based: * info for 2xx/3xx, warn for 4xx, error for 5xx. */ level?: AccessLogLevelConfig; /** * Custom hook fired at request start when provided. * Awaited before request handling continues. * Useful for writing initial access log records (DB insert, etc.). */ onRequest?: (context: AccessLogRequestContext) => void | Promise; /** * Custom hook fired when a request finishes when provided * (both normal completion and client aborts). * Awaited after the response finishes or aborts. * Use context.finishType to distinguish 'completed' from 'aborted'. */ onResponse?: (context: AccessLogResponseContext) => void | Promise; } interface ResponseTimeHeaderOptions { /** * Whether to emit the response-time header. * @default true */ enabled?: boolean; /** * Header name to emit. * @default 'X-Response-Time' */ headerName?: string; /** * Number of fractional digits to include in the emitted time. * @default 2 */ digits?: number; } /** * Base options for SSR * @template M Custom meta type extending BaseMeta for error/notFound handlers */ interface ServeSSROptions { /** * Response compression for non-streaming SSR HTML and API responses. * Negotiates `Accept-Encoding` and skips range or already-encoded replies. * * Static files served through `staticContentRouter` use the same shape but * handle compression in the static file layer so ETags and range requests * remain correct. * * @default true */ responseCompression?: boolean | ResponseCompressionOptions; /** * Optional response-time header emitted on completed responses. * Normal Fastify-managed replies apply and measure this in `onSend`. * * For hijacked/raw replies, it is applied when `reply.hijack()` is called so * `reply.getHeaders()` includes it before a * subsequent raw `writeHead(...)`. Access logging measures when the response * finishes, so raw/hijacked responses can report different timings there. * * @default false */ responseTimeHeader?: boolean | ResponseTimeHeaderOptions; /** * ID of the container element (defaults to "root") * This element will be formatted inline to prevent hydration issues */ containerID?: string; /** * Optional safe-to-share app configuration object. * Cloned and frozen per request. On SSR, exposed to React via * usePublicAppConfig() and injected as window.__PUBLIC_APP_CONFIG__. * * Keep this minimal and non-sensitive; it can be passed to the client. */ publicAppConfig?: Record; /** * Cookie forwarding controls for SSR * * Controls which cookies are forwarded: * - from client request → SSR loaders (via the Fetch `Cookie` header) * - from backend/server → client (via `Set-Cookie` headers) * * Behavior: * - If both arrays are empty or undefined, all cookies are allowed * - If `allowCookieNames` is non-empty, only cookies with those names are allowed * - `blockCookieNames` is always applied and will block those cookies even if in allow list */ cookieForwarding?: { /** * Cookie names that are allowed to be forwarded. * If provided and non-empty, only these cookie names will be forwarded. */ allowCookieNames?: string[]; /** * Cookie names that must never be forwarded (takes precedence over allow list). * * You can also set this to `true` to block ALL cookies from being forwarded. * When `true`, no cookies will be forwarded regardless of `allowCookieNames`. */ blockCookieNames?: string[] | true; }; /** * Array of plugins to register with the server * Plugins get access to a controlled Fastify instance */ plugins?: ServerPlugin[]; /** * Override the helpers used to construct API/Page envelopes. * Provide your own class (subclassing `APIResponseHelpers` recommended) to * inject default metadata or behavior. If not provided, the default * `APIResponseHelpers` will be used. */ APIResponseHelpersClass?: APIResponseHelpersClass; /** * Configuration for versioned API endpoints (shared by page data and generic API routes) * For page data loader handler endpoints, set pageDataEndpoint (default: "page_data") */ apiEndpoints?: APIEndpointConfig; /** * File upload configuration * When enabled, multipart file upload support will be available * Allows use of processFileUpload() in your plugins */ fileUploads?: FileUploadsConfig; /** * Name of the client folder within buildDir * Defaults to "client" if not provided */ clientFolderName?: string; /** * Name of the server folder within buildDir * Defaults to "server" if not provided */ serverFolderName?: string; /** * Custom 500 error page handler * Called when SSR rendering fails with an error * @param request The Fastify request object * @param error The error that occurred * @param isDevelopment Whether running in development mode * @returns HTML string for the error page */ get500ErrorPage?: (request: FastifyRequest, error: Error, isDevelopment: boolean) => string | Promise; /** * Custom error/not-found handlers for mixed SSR+API servers * These handlers return JSON envelope responses instead of HTML error pages * for requests matching the apiEndpoints.apiEndpointPrefix */ APIHandling?: { /** * Custom error handler for API routes * Called when an unhandled error occurs in API routes * * REQUIRED: Must return a proper API or Page envelope response according to api-envelope-structure.md * - For API requests (isPageData=false): Return APIErrorResponse envelope * - For Page requests (isPageData=true): Return PageErrorResponse envelope * * Params: (request, error, isDevelopment, isPageData) * - request: The Fastify request object * - error: The error that occurred * - isDevelopment: Whether running in development mode * - isPageData: Whether this is a page-data request (e.g., /api/v1/page_data/home) * * Required envelope return fields: * - status: "error" * - status_code: HTTP status code (400, 401, 404, 500, etc.) * - request_id: Unique request identifier * - type: "api" for API requests, "page" for page data requests * - data: null (always null for error responses) * - meta: Object containing metadata (page metadata required for page type) * - error: Object with { code, message, details? } */ errorHandler?: APIErrorHandlerFn; /** * Custom handler for API requests that did not match any route (404) * If provided, overrides the built-in envelope handler for API routes * * REQUIRED: Must return a proper API or Page envelope response according to api-envelope-structure.md * - For API requests (isPageData=false): Return APIErrorResponse envelope with status_code: 404 * - For Page requests (isPageData=true): Return PageErrorResponse envelope with status_code: 404 * * Params: (request, isPageData) * - request: The Fastify request object * - isPageData: Whether this is a page-data request (e.g., /api/v1/page_data/home) * * Required envelope return fields: * - status: "error" * - status_code: 404 * - request_id: Unique request identifier * - type: "api" for API requests, "page" for page data requests * - data: null (always null for error responses) * - meta: Object containing metadata (page metadata required for page type) * - error: Object with { code: "not_found", message, details? } */ notFoundHandler?: APINotFoundHandlerFn; }; /** * Custom handler for web requests that arrive while the server is shutting down. * * Function form handles web requests. Object form can split API and web * behavior for mixed SSR + API servers. Missing handlers fall back to * Unirend's default 503 response. */ closingHandler?: WebClosingHandlerFn | SplitClosingHandler; /** * Enable WebSocket support on the server * @default false */ enableWebSockets?: boolean; /** * WebSocket server configuration options * Only used when enableWebSockets is true */ webSocketOptions?: WebSocketOptions; /** * HTTPS server configuration * Provides first-class HTTPS support with key, cert, and SNI callback * * @example Basic HTTPS * ```ts * https: { * key: privateKey, // string | Buffer * cert: certificate, // string | Buffer * } * ``` * * @example SNI callback for multi-tenant SaaS * ```ts * https: { * key: defaultPrivateKey, // string | Buffer - Default cert * cert: defaultCertificate, // string | Buffer * sni: async (servername) => { * // Load certificate based on domain * const { key, cert } = await loadCertForDomain(servername); * * // Return a secure context for the domain * return tls.createSecureContext({ key, cert }); * }, * } * ``` */ https?: HTTPSOptions; /** * Curated Fastify options for SSR server configuration * Only exposes safe options that won't conflict with SSR setup */ fastifyOptions?: FastifyServerOptions; /** * Framework-level logging options adapted to Fastify under the hood. * * Note: Cannot be used together with `fastifyOptions.logger` or * `fastifyOptions.loggerInstance`. */ logging?: UnirendLoggingOptions; /** * First-party access logging configuration * Controls request/response logging without needing a custom plugin */ accessLog?: AccessLogConfig; /** * Custom client IP resolver. * When set, called once per request to populate `request.clientIP` — available * throughout the entire request lifecycle (plugins, hooks, page data loader * handlers, API route handlers, access log templates/hooks, etc.). * When not set, `request.clientIP` falls back to `request.ip` * (which reflects Fastify proxy handling when `fastifyOptions.trustProxy` * is configured). * * Use this when behind Cloudflare, AWS ALB, or other CDNs that carry the * real client IP in a custom header. */ getClientIP?: (request: FastifyRequest) => string | Promise; /** * Whether to automatically log errors via the server logger * When enabled, all errors are logged before custom error handlers run * Useful for debugging custom error pages that can't show stack traces * @default true */ logErrors?: boolean; /** * Label for this server instance, used in error log messages and access log templates. * Useful for distinguishing log output when running multiple server instances. * @default 'SSR' * @example 'SSR:marketing' */ serverLabel?: string; /** * Timeout in milliseconds for the SSR render fetch request. * If the render takes longer than this, the request is aborted. * @default 5000 (5 seconds) */ ssrRenderTimeout?: number; } interface ServeSSRDevOptions extends ServeSSROptions { } interface ServeSSRProdOptions extends ServeSSROptions { /** * Name of the server entry file to look for in the Vite manifest * Defaults to "EntrySSR" if not provided */ serverEntry?: string; /** * Path to the HTML template file relative to buildDir * Defaults to "client/index.html" if not provided * * @example * // Default behavior - uses buildDir/client/index.html * serveSSRProd('./build') * * @example * // Custom template location * serveSSRProd('./build', { template: 'dist/app.html' }) */ template?: string; /** * CDN base URL for rewriting asset URLs in HTML at runtime * If provided, rewrites