Skip to content

UserOfficeProject/user-office-factory

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

1,587 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

user-office-factory

Service responsible for rendering and exporting User Office content as:

  • PDF documents (proposal PDFs, sample PDFs, shipment labels)
  • XLSX exports (proposal and FAP/call FAP)
  • ZIP archives (attachments and proposal bundles)

The service exposes a small HTTP API and uses a workflow system to generate the requested output and stream it back to the client.

Requirements

  • Node.js >= 22 (see package.json#engines)
  • Postgres connectivity (for file/attachment data)
  • Chromium (via Puppeteer)

Quick Start

npm install
cp example.env .env
npm run dev

By default the server listens on port 4500.

HTTP API

Generate exports

POST /generate/:downloadType/:type

The response streams the generated output back to the client.

downloadType:

  • pdf
  • xlsx
  • zip

Supported type values:

  • pdf: proposal, sample, shipment-label
  • xlsx: proposal, fap, call_fap
  • zip: attachment, proposal

PDF generation

PDF generation uses Puppeteer. The service supports two modes:

Built-in Chromium (default)

By default, the service launches a local Chromium instance via Puppeteer. This is the simplest setup and maintains backward compatibility.

  • No additional configuration required
  • Chromium is bundled with Puppeteer
  • Recommended MAX_CONCURRENT_PDF_GENERATIONS: 2 (depending on resources)

Remote Browserless cluster

For better scalability and resource isolation, you can offload browser rendering to a remote Browserless cluster. This separates the Node.js application from the Chrome rendering workload.

Why use a remote browser instead of built-in Chromium?

  • Better scalability: Browser capacity can be scaled independently from API replicas.
  • Resource isolation: Chrome CPU/RAM spikes do not directly impact the Node.js process.
  • Higher throughput: Multiple factory instances can share one Browserless cluster.
  • Operational flexibility: Browser lifecycle, limits, and upgrades are managed in one place.
  • Improved resilience: Browser crashes are isolated from the app and easier to recover from.

Environment variables for Browserless:

  • BROWSER_WS_ENDPOINT - WebSocket endpoint of the Browserless cluster (e.g., ws://browserless:3000)
  • FACTORY_BASE_URL - Base URL where the factory service is reachable by the remote browser (e.g., http://factory:4500)

When BROWSER_WS_ENDPOINT is set, the service connects to the remote cluster instead of launching local Chromium. Each PDF generation creates a fresh browser session managed by Browserless.

Recommended MAX_CONCURRENT_PDF_GENERATIONS for Browserless: 5-10 (depending on cluster size and resources)

Switching modes (built-in <-> remote)

Use environment variables to select the browser mode.

Use built-in Chromium

  1. Do not set BROWSER_WS_ENDPOINT.
  2. Optionally remove FACTORY_BASE_URL (it is not required in built-in mode).
  3. Set MAX_CONCURRENT_PDF_GENERATIONS conservatively (start around 2).
  4. Optional: set UO_FEATURE_ALLOW_NO_SANDBOX=1 only if your runtime requires it.

Use remote Browserless

  1. Start/reach a Browserless service.
  2. Set BROWSER_WS_ENDPOINT to the Browserless WebSocket endpoint.
  3. Set FACTORY_BASE_URL to the factory URL resolvable by Browserless.
  4. Tune MAX_CONCURRENT_PDF_GENERATIONS to Browserless capacity (for one pod, approximate upper bound is CONCURRENT + QUEUED).
  5. Ignore UO_FEATURE_ALLOW_NO_SANDBOX (it only affects built-in Chromium launch).

Concurrency limiting

  • The service uses a semaphore to limit concurrent Puppeteer page work. (see MAX_CONCURRENT_PDF_GENERATIONS)
  • This protects CPU/memory under load (e.g. “download multiple proposals”).
  • Navigation and operation timeouts are controlled via PDF_GENERATION_TIMEOUT.

Configuration

Copy and adjust example.env as needed.

Server

  • NODE_PORT (default: 4500)
  • NODE_ENV (development / production)
  • REQUEST_BODY_LIMIT (default: 20mb) maximum accepted request body size for JSON/urlencoded payloads
    • Increase this when /generate requests include large embedded template/data payloads

Database

Either provide a full connection string:

  • DATABASE_CONNECTION_STRING

Or provide discrete settings:

  • DATABASE_HOSTNAME
  • DATABASE_PORT (default: 5432)
  • DATABASE_USER
  • DATABASE_PASSWORD
  • DATABASE_DATABASE

Puppeteer / PDF Generation

  • MAX_CONCURRENT_PDF_GENERATIONS (default: 2) to limit concurrent PDF generations, adjust based on available CPU/memory.
    • Built-in Chromium: recommended max. 2-4
    • Remote Browserless cluster: 5-10 (depending on cluster size and resources)
    • When Puppeteer throws like Protocol error: Connection closed. errors under load, reduce this value.
  • PDF_GENERATION_TIMEOUT (default: 60000 ms) to set maximum time for PDF generation
    • When Navigation timeout errors occur, increase this value.
  • PDF_MAX_RETRIES (default: 3) maximum attempts for transient PDF generation failures
    • Retry backoff is exponential (2s, 4s, 8s, ...).
  • PDF_DEBUG_HTML=1 to write the rendered HTML alongside the generated PDF
  • UO_FEATURE_ALLOW_NO_SANDBOX=1 to launch Chromium with --no-sandbox
    • Use only when your runtime cannot support Chromium sandboxing (common in some containers).
    • Applies only to built-in Chromium mode (puppeteer.launch), ignored in Browserless mode. Security note: disabling sandbox reduces browser process isolation.

Browserless (Remote Browser)

See k8s/browserless/ for Kubernetes deployment instructions.

  • BROWSER_WS_ENDPOINT - WebSocket endpoint of the Browserless cluster
    • Examples:
      • Docker Compose: ws://browserless:3000
      • Local dev + Browserless in Docker: ws://localhost:3010
      • Kubernetes: ws://browserless.default.svc.cluster.local:3000
    • When set, PDF generation uses the remote browser cluster instead of local Chromium
  • FACTORY_BASE_URL - Base URL of the factory app, used by the remote browser to fetch static assets (CSS, fonts, images, JS)
    • Defaults to http://localhost:<NODE_PORT> for local development
    • Must be set to a hostname resolvable by the Browserless container
    • Examples:
      • Docker Compose: http://factory:4500
      • Local dev + Browserless in Docker: http://host.docker.internal:4500
      • Kubernetes: http://<service-name>.<namespace>.svc.cluster.local:<port>

Resource planning

Set MAX_CONCURRENT_PDF_GENERATIONS based on the browser mode and available capacity:

  • Built-in Chromium mode: start around 2 and increase only if CPU/memory headroom allows it.
  • Remote Browserless mode: size against Browserless capacity.
    • Approximate upper bound per Browserless pod is CONCURRENT + QUEUED.
    • Example: CONCURRENT=5, QUEUED=10 => upper bound 15.
    • If multiple Browserless pods are behind a load balancer, total cluster capacity scales by pod count.

Practical tuning guidance:

  • If you see browser connection/queue errors, reduce MAX_CONCURRENT_PDF_GENERATIONS.
  • If requests time out, increase PDF_GENERATION_TIMEOUT and/or lower concurrency.
  • Increase concurrency gradually while monitoring CPU, memory, error rate, and PDF completeness.

Custom Templates

When creating custom templates that reference factory-hosted assets (images, fonts, CSS, JS), use the {{factoryBaseUrl}} Handlebars helper instead of hardcoding URLs.

  • Built-in Chromium mode: The browser runs locally and can access localhost directly
  • Browserless mode: The remote browser cannot resolve localhost - it needs the factory's network-reachable URL

Using {{factoryBaseUrl}} ensures templates work in both modes.

Example

Incorrect (hardcoded localhost - breaks with Browserless):

<img src="http://localhost:4500/static/images/logo.png" />
<link rel="stylesheet" href="http://localhost:4500/static/css/custom.css" />

Correct (using helper - works in all modes):

<img src="{{factoryBaseUrl}}/static/images/logo.png" />
<link rel="stylesheet" href="{{factoryBaseUrl}}/static/css/custom.css" />

The {{factoryBaseUrl}} helper is automatically available in all templates and resolves to:

  • http://localhost:4500 when using built-in Chromium (default)
  • The value of FACTORY_BASE_URL env var when using Browserless

Future improvements

  • Improve the HTML render waiting strategy before PDF generation to ensure pages are fully rendered.
  • After the waiting strategy is in place, avoid creating a new browser context for every request; evaluate reusing the default/shared context to improve cache reuse for static assets.
  • Leverage static asset caching (Cache-Control / max-age) together with context reuse to reduce repeated CSS/font/image fetches.
  • Retry logic for transient PDF generation errors (e.g., navigation timeout error etc...)
  • Config class to centralize and validate environment variable parsing and defaults.

OpenTelemetry

OpenTelemetry provides distributed tracing, logging, and metrics instrumentation for the factory service. This helps you monitor performance, debug issues, and understand request flows across your system.

Quick Start

The simplest setup requires just one environment variable:

# .env
OTEL_EXPORTER_OTLP_ENDPOINT="http://lgtm:4318/v1/traces"
OTEL_SERVICE_NAME="proposal-factory"

Then start the service:

npm run dev

If successful, you'll see this in the console:

Starting OpenTelemetry tracing with configuration: {
  tracesEndpoint: 'http://lgtm:4318/v1/traces',
  service: 'proposal-factory'
}

Environment Variables

Required for Tracing

Variable Description Example
OTEL_EXPORTER_OTLP_ENDPOINT OTLP HTTP endpoint for traces http://lgtm:4318/v1/traces
OTEL_SERVICE_NAME Service identifier in traces proposal-factory

Optional (Logs & Metrics)

Variable Description Example
OTEL_EXPORTER_OTLP_LOGS_ENDPOINT OTLP HTTP endpoint for logs http://lgtm:4318/v1/logs
OTEL_EXPORTER_OTLP_METRICS_ENDPOINT OTLP HTTP endpoint for metrics http://lgtm:4318/v1/metrics

Configuration Examples

Full observability (tracing + logs + metrics):

OTEL_EXPORTER_OTLP_ENDPOINT="http://lgtm:4318/v1/traces"
OTEL_EXPORTER_OTLP_LOGS_ENDPOINT="http://lgtm:4318/v1/logs"
OTEL_EXPORTER_OTLP_METRICS_ENDPOINT="http://lgtm:4318/v1/metrics"
OTEL_SERVICE_NAME="proposal-factory"
DEPENDENCY_CONFIG= "stfc"

Docker Compose with LGTM/Grafana:

# docker-compose.yml
services:
  factory:
    image: user-office-factory:latest
    environment:
      OTEL_EXPORTER_OTLP_ENDPOINT: "http://lgtm:4318/v1/traces"
      OTEL_EXPORTER_OTLP_LOGS_ENDPOINT: "http://lgtm:4318/v1/logs"
      OTEL_EXPORTER_OTLP_LOGS_ENDPOINT: "http://lgtm:4318/v1/metrics"
      DEPENDENCY_CONFIG: stfc
      OTEL_SERVICE_NAME: "proposal-factory"
    ports:
      - "4500:4500"

  lgtm:
    image: grafana/otel-lgtm:latest
    ports:
      - "3000:3000"    # Grafana
      - "4318:4318"    # OTLP HTTP receiver

Service started successfully:

Starting OpenTelemetry tracing with configuration: {
  tracesEndpoint: 'http://lgtm:4318/v1/traces',
  logsEndpoint: 'http://lgtm:4318/v1/logs',
  service: 'proposal-factory'
}

Goto "grafana explorer"

About

No description, website, or topics provided.

Resources

License

Stars

Watchers

Forks

Packages

 
 
 

Contributors