TPTruPDF

@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

MethodRoutePurpose
GET/health{ ok, service, queue: 'enabled' | 'disabled' }
POST/convertMultipart file (+ optional password field or X-Document-Password header). Synchronous conversion.
POST/convert/asyncSame body; returns 202 { jobId }. 503 when the job queue is disabled.
GET/convert/jobs/:id{ id, status, progress, result, error, attemptsMade }
DELETE/convert/jobs/:idCancel / remove a job (204)
POST/docgenMultipart template (HTML/Markdown/DOCX/…) + data (JSON) — server-side templating, returns a PDF
POST/bookmarks/readExtract the outline tree from an uploaded PDF
POST/bookmarks/writeWrite 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

StatusMeaning
400INVALID_INPUT / UNSUPPORTED_FORMAT
401PASSWORD_REQUIRED (encrypted source, no/wrong password)
413FILE_TOO_LARGE
502Upstream conversion failure
503Typed code when the conversion engine or the job queue is unavailable
504TIMEOUT
500INTERNAL

Strategies

Every format routes through the first strategy whose canHandle claims it. All strategies ship registered in the server app:

StrategyHandles
PassthroughStrategyPDF 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
HTMLStrategyhtml, htm, xhtml — via the HTML render pipeline
TextStrategytxt, markdown (md/mdx/…), json, xml, yaml, source code (js/ts/py/java/go/rust/…), sql
ImageStrategyjpg/jpeg/png/apng/gif/bmp/svg/webp/avif/ico and other browser-native images
RasterImageStrategyTIFF and multi-page rasters
MagickImageStrategyExtended raster pipeline — HEIC, camera raw, and other exotic raster formats
EmailStrategyeml, msg (with attachments listed)
XpsStrategyxps, oxps
XodStrategyxod — legacy WebViewer XOD archives
TruCadStrategydxf, 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)
RvtStrategyrvt, 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.

VariablePurpose
TRUPDF_LICENSELicense token — the server refuses to start without one (TRUPDF_LICENSE_DEV_MODE=1 for dev only)
TRUPDF_CONVERTER_API_KEYRequired in production. The viewer sends it via the converterApiKey prop (X-TruPDF-API-Key)
CORS_ORIGINSRequired — comma-separated allowed origins; no default
STORAGE_ADAPTERlocal | s3 | azure | webhook
LOCAL_STORAGE_DIROutput directory for the local adapter
LOCAL_STORAGE_PUBLIC_URLRequired 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_URLTruCAD engine sidecar URL (2D CAD formats)
PORTDefault 3002
CONVERSION_TIMEOUT_MSDefault 1800000
MAX_UPLOAD_BYTESDefault 0 (unlimited)
CACHE_TTL_SECONDSDefault 604800
RETENTION_SWEEP_INTERVAL_MSStored-result cleanup sweep interval
RETENTION_MAX_AGE_MSStored-result max age
LOG_LEVELLogger 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.