This implementation adds asynchronous PDF certificate generation for carbon credit retirements. Certificates are generated as background jobs, uploaded to IPFS via Pinata, and users are notified via email when ready. This prevents API timeouts and improves user experience.
- Added certificate-related fields to
RetirementRecordmodel - Fields track certificate status, IPFS CID, URL, retry count, and timestamps
- Migration required:
npx prisma migrate dev --name add_certificate_fields
- Generates PDF certificates using PDFKit
- Includes retirement details, beneficiary, amount, project info
- Professional styling with borders and formatting
- Returns PDF as Buffer for upload
- Uploads PDF files to Pinata (IPFS gateway)
- Returns IPFS CID and public gateway URL
- Verifies pin status
- Handles API authentication
- Sends email notifications when certificate is ready
- Sends failure notifications with retry information
- Supports SMTP configuration or mock mode for development
- HTML email templates included
- Orchestrates the entire workflow
- Polls for pending certificates every 60 seconds
- Handles retries (up to 3 attempts with exponential backoff)
- Updates retirement record with certificate details
- Manages status transitions
- Updated QueueProcessor to handle certificate generation jobs
- Integrated with BullMQ for job processing
- Automatic retry logic with exponential backoff
GET /retirements/certificate-status/:id
Returns certificate generation status and IPFS URL.
GET /retirements/:id
Now includes certificate fields in response.
- Added environment variables for Pinata and SMTP
- Updated
.env.examplewith new configuration options - Supports mock mode for development (no SMTP required)
cd carbonledger/backend
npm installThis installs:
pdfkit- PDF generationpinata- IPFS/Pinata clientqrcode- QR code generationnodemailer- Email notifications
npx prisma migrate dev --name add_certificate_fieldsThis creates the migration and updates your database schema.
Copy .env.example to .env and fill in:
# Required for certificate generation
IPFS_API_KEY=your_pinata_api_key
IPFS_SECRET_KEY=your_pinata_secret_key
# Optional for email notifications (mock mode if not set)
SMTP_HOST=smtp.gmail.com
SMTP_PORT=587
SMTP_USER=your_email@gmail.com
SMTP_PASS=your_app_password
SMTP_FROM=noreply@carbonledger.io
SMTP_SECURE=falsenpm run start:devYou should see logs like:
[QueueModule] Polling for pending certificates...
-
User Retires Credits
POST /credits/retire → RetirementRecord created with certificateStatus = "pending_certificate" → API returns immediately (no blocking) -
Background Polling (Every 60 seconds)
CertificateProcessor.pollPendingCertificates() → Query pending certificates → For each: processCertificateGeneration() -
Certificate Generation
a. Update status to "generating" b. Generate PDF with retirement details c. Upload to Pinata d. Update record with CID and URL e. Send success email -
User Retrieval
GET /retirements/certificate-status/:id → Returns certificate URL and status
- Max Retries: 3 attempts
- Backoff: Exponential (5s, 10s, 20s)
- Failure Handling: After 3 failed attempts:
- Certificate marked as "failed"
- User notified via email
- Manual intervention may be required
carbonledger/backend/src/
├── certificates/
│ ├── certificate.service.ts # PDF generation
│ ├── pinata.service.ts # IPFS upload
│ ├── notification.service.ts # Email notifications
│ ├── certificate.processor.ts # Orchestration & polling
│ └── certificates.module.ts # Module definition
├── queue/
│ ├── queue.processor.ts # Updated with certificate handler
│ ├── queue.module.ts # Updated with polling setup
│ └── queue.constants.ts # Job types
├── retirements/
│ ├── retirements.service.ts # Updated with certificate methods
│ ├── retirements.controller.ts # Updated with certificate endpoint
│ └── retirements.module.ts # Updated imports
├── app.module.ts # Updated with CertificatesModule
└── prisma/
└── schema.prisma # Updated RetirementRecord model
curl -X POST http://localhost:3001/api/v1/credits/retire \
-H "Authorization: Bearer $JWT_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"batchId": "batch-123",
"amount": 100,
"beneficiary": "Company XYZ",
"retirementReason": "Carbon offset",
"holderPublicKey": "GXXXXXX"
}'Response:
{
"retirementId": "ret-batch-123-1234567890",
"certificateStatus": "pending_certificate",
"certificateCid": null,
"certificateUrl": null,
...
}curl http://localhost:3001/api/v1/retirements/certificate-status/ret-batch-123-1234567890Response (after ~60 seconds):
{
"retirementId": "ret-batch-123-1234567890",
"status": "completed",
"cid": "QmXxxx...",
"url": "https://gateway.pinata.cloud/ipfs/QmXxxx...",
"generatedAt": "2024-05-30T10:30:00Z",
"failedAt": null,
"retries": 0
}curl http://localhost:3001/api/v1/queue/statsResponse:
{
"waiting": 0,
"active": 0,
"completed": 5,
"failed": 0,
"delayed": 0
}✅ Job polls for retirements with status=pending_certificate every 60 seconds
- Implemented in
CertificateProcessor.pollPendingCertificates() - Called every 60 seconds via
setIntervalinQueueModule.onModuleInit()
✅ Generates a PDF certificate and uploads it to IPFS via Pinata
CertificateService.generatePdf()creates professional PDFPinataService.uploadFile()uploads to Pinata- Returns CID and public gateway URL
✅ Updates the retirement record with the IPFS CID and public URL
CertificateProcessor.processCertificateGeneration()updates:certificateCid- IPFS CIDcertificateUrl- Public gateway URLcertificateGeneratedAt- TimestampcertificateStatus- "completed"
✅ Retries failed certificate generation up to 3 times before marking as failed
- Retry logic in
CertificateProcessor.processCertificateGeneration() - Increments
certificateRetriescounter - After 3 attempts, marks as "failed"
- Exponential backoff via BullMQ
✅ Sends a notification to the user when the certificate is ready
NotificationService.sendCertificateReady()sends email- Includes certificate URL and retirement details
- Also sends failure notification if generation fails
- Polling Interval: 60 seconds (configurable)
- Batch Size: Max 10 certificates per poll
- PDF Generation: ~500ms per certificate
- IPFS Upload: ~1-2 seconds per certificate
- Email Send: ~500ms per email
- Total Time: ~2-3 seconds per certificate (non-blocking)
npm run start:dev
# Look for: "Polling for pending certificates..."
# Look for: "Certificate generated successfully..."
# Look for: "Certificate generation failed..."# Connect to PostgreSQL
psql postgresql://carbonledger:changeme@localhost:5432/carbonledger
# Check pending certificates
SELECT retirementId, certificateStatus, certificateRetries
FROM "RetirementRecord"
WHERE certificateStatus != 'completed';
# Check failed certificates
SELECT retirementId, certificateStatus, certificateFailedAt
FROM "RetirementRecord"
WHERE certificateStatus = 'failed';# Connect to Redis
redis-cli
# Check queue stats
LLEN carbonledger:waiting
LLEN carbonledger:active
LLEN carbonledger:completed
LLEN carbonledger:failedProblem: Certificates stuck in "pending_certificate" status
Solutions:
- Check Redis connection:
redis-cli ping - Check Pinata credentials:
echo $IPFS_API_KEY - Check logs for errors:
npm run start:dev - Verify database migration:
npx prisma migrate status - Check Pinata account quota and API limits
Problem: Users not receiving certificate ready emails
Solutions:
- Verify SMTP credentials in
.env - Check firewall/network access to SMTP server
- Review logs for email errors
- Test with mock mode (remove SMTP config)
- Check email spam folder
Problem: "Pinata upload failed" errors
Solutions:
- Verify Pinata API key and secret
- Check Pinata account quota
- Verify network connectivity
- Check file size (should be < 10MB)
- Try uploading manually to Pinata dashboard
Problem: Memory usage increasing over time
Solutions:
- Reduce polling batch size (currently 10)
- Increase polling interval (currently 60s)
- Monitor PDF generation memory usage
- Check for memory leaks in dependencies
- API Keys: Store Pinata credentials in environment variables only
- Email Credentials: Use app-specific passwords, not account passwords
- IPFS URLs: Public gateway URLs are accessible to anyone with the CID
- Retirement Data: Sensitive data (beneficiary, reason) stored in PDF
- Rate Limiting: Consider adding rate limits to certificate endpoints
- Access Control: Ensure only authenticated users can check certificate status
- Webhook Notifications: Use Pinata webhooks instead of polling
- Parallel Processing: Process multiple certificates in parallel
- Certificate Customization: Allow users to customize certificate design
- Blockchain Verification: Store certificate CID on-chain
- Certificate Revocation: Support certificate revocation if needed
- Analytics: Track certificate generation metrics
- Caching: Cache generated certificates for faster retrieval
- Batch Operations: Support bulk certificate generation
- See
CERTIFICATE_GENERATION.mdfor detailed technical documentation - See
backend/package.jsonfor dependency versions - See
backend/prisma/schema.prismafor database schema - See
.env.examplefor configuration options
If you need to rollback this implementation:
-
Revert Database
npx prisma migrate resolve --rolled-back add_certificate_fields
-
Revert Code
git revert <commit-hash>
-
Reinstall Dependencies
npm install
-
Restart Backend
npm run start:dev
For issues or questions:
- Check logs:
npm run start:dev - Review documentation:
CERTIFICATE_GENERATION.md - Check database:
npx prisma studio - Monitor queue:
GET /queue/stats