Deployment
A production deployment guide for applications using the VerifyKit SDK.
Production Checklist
Before deploying to production, verify the following:
- WASM loads successfully (no CSP or bundler errors)
- PDF.js worker is configured and loading
- CMap and standard font files are accessible (if using non-Latin PDFs)
- Revocation proxy endpoint has correct CORS headers (if using
@trexolab/verifykit-plugin-revocation) - Bundle size is acceptable for your performance targets
- Trust store is configured with any custom root CAs your PDFs require
Content Security Policy (CSP)
The VerifyKit core engine runs WebAssembly. If your application uses a Content Security Policy, you must allow WASM execution in the script-src directive.
Recommended CSP
Content-Security-Policy: script-src 'self' 'wasm-unsafe-eval';
The wasm-unsafe-eval directive allows WebAssembly compilation and instantiation without allowing arbitrary JavaScript eval(). This is the recommended approach and is supported in all modern browsers.
Alternative (older browsers)
If you need to support older browsers that do not recognize wasm-unsafe-eval, use:
Content-Security-Policy: script-src 'self' 'unsafe-eval';
Warning: unsafe-eval also permits JavaScript eval(), which is less secure. Use wasm-unsafe-eval when possible.
Common CSP Errors
If WASM fails to load, you will see errors like:
Refused to compile or instantiate WebAssembly module because 'unsafe-eval'
is not an allowed source of script in the following Content Security Policy...
or:
CompileError: WebAssembly.instantiate(): Wasm code generation disallowed by embedder
Both indicate that your CSP needs the wasm-unsafe-eval (or unsafe-eval) directive.
PDF.js Worker
The VerifyKit viewer uses PDF.js to render PDF pages, offloading page rendering to a Web Worker.
workerUrl is required
The pdf.js library is bundled into @trexolab/verifykit-react, but the worker is not — it must be fetched at runtime from a URL you supply. workerUrl is therefore a required config option on both the React and Vanilla packages; the provider throws if it is missing. There is no automatic default.
The worker must match the bundled pdf.js version (5.5.207) exactly:
<VerifyKitProvider config={{
workerUrl: 'https://unpkg.com/pdfjs-dist@5.5.207/legacy/build/pdf.worker.min.mjs',
}}>Self-hosting the worker
If your CSP blocks external scripts, or the deployment cannot reach a CDN at
runtime, put the worker in your own static assets. pdf.js is bundled inside the
SDK, so there is no node_modules/pdfjs-dist to copy from — download the pinned
version (pdfjs-dist@5.5.207, legacy build) as a build step:
curl -o static/pdf.worker.min.mjs \
https://unpkg.com/pdfjs-dist@5.5.207/legacy/build/pdf.worker.min.mjs# or from the npm registry instead of a CDN
npm pack pdfjs-dist@5.5.207 --pack-destination /tmp \
&& tar -xzOf /tmp/pdfjs-dist-5.5.207.tgz package/legacy/build/pdf.worker.min.mjs \
> static/pdf.worker.min.mjsimport { VerifyKitProvider } from '@trexolab/verifykit-react'
<VerifyKitProvider config={{ workerUrl: '/static/pdf.worker.min.mjs' }}>
{/* ... */}
</VerifyKitProvider>The version has to match the bundled library exactly, so re-run the download whenever you upgrade VerifyKit — a mismatch is a hard failure, and the console names both versions when it happens. See Installation → PDF.js Worker Setup for the full explanation.
CMap and Standard Font Files
PDF documents using CJK (Chinese, Japanese, Korean) fonts or certain legacy encodings require CMap files for correct text rendering. PDFs may also reference the 14 standard PDF fonts.
Default Behavior
The viewer loads CMap and standard font files from the Mozilla CDN by default. This works for most deployments without configuration.
Self-Hosting
If your application cannot reach external CDNs (e.g., air-gapped environments, strict CSP):
- Copy the CMap files from
node_modules/@trexolab/verifykit-react/cmaps/to a public directory (e.g.,/static/cmaps/). - Copy the standard fonts from
node_modules/@trexolab/verifykit-react/standard_fonts/to a public directory (e.g.,/static/standard_fonts/). - Download the worker into the same tree, as in Self-hosting the worker above. It ships in neither the SDK package nor your
node_modules, so this step is not optional for an air-gapped deployment — and the file must bepdfjs-dist@5.5.207, legacy build. - Configure the viewer — every one of these paths is now your own origin:
<VerifyKitProvider config={{
workerUrl: '/static/pdf.worker.min.mjs',
cMapUrl: '/static/cmaps/',
standardFontDataUrl: '/static/standard_fonts/',
}}>
{/* ... */}
</VerifyKitProvider>Bundle Size
The verification engine is about 1.1 MB and is the largest thing
@trexolab/verifykit-core ships — but it is a .wasm file rather than part of
the JavaScript, so it is not in your JS bundle at all. Your bundler emits it as a
hashed asset, the browser compiles it while it downloads, and it is cached apart
from the JS, which changes far more often. What follows is about the JavaScript.
Tree Shaking
All VerifyKit packages support tree shaking. If you only use verifyPdf(), unused exports like extractPdfMetadata() and utility functions will be eliminated by your bundler.
Code Splitting
For client-side applications, dynamically import VerifyKit so the WASM binary is not included in the initial page load:
// Load VerifyKit only when needed
const { verifyPdf } = await import('@trexolab/verifykit-core')
const result = await verifyPdf(buffer)The React viewer already code-splits pdf.js for you: it is loaded lazily, inside the effects that use it, so it lands in its own chunk rather than your initial bundle — and never runs on the server.
Next.js
Since 0.6.0, nothing special is required. Import the viewer directly, in a Server Component or a Client Component:
// app/page.tsx — a Server Component. No 'use client', no next/dynamic.
import { VerifyKitProvider, Viewer } from '@trexolab/verifykit-react'
import '@trexolab/verifykit-react/styles.css'
export default function Page() {
return (
<VerifyKitProvider config={{ workerUrl: '/pdf.worker.min.mjs' }}>
<Viewer />
</VerifyKitProvider>
)
}The package ships a 'use client' directive, so it is its own client boundary —
you do not need a wrapper file. It server-renders and hydrates cleanly under both
Turbopack and webpack, in dev and in production builds.
Add 'use client' to your file only when that file itself uses hooks — e.g.
when you call useVerification() to drive loading and read results:
'use client'
import { VerifyKitProvider, Viewer, useVerification, defaultLayoutPlugin } from '@trexolab/verifykit-react'Before 0.6.0 the viewer had to be wrapped in
dynamic(() => import('./Viewer'), { ssr: false }), because importing it evaluated pdf.js at module scope and threwDOMMatrix is not definedunder Node. That is fixed — butssr: falsestill works if you have it in place, so upgrading does not force a change.
Serving the WASM from somewhere else
The engine is loaded from wherever your bundler emitted it, which is normally
what you want. setWasmUrl() overrides that — for serving it from a CDN, from an
origin your CSP already allows, or from a path fixed by an air-gapped
deployment:
import { setWasmUrl } from '@trexolab/verifykit-core'
// Call before createVerifier() / verifyPdf() — the engine reads this once.
setWasmUrl('https://cdn.example.com/verifykit/verifykit_core_wasm_bg.wasm')The file to copy is verifykit_core_wasm_bg.wasm, from pkg/ inside
@trexolab/verifykit-core. Serve it as application/wasm: browsers refuse to
stream-compile any other content type and fall back to a slower path, with a
console warning naming the type they got.
createVerifier({ wasmUrl }) does the same thing for a single verifier, and the
vanilla build takes wasmUrl in VerifyKit.create().
CORS for Revocation Proxy
If you use @trexolab/verifykit-plugin-revocation, the plugin makes POST requests from the browser to your revocation proxy endpoint. The proxy must return appropriate CORS headers.
Required CORS Headers
Access-Control-Allow-Origin: <your-app-origin>
Access-Control-Allow-Methods: POST, OPTIONS
Access-Control-Allow-Headers: Content-Type
Next.js API Route
The handleRevocation() handler from @trexolab/verifykit-plugin-revocation/handler works automatically when mounted on the same origin as your frontend (the recommended setup). If you need cross-origin access, add CORS headers manually in your server framework:
// app/api/revocation/route.ts
import { handleRevocation } from '@trexolab/verifykit-plugin-revocation/handler'
const handler = handleRevocation()
export async function POST(req: Request) {
const res = await handler(req)
res.headers.set('Access-Control-Allow-Origin', 'https://myapp.example.com')
return res
}SSRF Protection
The revocation proxy fetches external URLs (CRL distribution points and OCSP responder URLs) embedded in certificates. To prevent Server-Side Request Forgery (SSRF) attacks, the handler includes a urlFilter option:
export const POST = handleRevocation({
urlFilter: (url) => {
const parsed = new URL(url)
// Only allow HTTP/HTTPS to public internet
if (!['http:', 'https:'].includes(parsed.protocol)) return false
// Block internal networks
if (parsed.hostname === 'localhost') return false
if (parsed.hostname.startsWith('127.')) return false
if (parsed.hostname.startsWith('10.')) return false
if (parsed.hostname.startsWith('192.168.')) return false
return true
},
})The default urlFilter allows URLs whose hostname starts with crl. or ocsp., contains .crl. or .ocsp., or whose path ends with .crl. Only HTTP and HTTPS protocols are permitted. Override it if your CA endpoints do not match these patterns.
CDN Deployment (Vanilla JS)
The @trexolab/verifykit-vanilla package produces a UMD bundle that can be loaded directly from a CDN or self-hosted:
CDN Usage
<script src="https://verifykit.trexolab.com/cdn/verifykit.umd.js"></script>
<link rel="stylesheet" href="https://verifykit.trexolab.com/cdn/verifykit.css" />
<div id="viewer" style="height: 100vh"></div>
<script>
VerifyKit.create(document.getElementById('viewer'), {
workerUrl: 'https://unpkg.com/pdfjs-dist@5.5.207/legacy/build/pdf.worker.min.mjs',
})
</script>Self-Hosting
To self-host the UMD bundle:
-
Copy the built files from
packages/vanilla/dist/:verifykit.umd.js— the main UMD bundleverifykit.css— the viewer styles
-
Serve them from your static file server or CDN.
-
Reference them in your HTML:
<script src="/static/verifykit.umd.js"></script>
<link rel="stylesheet" href="/static/verifykit.css" />Subresource Integrity (SRI)
When loading from a CDN, use SRI hashes to ensure the file has not been tampered with:
<script
src="https://cdn.example.com/verifykit.umd.js"
integrity="sha384-<hash>"
crossorigin="anonymous"
></script>Generate the hash with:
cat verifykit.umd.js | openssl dgst -sha384 -binary | openssl base64 -AAIA Network Implications
AIA (Authority Information Access) certificate chain resolution is enabled by default (enableAIA: true). When enabled, the core engine may make HTTP requests to CA-hosted URLs to fetch missing intermediate certificates. In production:
- Ensure outbound HTTP access is available if AIA is enabled.
- For air-gapped or restricted network environments, disable AIA:
enableAIA: false. - AIA requests are cached in memory for the lifetime of the verifier instance.
Environment-Specific Notes
Next.js
- No
ssr: falseand nonext/dynamicneeded as of v0.6.0 — see Next.js above. Import the viewer directly, including from a Server Component. Verified under Turbopack and webpack, dev and production builds. - No
config.resolve.alias.canvas = falseworkaround is needed: VerifyKit renders via PDF.js's browser canvas and never imports the Nodecanvaspackage. - The revocation proxy API route works with both Pages Router (
pages/api/revocation.ts) and App Router (app/api/revocation/route.ts). - If using the Edge runtime, note that WASM support varies. The default Node.js runtime is recommended.
Vite
- No special configuration is needed. Vite resolves the
.wasmfrom the reference the engine makes and emits it as an asset, withoutvite-plugin-wasm. - For production builds, Vite will tree-shake unused exports automatically.
Webpack
- No special configuration is needed as of v0.3.1+. Remove any
asyncWebAssemblyexperiment configuration from older setups.
Node.js (Server-Side)
- Requires Node.js >= 20.19.0.
- WASM loads from the filesystem automatically. No HTTP server is needed for the WASM binary.
- For large PDFs, increase memory with
--max-old-space-size=4096.