Thapa Technical — Dev Blog
Node.js PDF Generation Stripe Webhooks

How to Build Custom
Invoicing in Node.js.

Automating invoicing is core to any SaaS or e-commerce solution. This guide walks you through designing invoice relational structures, generating high-performance PDFs, and listening to Stripe webhooks.

1. Database Schema Blueprint

A robust invoicing system requires distinct database objects. You must separate the Invoice header metadata (payment method, statuses) from individual Invoice Line Items (products, quantities, unit prices) to support historical compliance.

// SQL structure for standard Invoices CREATE TABLE invoices ( id VARCHAR(36) PRIMARY KEY, customer_id VARCHAR(36) NOT NULL, invoice_number VARCHAR(50) UNIQUE, status VARCHAR(20) DEFAULT 'unpaid', subtotal DECIMAL(10,2), tax DECIMAL(10,2), total DECIMAL(10,2), created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP );

2. High-Performance PDF Generation

While spawning a headless browser like Puppeteer is ideal for complicated layouts, drawing raw shapes and texts with pdfkit uses significantly less RAM and compiles the output buffer in milliseconds.

const PDFDocument = require('pdfkit'); const fs = require('fs'); function generateInvoicePDF(invoiceData, path) { const doc = new PDFDocument({ size: 'A4', margin: 50 }); doc.pipe(fs.createWriteStream(path)); // Add invoice headers doc.fontSize(20).text('KODYFIER INVOICE', { align: 'right' }); doc.fontSize(10).text(`Number: ${invoiceData.invoice_number}`); doc.text(`Date: ${new Date().toLocaleDateString()}`); doc.moveDown(); doc.text(`To: ${invoiceData.customer_name}`); // Draw line table doc.lineGap(10); invoiceData.items.forEach(item => { doc.text(`${item.name} x${item.qty} - ₹${item.price}`); }); doc.end(); }
Using raw stream piping allows Node.js to upload the PDF to cloud storage (like AWS S3) concurrently as it builds, keeping memory leaks to zero.

3. Stripe Webhook Listener

Never mark an invoice paid based solely on frontend redirection. Webhooks provide transactional integrity. You must validate the Stripe webhook signature headers using the raw buffer input before processing.

const express = require('express'); const stripe = require('stripe')(process.env.STRIPE_SECRET_KEY); const app = express(); // Note: stripe requires the RAW buffer body for signature checks app.post('/webhook', express.raw({ type: 'application/json' }), (req, res) => { const sig = req.headers['stripe-signature']; let event; try { event = stripe.webhooks.constructEvent(req.body, sig, process.env.STRIPE_WEBHOOK_SECRET); } catch (err) { return res.status(400).send(`Webhook Error: ${err.message}`); } // Handle success checkout event if (event.type === 'checkout.session.completed') { const session = event.data.object; markInvoicePaid(session.metadata.invoice_id); } res.json({ received: true }); });

Discussion

Leave a Reply
Ready to Learn Backend?
Build Enterprise Apps

Master Express, MongoDB, PostgreSQL, Stripe integration, and deployment workflows in our live online classes.

Explore Courses
Also check: /courses/full-stack-ai-bootcamp.php — Full Stack Bootcamp