@girardmedia/bootspring 3.3.2 → 3.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (171) hide show
  1. package/assets/agents/accessibility-auditor.md +39 -0
  2. package/assets/agents/api-designer.md +40 -0
  3. package/assets/agents/auth-implementer.md +64 -0
  4. package/assets/agents/bug-hunter.md +42 -0
  5. package/assets/agents/bundle-analyzer.md +40 -0
  6. package/assets/agents/cache-optimizer.md +55 -0
  7. package/assets/agents/changelog-writer.md +55 -0
  8. package/assets/agents/ci-cd-builder.md +40 -0
  9. package/assets/agents/code-explainer.md +39 -0
  10. package/assets/agents/code-reviewer.md +39 -0
  11. package/assets/agents/cost-optimizer.md +57 -0
  12. package/assets/agents/cron-scheduler.md +51 -0
  13. package/assets/agents/data-seeder.md +56 -0
  14. package/assets/agents/database-architect.md +40 -0
  15. package/assets/agents/dependency-updater.md +40 -0
  16. package/assets/agents/deploy-checker.md +40 -0
  17. package/assets/agents/docker-optimizer.md +40 -0
  18. package/assets/agents/documentation-writer.md +40 -0
  19. package/assets/agents/email-builder.md +55 -0
  20. package/assets/agents/env-setup.md +40 -0
  21. package/assets/agents/error-handler.md +40 -0
  22. package/assets/agents/eslint-fixer.md +46 -0
  23. package/assets/agents/feature-flagger.md +69 -0
  24. package/assets/agents/git-detective.md +39 -0
  25. package/assets/agents/graphql-builder.md +60 -0
  26. package/assets/agents/incident-responder.md +59 -0
  27. package/assets/agents/log-analyzer.md +39 -0
  28. package/assets/agents/migration-planner.md +41 -0
  29. package/assets/agents/monorepo-navigator.md +39 -0
  30. package/assets/agents/nextjs-expert.md +57 -0
  31. package/assets/agents/notification-builder.md +56 -0
  32. package/assets/agents/onboarding-guide.md +39 -0
  33. package/assets/agents/performance-profiler.md +40 -0
  34. package/assets/agents/prisma-expert.md +57 -0
  35. package/assets/agents/rate-limiter.md +58 -0
  36. package/assets/agents/react-expert.md +58 -0
  37. package/assets/agents/refactorer.md +42 -0
  38. package/assets/agents/regex-builder.md +46 -0
  39. package/assets/agents/release-manager.md +40 -0
  40. package/assets/agents/s3-manager.md +58 -0
  41. package/assets/agents/schema-validator.md +40 -0
  42. package/assets/agents/search-builder.md +62 -0
  43. package/assets/agents/security-auditor.md +39 -0
  44. package/assets/agents/sitemap-generator.md +53 -0
  45. package/assets/agents/stripe-integrator.md +59 -0
  46. package/assets/agents/tailwind-expert.md +55 -0
  47. package/assets/agents/tech-debt-tracker.md +39 -0
  48. package/assets/agents/test-writer.md +42 -0
  49. package/assets/agents/type-fixer.md +45 -0
  50. package/assets/agents/webhook-builder.md +54 -0
  51. package/assets/rules/cpp.md +53 -0
  52. package/assets/rules/css.md +52 -0
  53. package/assets/rules/go.md +50 -0
  54. package/assets/rules/html.md +52 -0
  55. package/assets/rules/java.md +51 -0
  56. package/assets/rules/kotlin.md +50 -0
  57. package/assets/rules/php.md +51 -0
  58. package/assets/rules/python.md +51 -0
  59. package/assets/rules/ruby.md +51 -0
  60. package/assets/rules/rust.md +49 -0
  61. package/assets/rules/shell.md +52 -0
  62. package/assets/rules/sql.md +49 -0
  63. package/assets/rules/swift.md +50 -0
  64. package/assets/rules/typescript.md +52 -0
  65. package/assets/rules/yaml-json.md +51 -0
  66. package/assets/skills/accessibility.md +210 -0
  67. package/assets/skills/agent-patterns.md +387 -0
  68. package/assets/skills/ai-integration.md +263 -0
  69. package/assets/skills/animation-patterns.md +224 -0
  70. package/assets/skills/api-design.md +218 -0
  71. package/assets/skills/api-gateway.md +341 -0
  72. package/assets/skills/api-versioning.md +226 -0
  73. package/assets/skills/astro-patterns.md +233 -0
  74. package/assets/skills/auth-patterns.md +248 -0
  75. package/assets/skills/aws-patterns.md +171 -0
  76. package/assets/skills/background-jobs.md +162 -0
  77. package/assets/skills/browser-extensions.md +309 -0
  78. package/assets/skills/caching-patterns.md +253 -0
  79. package/assets/skills/ci-cd.md +251 -0
  80. package/assets/skills/cli-development.md +296 -0
  81. package/assets/skills/code-review.md +185 -0
  82. package/assets/skills/cron-patterns.md +327 -0
  83. package/assets/skills/data-fetching.md +231 -0
  84. package/assets/skills/database-migrations.md +346 -0
  85. package/assets/skills/database-patterns.md +219 -0
  86. package/assets/skills/debugging.md +281 -0
  87. package/assets/skills/design-system.md +289 -0
  88. package/assets/skills/django-patterns.md +182 -0
  89. package/assets/skills/docker-patterns.md +235 -0
  90. package/assets/skills/e2e-testing.md +287 -0
  91. package/assets/skills/edge-computing.md +268 -0
  92. package/assets/skills/electron-patterns.md +266 -0
  93. package/assets/skills/email-templates.md +206 -0
  94. package/assets/skills/error-handling.md +265 -0
  95. package/assets/skills/event-driven.md +232 -0
  96. package/assets/skills/express-patterns.md +239 -0
  97. package/assets/skills/fastapi-patterns.md +198 -0
  98. package/assets/skills/feature-flags.md +212 -0
  99. package/assets/skills/figma-to-code.md +298 -0
  100. package/assets/skills/file-upload.md +228 -0
  101. package/assets/skills/forms-patterns.md +264 -0
  102. package/assets/skills/gcp-patterns.md +189 -0
  103. package/assets/skills/git-workflow.md +187 -0
  104. package/assets/skills/golang-patterns.md +185 -0
  105. package/assets/skills/graphql-patterns.md +244 -0
  106. package/assets/skills/i18n-patterns.md +172 -0
  107. package/assets/skills/image-processing.md +350 -0
  108. package/assets/skills/java-springboot.md +226 -0
  109. package/assets/skills/kotlin-patterns.md +207 -0
  110. package/assets/skills/kubernetes-patterns.md +326 -0
  111. package/assets/skills/laravel-patterns.md +261 -0
  112. package/assets/skills/llm-fine-tuning.md +335 -0
  113. package/assets/skills/load-testing.md +303 -0
  114. package/assets/skills/logging-observability.md +228 -0
  115. package/assets/skills/markdown-processing.md +318 -0
  116. package/assets/skills/mcp-server-patterns.md +292 -0
  117. package/assets/skills/microservices.md +272 -0
  118. package/assets/skills/migration-patterns.md +239 -0
  119. package/assets/skills/mongodb-patterns.md +189 -0
  120. package/assets/skills/monorepo-patterns.md +287 -0
  121. package/assets/skills/nextjs-app-router.md +237 -0
  122. package/assets/skills/notification-patterns.md +348 -0
  123. package/assets/skills/oauth-patterns.md +246 -0
  124. package/assets/skills/payment-integration.md +222 -0
  125. package/assets/skills/pdf-generation.md +307 -0
  126. package/assets/skills/performance-optimization.md +277 -0
  127. package/assets/skills/php-patterns.md +210 -0
  128. package/assets/skills/prisma-patterns.md +241 -0
  129. package/assets/skills/prompt-engineering.md +193 -0
  130. package/assets/skills/pwa-patterns.md +247 -0
  131. package/assets/skills/python-patterns.md +158 -0
  132. package/assets/skills/python-testing.md +172 -0
  133. package/assets/skills/queue-patterns.md +295 -0
  134. package/assets/skills/rag-patterns.md +159 -0
  135. package/assets/skills/rate-limiting.md +319 -0
  136. package/assets/skills/react-components.md +201 -0
  137. package/assets/skills/react-native-patterns.md +299 -0
  138. package/assets/skills/real-time-patterns.md +181 -0
  139. package/assets/skills/redis-patterns.md +188 -0
  140. package/assets/skills/refactoring.md +218 -0
  141. package/assets/skills/regex-patterns.md +191 -0
  142. package/assets/skills/remix-patterns.md +262 -0
  143. package/assets/skills/responsive-design.md +199 -0
  144. package/assets/skills/ruby-rails-patterns.md +178 -0
  145. package/assets/skills/rust-patterns.md +211 -0
  146. package/assets/skills/search-patterns.md +227 -0
  147. package/assets/skills/security-hardening.md +237 -0
  148. package/assets/skills/seo-patterns.md +179 -0
  149. package/assets/skills/serverless-patterns.md +223 -0
  150. package/assets/skills/sql-optimization.md +154 -0
  151. package/assets/skills/state-management.md +254 -0
  152. package/assets/skills/storybook-patterns.md +330 -0
  153. package/assets/skills/svelte-patterns.md +258 -0
  154. package/assets/skills/swift-patterns.md +227 -0
  155. package/assets/skills/tailwind-patterns.md +272 -0
  156. package/assets/skills/tdd-workflow.md +199 -0
  157. package/assets/skills/terraform-patterns.md +270 -0
  158. package/assets/skills/testing-react.md +240 -0
  159. package/assets/skills/testing-vitest.md +232 -0
  160. package/assets/skills/typescript-strict.md +159 -0
  161. package/assets/skills/video-processing.md +340 -0
  162. package/assets/skills/vue-patterns.md +247 -0
  163. package/assets/skills/web-workers.md +327 -0
  164. package/assets/skills/webhooks-patterns.md +283 -0
  165. package/assets/skills/websocket-patterns.md +306 -0
  166. package/dist/cli/index.js +941 -958
  167. package/dist/core/index.d.ts +341 -11
  168. package/dist/core.js +58 -95
  169. package/dist/mcp/index.d.ts +33 -1
  170. package/dist/mcp-server.js +177 -255
  171. package/package.json +4 -1
