@trupdf/converter
The conversion microservice. Server-side only (Node) — the browser viewer never bundles it; it talks to the service over HTTP.
From the viewer
The viewer uploads non-PDF sources automatically when you point it at your deployed converter:
<DocViewer
source={file}
converterUrl={process.env.NEXT_PUBLIC_TRUPDF_CONVERTER_URL!}
converterApiKey={process.env.NEXT_PUBLIC_TRUPDF_CONVERTER_KEY!} // sent as X-TruPDF-API-Key
/>HTTP API
| Method | Route | Purpose |
|---|---|---|
GET | /health | { ok, service, queue: 'enabled' | 'disabled' } |
POST | /convert | Multipart file (+ optional password field or X-Document-Password header). Synchronous conversion. |
POST | /convert/async | Same body; returns 202 { jobId }. 503 when the job queue is disabled. |
GET | /convert/jobs/:id | { id, status, progress, result, error, attemptsMade } |
DELETE | /convert/jobs/:id | Cancel / remove a job (204) |
POST | /docgen | Multipart template (HTML/Markdown/DOCX/…) + data (JSON) — server-side templating, returns a PDF |
POST | /bookmarks/read | Extract the outline tree from an uploaded PDF |
POST | /bookmarks/write | Write an outline tree back into an uploaded PDF |
POST | /fetch-url | { url } — SSRF-guarded proxy fetch; original filename returned via X-TruPDF-Original-Name |
POST /convert responds with
{ url, hash, strategy, fromCache, durationMs, bytes }.
HTTP error codes
| Status | Meaning |
|---|---|
| 400 | INVALID_INPUT / UNSUPPORTED_FORMAT |
| 401 | PASSWORD_REQUIRED (encrypted source, no/wrong password) |
| 413 | FILE_TOO_LARGE |
| 502 | Upstream conversion failure |
| 503 | Typed code when the conversion engine or the job queue is unavailable |
| 504 | TIMEOUT |
| 500 | INTERNAL |
Strategies
Every format routes through the first strategy whose canHandle
claims it. All strategies ship registered in the server app:
| Strategy | Handles |
|---|---|
PassthroughStrategy | PDF in → PDF out (validation + cache only) |
OfficeStrategy | ~50 office extensions — doc/docx/docm/dot*, odt/rtf/pages, xls/xlsx/xlsm/xlsb/ods/csv/tsv/numbers, ppt/pptx/odp/key, vsd*/pub, epub/eps — via the office pipeline |
HTMLStrategy | html, htm, xhtml — via the HTML render pipeline |
TextStrategy | txt, markdown (md/mdx/…), json, xml, yaml, source code (js/ts/py/java/go/rust/…), sql |
ImageStrategy | jpg/jpeg/png/apng/gif/bmp/svg/webp/avif/ico and other browser-native images |
RasterImageStrategy | TIFF and multi-page rasters |
MagickImageStrategy | Extended raster pipeline — HEIC, camera raw, and other exotic raster formats |
EmailStrategy | eml, msg (with attachments listed) |
XpsStrategy | xps, oxps |
XodStrategy | xod — legacy WebViewer XOD archives |
TruCadStrategy | dxf, dwg (R12–R2018, incl. CTB plot styles), dwf, dwfx, dgn — via the TruCAD engine (TruenoTech's own clean-room CAD engine, deployed as a sidecar service; env TRUCAD_SIDECAR_URL) |
RvtStrategy | rvt, rfa — typed rejection: Revit files are 3D BIM, not 2D CAD; the error tells the caller to export to DWG/DXF/PDF first |
Each strategy implements:
interface ConversionStrategy {
readonly id: string;
canHandle(mimeType: string, filename: string): boolean;
convert(filename: string, mimeType: string, data: Buffer, options?: ConversionOptions): Promise<Buffer>;
cacheVariant?(mimeType: string, filename: string): string;
}Add a new format by implementing the interface and registering the
strategy with ConversionService:
import {
ConversionService,
PassthroughStrategy,
OfficeStrategy,
HTMLStrategy,
TextStrategy,
ImageStrategy,
RasterImageStrategy,
TruCadStrategy,
} from '@trupdf/converter';ConversionService routes a request to the first matching strategy,
consults the result cache (SHA-256 of input bytes + strategy variant),
and returns { inputHash, pdf, strategy, fromCache, durationMs }.
Storage adapters
import {
LocalStorageAdapter,
S3StorageAdapter, // AWS S3 + MinIO (path-style)
AzureBlobStorageAdapter,
WebhookStorageAdapter, // custom HTTP sink with HMAC signing
createStorageAdapter, // env-driven factory
} from '@trupdf/converter';All implement:
interface StorageAdapter {
put(key: string, bytes: Uint8Array, meta?: Metadata): Promise<{ url: string }>;
get(key: string): Promise<Uint8Array | null>;
exists(key: string): Promise<boolean>;
remove(key: string): Promise<void>;
}Cache & queue
import {
RedisConversionCache,
MemoryConversionCache,
ConversionQueue,
createConversionWorker,
} from '@trupdf/converter';createConversionWorker registers job-queue consumers and wires them
to the strategy router. MemoryConversionCache is for tests; the
persistent result cache dedupes identical uploads across requests.
Server app
import { buildApp, buildRouter } from '@trupdf/converter';
const app = buildApp({ logger, cors, license });
app.use(buildRouter({ queue, storage, license }));
app.listen(3002);Environment
Internal service wiring (engine, queue, cache) is preconfigured by the
bundled docker-compose.yml; you only set the variables below.
| Variable | Purpose |
|---|---|
TRUPDF_LICENSE | License token — the server refuses to start without one (TRUPDF_LICENSE_DEV_MODE=1 for dev only) |
TRUPDF_CONVERTER_API_KEY | Required in production. The viewer sends it via the converterApiKey prop (X-TruPDF-API-Key) |
CORS_ORIGINS | Required — comma-separated allowed origins; no default |
STORAGE_ADAPTER | local | s3 | azure | webhook |
LOCAL_STORAGE_DIR | Output directory for the local adapter |
LOCAL_STORAGE_PUBLIC_URL | Required with local — public URL prefix for stored results |
S3_* / AWS_* | S3 adapter credentials + bucket (MinIO path-style supported) |
AZURE_* | Azure Blob adapter connection + container |
WEBHOOK_* | Webhook adapter endpoint + HMAC secret |
TRUCAD_SIDECAR_URL | TruCAD engine sidecar URL (2D CAD formats) |
PORT | Default 3002 |
CONVERSION_TIMEOUT_MS | Default 1800000 |
MAX_UPLOAD_BYTES | Default 0 (unlimited) |
CACHE_TTL_SECONDS | Default 604800 |
RETENTION_SWEEP_INTERVAL_MS | Stored-result cleanup sweep interval |
RETENTION_MAX_AGE_MS | Stored-result max age |
LOG_LEVEL | Logger level |
Types
interface ConversionRequest {
filename: string;
mimeType: string;
data: Buffer;
password?: string; // opens encrypted sources
}
interface ConversionResult {
inputHash: string; // SHA-256 of the input — the cache key
pdf: Buffer;
strategy: string;
fromCache: boolean;
durationMs: number;
}
class ConversionError extends Error {
code: // stable wire codes — switch on these, don't parse messages
| 'UNSUPPORTED_FORMAT'
| 'GOTENBERG_UNREACHABLE' // conversion engine unreachable
| 'GOTENBERG_REJECTED' // conversion engine rejected the input
| 'PASSWORD_REQUIRED'
| 'INVALID_INPUT'
| 'TIMEOUT'
| 'CAD_CONVERTER_UNAVAILABLE'
| 'CAD_CONVERTER_FAILED'
| 'INTERNAL';
}License gate
The server boot reads TRUPDF_LICENSE (or TRUPDF_LICENSE_DEV_MODE=1
for synthetic enterprise) and refuses to start without one. The
strategy router calls gate.require('convert') per request.