MohammedMohammedMohammedAnas K V

Initializing
0%
Press?for keyboard shortcuts
Back to Blog
Integration

Solving the QZ Tray Certificate Nightmare: A Production Guide

How to properly configure QZ Tray certificate uploads and eliminate those frustrating silent print failures in enterprise applications.

February 10, 2025
12 min read
QZ TrayAngularNestJSJavaScriptEnterprise

The Problem Everyone Faces with QZ Tray

If you've integrated QZ Tray for silent printing in your ERP or POS system, you've probably hit this wall: certificates silently fail, customers see "ready" but nothing prints, and you have no idea why.

After implementing QZ Tray across multiple CAAD ERP deployments, here's the complete battle-tested solution.

Understanding QZ Tray's Trust Model

QZ Tray requires a trusted certificate chain between the browser and the tray app. Without this:

  • Prints silently fail
  • Users see confusing errors
  • Production environments break mysteriously

Step 1: Generate Your Certificate Authority

First, create a self-signed root CA:

bash
# Generate CA private key
openssl genrsa -out private/ca.key 4096

# Generate CA certificate (valid 10 years)
openssl req -new -x509 -days 3650 -key private/ca.key \
  -out certs/ca.crt \
  -subj "/C=AE/ST=Dubai/L=Dubai/O=YourCompany/CN=YourCompany Root CA"

Critical: The Common Name (CN) must match your production domain.

Step 2: Generate Site Certificate

bash
# Generate site private key
openssl genrsa -out private/site.key 2048

# Create certificate signing request
openssl req -new -key private/site.key \
  -out site.csr \
  -subj "/C=AE/ST=Dubai/L=Dubai/O=YourCompany/CN=yourdomain.com"

# Sign with your CA
openssl x509 -req -in site.csr \
  -CA certs/ca.crt -CAkey private/ca.key \
  -out certs/site.crt -days 365

Step 3: Convert to PKCS12 Format

QZ Tray needs PKCS12 (.p12) format:

bash
openssl pkcs12 -export \
  -inkey private/site.key \
  -in certs/site.crt \
  -certfile certs/ca.crt \
  -out certs/site.p12 \
  -name "qz-tray-cert"

You'll be prompted for a password. Remember this - your app needs it.

Step 4: Upload Certificate to QZ Tray

Secure upload endpoint:

typescript
@Post('qz/certificate')
@UseGuards(AuthGuard)
async uploadCertificate(
  @UploadedFile() file: Express.Multer.File,
  @Body() body: { password: string }
) {
  if (!file.originalname.endsWith('.p12')) {
    throw new BadRequestException('Only .p12 files allowed');
  }

  const certPath = `/secure/qz-certs/${this.tenantId}/${file.filename}`;
  await fs.promises.writeFile(certPath, file.buffer, { mode: 0o600 });

  return { success: true, path: certPath };
}

Step 5: Configure QZ Tray Connection

In your Angular app:

typescript
async setupQZTray() {
  const qz = await import('qz-tray');

  qz.security.setCertificatePromise((resolve, reject) => {
    fetch('/api/v1/qz/certificate/download', {
      credentials: 'include',
      headers: { 'X-Tenant-ID': this.tenantId }
    })
    .then(res => res.arrayBuffer())
    .then(buf => resolve(buf))
    .catch(reject);
  });

  qz.security.setSignaturePromise((toSign) => {
    return (async () => {
      const data = new TextEncoder().encode(toSign);
      const hashBuffer = await crypto.subtle.digest('SHA-512', data);
      const hashArray = Array.from(new Uint8Array(hashBuffer));
      return hashArray.map(b => b.toString(16).padStart(2, '0')).join('');
    })();
  });

  await qz.websocket.connect();
}

Common Pitfalls I Hit in Production

Certificate Path Mismatch

The CN in your cert MUST match the URL users access the app from.

Solution: Use Subject Alternative Names (SAN):

bash
openssl x509 -req -in site.csr \
  -CA certs/ca.crt -CAkey private/ca.key \
  -extfile <(echo "subjectAltName=DNS:yourdomain.com,DNS:www.yourdomain.com") \
  -out certs/site.crt -days 365

CA Not Trusted by Client Machine

Self-signed CAs need to be installed on every client machine:

Windows:

powershell
certutil -addstore -f "ROOT" ca.crt

macOS:

bash
sudo security add-trusted-cert -d -r trustRoot \
  -k /Library/Keychains/System.keychain ca.crt

The 2-Minute Test Script

After deployment, run this on a client machine:

javascript
(async () => {
  const qz = await import('qz-tray');
  await qz.websocket.connect();
  const printers = await qz.printers.find();
  console.log('Printers:', printers);

  const config = qz.configs.create('Test Printer');
  const data = [{
    type: 'raw',
    format: 'plain',
    data: 'TEST PRINT\n'
  }];
  await qz.print(config, data);
})();

If you see your test print, everything's working.

Wrapping Up

QZ Tray certificate management is the hardest part of the integration. The key insights:

  • Generate certs matching your exact production domain
  • Use PKCS12 format (.p12)
  • Install root CA on every client machine
  • Secure upload endpoint with auth
  • Monitor health in production

I've deployed this pattern to production CAAD ERP systems serving 20K+ users with zero certificate issues since implementation.

Technologies

QZ TrayAngularNestJSJavaScriptEnterprise
Share