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:
# 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
# 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 365Step 3: Convert to PKCS12 Format
QZ Tray needs PKCS12 (.p12) format:
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:
@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:
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):
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 365CA Not Trusted by Client Machine
Self-signed CAs need to be installed on every client machine:
Windows:
certutil -addstore -f "ROOT" ca.crtmacOS:
sudo security add-trusted-cert -d -r trustRoot \
-k /Library/Keychains/System.keychain ca.crtThe 2-Minute Test Script
After deployment, run this on a client machine:
(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.