@@ -0,0 +1,222 @@
1
+ ---
2
+ name: payment-integration
3
+ description: Payment integration patterns with Stripe, webhooks, idempotency, subscription billing, and PCI compliance.
4
+ ---
5
+
6
+ # Payment Integration
7
+
8
+ ## When to Use
9
+ Apply when your application needs to accept payments -- one-time purchases, subscriptions, usage-based billing, or marketplace payouts. Stripe is the default choice for most SaaS and e-commerce applications. These patterns apply broadly to any payment provider. Never store card numbers or build custom card forms unless PCI DSS compliance is already in place.
10
+
11
+ ## How It Works
12
+
13
+ ### Stripe Checkout -- One-Time Payments
14
+
15
+ ```typescript
16
+ import Stripe from "stripe";
17
+
18
+ const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!);
19
+
20
+ app.post("/api/checkout", async (req, res) => {
21
+ const session = await stripe.checkout.sessions.create({
22
+ mode: "payment",
23
+ customer_email: req.user.email,
24
+ line_items: [{
25
+ price_data: {
26
+ currency: "usd",
27
+ product_data: { name: req.body.productName },
28
+ unit_amount: req.body.priceInCents, // always cents, never dollars
29
+ },
30
+ quantity: req.body.quantity,
31
+ }],
32
+ metadata: { userId: req.user.id, orderId: req.body.orderId },
33
+ success_url: `${process.env.APP_URL}/checkout/success?session_id={CHECKOUT_SESSION_ID}`,
34
+ cancel_url: `${process.env.APP_URL}/checkout/cancel`,
35
+ expires_at: Math.floor(Date.now() / 1000) + 1800,
36
+ });
37
+
38
+ res.json({ url: session.url });
39
+ });
40
+ ```
41
+
42
+ ### Subscriptions with Stripe Billing
43
+
44
+ ```typescript
45
+ app.post("/api/subscribe", async (req, res) => {
46
+ let customerId = req.user.stripeCustomerId;
47
+ if (!customerId) {
48
+ const customer = await stripe.customers.create({
49
+ email: req.user.email,
50
+ metadata: { userId: req.user.id },
51
+ });
52
+ customerId = customer.id;
53
+ await db.users.update(req.user.id, { stripeCustomerId: customerId });
54
+ }
55
+
56
+ const session = await stripe.checkout.sessions.create({
57
+ mode: "subscription",
58
+ customer: customerId,
59
+ line_items: [{ price: req.body.stripePriceId, quantity: 1 }],
60
+ subscription_data: {
61
+ trial_period_days: 14,
62
+ metadata: { userId: req.user.id, plan: req.body.plan },
63
+ },
64
+ success_url: `${process.env.APP_URL}/dashboard?upgraded=true`,
65
+ cancel_url: `${process.env.APP_URL}/pricing`,
66
+ });
67
+
68
+ res.json({ url: session.url });
69
+ });
70
+
71
+ // Customer portal for self-service billing management
72
+ app.post("/api/billing/portal", async (req, res) => {
73
+ const session = await stripe.billingPortal.sessions.create({
74
+ customer: req.user.stripeCustomerId,
75
+ return_url: `${process.env.APP_URL}/settings/billing`,
76
+ });
77
+ res.json({ url: session.url });
78
+ });
79
+ ```
80
+
81
+ ### Webhook Handling -- Source of Truth
82
+
83
+ Webhooks are the source of truth for payment state. The success_url page is just UI:
84
+
85
+ ```typescript
86
+ app.post(
87
+ "/api/webhooks/stripe",
88
+ express.raw({ type: "application/json" }),
89
+ async (req, res) => {
90
+ const sig = req.headers["stripe-signature"] as string;
91
+ let event: Stripe.Event;
92
+
93
+ try {
94
+ event = stripe.webhooks.constructEvent(
95
+ req.body, sig, process.env.STRIPE_WEBHOOK_SECRET!
96
+ );
97
+ } catch {
98
+ return res.status(400).send("Invalid signature");
99
+ }
100
+
101
+ switch (event.type) {
102
+ case "checkout.session.completed": {
103
+ const session = event.data.object as Stripe.Checkout.Session;
104
+ await fulfillOrder(session);
105
+ break;
106
+ }
107
+ case "invoice.paid": {
108
+ const invoice = event.data.object as Stripe.Invoice;
109
+ await extendSubscription(invoice);
110
+ break;
111
+ }
112
+ case "invoice.payment_failed": {
113
+ const invoice = event.data.object as Stripe.Invoice;
114
+ await handlePaymentFailure(invoice);
115
+ break;
116
+ }
117
+ case "customer.subscription.deleted": {
118
+ const sub = event.data.object as Stripe.Subscription;
119
+ await revokeAccess(sub);
120
+ break;
121
+ }
122
+ }
123
+ res.json({ received: true });
124
+ }
125
+ );
126
+ ```
127
+
128
+ ### Idempotency
129
+
130
+ Payment operations must be safe to retry:
131
+
132
+ ```typescript
133
+ // Stripe idempotency key tied to business entity
134
+ const charge = await stripe.charges.create(
135
+ { amount: 2000, currency: "usd", customer: customerId, metadata: { orderId } },
136
+ { idempotencyKey: `charge-${orderId}` }
137
+ );
138
+
139
+ // Webhook deduplication
140
+ async function processWebhook(event: Stripe.Event): Promise<void> {
141
+ const acquired = await redis.set(`stripe:event:${event.id}`, "1", "NX", "EX", 86400);
142
+ if (!acquired) return; // duplicate
143
+ // Process event...
144
+ }
145
+
146
+ // Database-level idempotency for fulfillment
147
+ async function fulfillOrder(session: Stripe.Checkout.Session): Promise<void> {
148
+ const orderId = session.metadata!.orderId;
149
+ const result = await db.query(
150
+ `UPDATE orders SET status = 'fulfilled', stripe_session_id = $1
151
+ WHERE id = $2 AND status = 'pending'`,
152
+ [session.id, orderId]
153
+ );
154
+ if (result.rowCount === 0) return; // already fulfilled
155
+ await provisionAccess(orderId);
156
+ }
157
+ ```
158
+
159
+ ### Refunds
160
+
161
+ ```typescript
162
+ app.post("/api/orders/:id/refund", async (req, res) => {
163
+ const order = await db.orders.findById(req.params.id);
164
+ if (!order) return res.status(404).json({ error: "Order not found" });
165
+ if (order.userId !== req.user.id) return res.status(403).json({ error: "Forbidden" });
166
+
167
+ const daysSincePurchase = (Date.now() - order.createdAt.getTime()) / 86400000;
168
+ if (daysSincePurchase > 30) {
169
+ return res.status(400).json({ error: "Refund window closed (30 days)" });
170
+ }
171
+
172
+ const refund = await stripe.refunds.create(
173
+ {
174
+ payment_intent: order.stripePaymentIntentId,
175
+ amount: req.body.partial ? req.body.amountInCents : undefined,
176
+ reason: "requested_by_customer",
177
+ },
178
+ { idempotencyKey: `refund-${order.id}` }
179
+ );
180
+
181
+ await db.orders.update(order.id, {
182
+ status: refund.amount === order.totalCents ? "refunded" : "partially_refunded",
183
+ });
184
+ res.json({ refundId: refund.id, amount: refund.amount });
185
+ });
186
+ ```
187
+
188
+ ### PCI Compliance Essentials
189
+
190
+ ```
191
+ NEVER DO:
192
+ - Store card numbers, CVV, or full magnetic stripe data
193
+ - Build custom card input forms (use Stripe Elements or Checkout)
194
+ - Log raw payment API responses containing card data
195
+ - Send card data to your server (use client-side tokenization)
196
+
197
+ ALWAYS DO:
198
+ - Use Stripe Checkout or Elements (SAQ A compliance)
199
+ - Verify webhook signatures on every request
200
+ - Use HTTPS for all payment-related endpoints
201
+ - Store only Stripe customer/subscription/payment IDs
202
+ ```
203
+
204
+ ## Examples
205
+
206
+ | Scenario | Stripe Feature | Key Consideration |
207
+ |----------|---------------|-------------------|
208
+ | SaaS monthly plan | Checkout + Subscriptions | Dunning emails for failed payments |
209
+ | One-time purchase | Checkout Session | Fulfill via webhook only |
210
+ | Marketplace payouts | Stripe Connect | Destination charges for platform fee |
211
+ | Usage-based billing | Metered subscriptions | Report usage via Usage Records API |
212
+ | B2B invoicing | Invoices API | Net-30 terms, PDF generation |
213
+
214
+ ## Checklist
215
+ - [ ] Payments use Stripe Checkout or Elements for PCI compliance
216
+ - [ ] Webhook signature verified on every request
217
+ - [ ] Webhook route uses `express.raw()`, not `express.json()`
218
+ - [ ] All mutating Stripe calls include idempotency keys
219
+ - [ ] Fulfillment happens in webhook handlers, not success page
220
+ - [ ] Duplicate webhook events detected and skipped
221
+ - [ ] Amounts always in cents (integer), never dollars (float)
222
+ - [ ] Failed subscription payments trigger dunning sequence
@@ -0,0 +1,307 @@
1
+ ---
2
+ name: pdf-generation
3
+ description: PDF generation patterns with Puppeteer, jsPDF, server-side rendering from HTML templates, streaming, and batch processing.
4
+ ---
5
+
6
+ # PDF Generation Patterns
7
+
8
+ ## When to Use
9
+ Generate PDFs for invoices, reports, certificates, contracts, and any document that needs consistent formatting across devices. Use Puppeteer for pixel-perfect HTML-to-PDF conversion with full CSS support. Use jsPDF for lightweight client-side generation. Choose server-side rendering when you need consistent output regardless of client, and streaming for large documents that should not be held in memory.
10
+
11
+ ## How It Works
12
+
13
+ ### Puppeteer HTML-to-PDF
14
+
15
+ ```typescript
16
+ // src/pdf/puppeteer-generator.ts
17
+ import puppeteer, { type Browser } from 'puppeteer';
18
+
19
+ let browser: Browser | null = null;
20
+
21
+ async function getBrowser(): Promise<Browser> {
22
+ if (!browser) {
23
+ browser = await puppeteer.launch({
24
+ headless: true,
25
+ args: ['--no-sandbox', '--disable-setuid-sandbox', '--disable-gpu'],
26
+ });
27
+ }
28
+ return browser;
29
+ }
30
+
31
+ export async function generatePdfFromHtml(html: string, options?: {
32
+ format?: 'A4' | 'Letter';
33
+ landscape?: boolean;
34
+ margin?: { top: string; right: string; bottom: string; left: string };
35
+ headerTemplate?: string;
36
+ footerTemplate?: string;
37
+ }): Promise<Buffer> {
38
+ const browser = await getBrowser();
39
+ const page = await browser.newPage();
40
+
41
+ try {
42
+ await page.setContent(html, { waitUntil: 'networkidle0' });
43
+
44
+ const pdf = await page.pdf({
45
+ format: options?.format ?? 'A4',
46
+ landscape: options?.landscape ?? false,
47
+ printBackground: true,
48
+ margin: options?.margin ?? { top: '20mm', right: '15mm', bottom: '20mm', left: '15mm' },
49
+ displayHeaderFooter: !!(options?.headerTemplate || options?.footerTemplate),
50
+ headerTemplate: options?.headerTemplate ?? '',
51
+ footerTemplate: options?.footerTemplate ?? `
52
+ <div style="font-size:9px; width:100%; text-align:center; color:#666;">
53
+ Page <span class="pageNumber"></span> of <span class="totalPages"></span>
54
+ </div>
55
+ `,
56
+ });
57
+
58
+ return Buffer.from(pdf);
59
+ } finally {
60
+ await page.close();
61
+ }
62
+ }
63
+
64
+ // Cleanup on shutdown
65
+ process.on('SIGTERM', async () => {
66
+ if (browser) await browser.close();
67
+ });
68
+ ```
69
+
70
+ ### Invoice Template
71
+
72
+ ```typescript
73
+ // src/pdf/templates/invoice.ts
74
+ interface InvoiceData {
75
+ invoiceNumber: string;
76
+ date: string;
77
+ dueDate: string;
78
+ company: { name: string; address: string; email: string };
79
+ customer: { name: string; address: string; email: string };
80
+ items: Array<{ description: string; quantity: number; unitPrice: number }>;
81
+ taxRate: number;
82
+ notes?: string;
83
+ }
84
+
85
+ export function renderInvoiceHtml(data: InvoiceData): string {
86
+ const subtotal = data.items.reduce((sum, item) => sum + item.quantity * item.unitPrice, 0);
87
+ const tax = subtotal * data.taxRate;
88
+ const total = subtotal + tax;
89
+
90
+ return `<!DOCTYPE html>
91
+ <html>
92
+ <head>
93
+ <style>
94
+ body { font-family: 'Helvetica Neue', Arial, sans-serif; color: #333; margin: 0; padding: 40px; }
95
+ .header { display: flex; justify-content: space-between; margin-bottom: 40px; }
96
+ .company-name { font-size: 24px; font-weight: 700; color: #1a1a1a; }
97
+ .invoice-title { font-size: 32px; font-weight: 300; color: #666; text-align: right; }
98
+ .meta { display: grid; grid-template-columns: 1fr 1fr; gap: 20px; margin-bottom: 40px; }
99
+ .meta-label { font-size: 12px; text-transform: uppercase; color: #999; margin-bottom: 4px; }
100
+ table { width: 100%; border-collapse: collapse; margin-bottom: 30px; }
101
+ th { text-align: left; padding: 12px 8px; border-bottom: 2px solid #333; font-size: 12px; text-transform: uppercase; }
102
+ td { padding: 12px 8px; border-bottom: 1px solid #eee; }
103
+ .amount { text-align: right; }
104
+ .totals { width: 300px; margin-left: auto; }
105
+ .totals td { border: none; padding: 6px 8px; }
106
+ .totals .total { font-size: 18px; font-weight: 700; border-top: 2px solid #333; }
107
+ .notes { margin-top: 40px; padding: 20px; background: #f8f8f8; border-radius: 4px; font-size: 14px; }
108
+ </style>
109
+ </head>
110
+ <body>
111
+ <div class="header">
112
+ <div>
113
+ <div class="company-name">${data.company.name}</div>
114
+ <div>${data.company.address}</div>
115
+ <div>${data.company.email}</div>
116
+ </div>
117
+ <div class="invoice-title">INVOICE</div>
118
+ </div>
119
+
120
+ <div class="meta">
121
+ <div>
122
+ <div class="meta-label">Bill To</div>
123
+ <div><strong>${data.customer.name}</strong></div>
124
+ <div>${data.customer.address}</div>
125
+ <div>${data.customer.email}</div>
126
+ </div>
127
+ <div style="text-align: right;">
128
+ <div class="meta-label">Invoice #</div>
129
+ <div>${data.invoiceNumber}</div>
130
+ <div class="meta-label" style="margin-top: 8px;">Date</div>
131
+ <div>${data.date}</div>
132
+ <div class="meta-label" style="margin-top: 8px;">Due Date</div>
133
+ <div>${data.dueDate}</div>
134
+ </div>
135
+ </div>
136
+
137
+ <table>
138
+ <thead>
139
+ <tr><th>Description</th><th class="amount">Qty</th><th class="amount">Unit Price</th><th class="amount">Amount</th></tr>
140
+ </thead>
141
+ <tbody>
142
+ ${data.items.map((item) => `
143
+ <tr>
144
+ <td>${item.description}</td>
145
+ <td class="amount">${item.quantity}</td>
146
+ <td class="amount">$${item.unitPrice.toFixed(2)}</td>
147
+ <td class="amount">$${(item.quantity * item.unitPrice).toFixed(2)}</td>
148
+ </tr>
149
+ `).join('')}
150
+ </tbody>
151
+ </table>
152
+
153
+ <table class="totals">
154
+ <tr><td>Subtotal</td><td class="amount">$${subtotal.toFixed(2)}</td></tr>
155
+ <tr><td>Tax (${(data.taxRate * 100).toFixed(0)}%)</td><td class="amount">$${tax.toFixed(2)}</td></tr>
156
+ <tr class="total"><td>Total</td><td class="amount">$${total.toFixed(2)}</td></tr>
157
+ </table>
158
+
159
+ ${data.notes ? `<div class="notes"><strong>Notes:</strong> ${data.notes}</div>` : ''}
160
+ </body>
161
+ </html>`;
162
+ }
163
+ ```
164
+
165
+ ### jsPDF Client-Side Generation
166
+
167
+ ```typescript
168
+ // src/pdf/client-generator.ts
169
+ import jsPDF from 'jspdf';
170
+ import autoTable from 'jspdf-autotable';
171
+
172
+ export function generateReport(data: ReportData): jsPDF {
173
+ const doc = new jsPDF({ orientation: 'portrait', unit: 'mm', format: 'a4' });
174
+
175
+ // Header
176
+ doc.setFontSize(24);
177
+ doc.text('Monthly Report', 20, 30);
178
+
179
+ doc.setFontSize(12);
180
+ doc.setTextColor(100);
181
+ doc.text(`Generated: ${new Date().toLocaleDateString()}`, 20, 40);
182
+
183
+ // Summary section
184
+ doc.setFontSize(16);
185
+ doc.setTextColor(0);
186
+ doc.text('Summary', 20, 60);
187
+
188
+ doc.setFontSize(11);
189
+ doc.text(`Total Revenue: $${data.revenue.toLocaleString()}`, 20, 70);
190
+ doc.text(`Active Users: ${data.activeUsers.toLocaleString()}`, 20, 78);
191
+ doc.text(`Conversion Rate: ${(data.conversionRate * 100).toFixed(1)}%`, 20, 86);
192
+
193
+ // Data table
194
+ autoTable(doc, {
195
+ startY: 100,
196
+ head: [['Month', 'Revenue', 'Users', 'Conversion']],
197
+ body: data.monthly.map((m) => [
198
+ m.month,
199
+ `$${m.revenue.toLocaleString()}`,
200
+ m.users.toLocaleString(),
201
+ `${(m.conversion * 100).toFixed(1)}%`,
202
+ ]),
203
+ styles: { fontSize: 10, cellPadding: 4 },
204
+ headStyles: { fillColor: [37, 99, 235], textColor: 255 },
205
+ alternateRowStyles: { fillColor: [245, 247, 250] },
206
+ });
207
+
208
+ return doc;
209
+ }
210
+
211
+ // Download in browser
212
+ function downloadPdf(data: ReportData) {
213
+ const doc = generateReport(data);
214
+ doc.save('monthly-report.pdf');
215
+ }
216
+
217
+ // Get as blob for upload
218
+ async function uploadPdf(data: ReportData): Promise<void> {
219
+ const doc = generateReport(data);
220
+ const blob = doc.output('blob');
221
+ const formData = new FormData();
222
+ formData.append('file', blob, 'report.pdf');
223
+ await fetch('/api/upload', { method: 'POST', body: formData });
224
+ }
225
+ ```
226
+
227
+ ### Express PDF Endpoint with Streaming
228
+
229
+ ```typescript
230
+ // src/routes/pdf.ts
231
+ import { Router } from 'express';
232
+ import { generatePdfFromHtml } from '../pdf/puppeteer-generator';
233
+ import { renderInvoiceHtml } from '../pdf/templates/invoice';
234
+
235
+ const router = Router();
236
+
237
+ router.get('/api/invoices/:id/pdf', async (req, res) => {
238
+ try {
239
+ const invoice = await getInvoice(req.params.id);
240
+ if (!invoice) return res.status(404).json({ error: 'Invoice not found' });
241
+
242
+ const html = renderInvoiceHtml(invoice);
243
+ const pdf = await generatePdfFromHtml(html, {
244
+ footerTemplate: `<div style="font-size:8px;text-align:center;width:100%;color:#999;">
245
+ Invoice ${invoice.invoiceNumber} | Page <span class="pageNumber"></span>
246
+ </div>`,
247
+ });
248
+
249
+ res.set({
250
+ 'Content-Type': 'application/pdf',
251
+ 'Content-Disposition': `inline; filename="invoice-${invoice.invoiceNumber}.pdf"`,
252
+ 'Content-Length': String(pdf.length),
253
+ 'Cache-Control': 'private, max-age=3600',
254
+ });
255
+
256
+ res.send(pdf);
257
+ } catch (err) {
258
+ console.error('PDF generation failed:', err);
259
+ res.status(500).json({ error: 'PDF generation failed' });
260
+ }
261
+ });
262
+
263
+ // Batch PDF generation
264
+ router.post('/api/invoices/batch-pdf', async (req, res) => {
265
+ const { invoiceIds } = req.body;
266
+ const archiver = require('archiver');
267
+ const archive = archiver('zip');
268
+
269
+ res.set({
270
+ 'Content-Type': 'application/zip',
271
+ 'Content-Disposition': 'attachment; filename="invoices.zip"',
272
+ });
273
+
274
+ archive.pipe(res);
275
+
276
+ for (const id of invoiceIds) {
277
+ const invoice = await getInvoice(id);
278
+ if (!invoice) continue;
279
+ const html = renderInvoiceHtml(invoice);
280
+ const pdf = await generatePdfFromHtml(html);
281
+ archive.append(pdf, { name: `invoice-${invoice.invoiceNumber}.pdf` });
282
+ }
283
+
284
+ await archive.finalize();
285
+ });
286
+
287
+ export default router;
288
+ ```
289
+
290
+ ## Examples
291
+
292
+ | Method | Environment | CSS Support | Best For |
293
+ |--------|-------------|-------------|----------|
294
+ | Puppeteer | Server (Node.js) | Full (Chrome rendering) | Complex layouts, pixel-perfect |
295
+ | jsPDF | Client or server | None (manual layout) | Simple reports, tables |
296
+ | @react-pdf/renderer | Server or client | Subset (flexbox) | React-based templates |
297
+ | wkhtmltopdf | Server (binary) | Good (WebKit) | Legacy systems |
298
+
299
+ ## Checklist
300
+ - [ ] Puppeteer browser instance reused across requests (not launched per PDF)
301
+ - [ ] Templates use print-friendly CSS (no background colors unless `printBackground: true`)
302
+ - [ ] Page margins, headers, and footers configured with page numbers
303
+ - [ ] PDF Content-Disposition header set correctly (inline for preview, attachment for download)
304
+ - [ ] Large batch generations use streaming or queued processing
305
+ - [ ] Browser instance cleaned up on server shutdown
306
+ - [ ] Fonts embedded or using web-safe fallbacks for consistent rendering
307
+ - [ ] PDF generation errors return proper HTTP error responses