1 # Stripe Code Templates
2
3 ## Code Files
4
5 Filename: seed-products.ts (in the scripts directory)
6
7 ```ts
8 import { getUncachableStripeClient } from './stripeClient';
9
10 /**
11 * Script to create products and prices in Stripe
12 *
13 * This script is idempotent - it checks if products exist before creating them.
14 * You can run it multiple times safely.
15 *
16 * Modify this script to add the products and prices you need.
17 *
18 * Run with: npx tsx seed-products.ts
19 * Or after compiling: node seed-products.js
20 */
21 async function createProducts() {
22 try {
23 const stripe = await getUncachableStripeClient();
24
25 console.log('Creating products and prices in Stripe...');
26
27 // Check if Pro Plan product already exists
28 const existingProducts = await stripe.products.search({
29 query: "name:'Pro Plan' AND active:'true'"
30 });
31
32 if (existingProducts.data.length > 0) {
33 console.log('Pro Plan product already exists. Skipping creation.');
34 console.log(`Existing product ID: ${existingProducts.data[0].id}`);
35 return;
36 }
37
38 // Create a Pro Plan product
39 const proProduct = await stripe.products.create({
40 name: 'Pro Plan',
41 description: 'Professional subscription with advanced features',
42 });
43 console.log(`Created product: ${proProduct.name} (${proProduct.id})`);
44
45 // Create a monthly price for the Pro Plan
46 const proMonthlyPrice = await stripe.prices.create({
47 product: proProduct.id,
48 unit_amount: 2900, // $29.00
49 currency: 'usd',
50 recurring: {
51 interval: 'month',
52 },
53 });
54 console.log(`Created monthly price: $29.00/month (${proMonthlyPrice.id})`);
55
56 // Create a yearly price for the Pro Plan
57 const proYearlyPrice = await stripe.prices.create({
58 product: proProduct.id,
59 unit_amount: 29000, // $290.00 (save ~17%)
60 currency: 'usd',
61 recurring: {
62 interval: 'year',
63 },
64 });
65 console.log(`Created yearly price: $290.00/year (${proYearlyPrice.id})`);
66
67 console.log('✓ Products and prices created successfully!');
68 console.log('Webhooks will sync this data to your database automatically.');
69
70 } catch (error: any) {
71 console.error('Error creating products:', error.message);
72 process.exit(1);
73 }
74 }
75
76 // Run the function
77 createProducts();
78 ```
79
80 Filename: stripeClient.ts (in the API server directory; also copy to the scripts directory)
81
82 ```ts
83 import Stripe from 'stripe';
84 import { StripeSync } from 'stripe-replit-sync';
85
86 /**
87 * Fetches Stripe credentials from the Replit connection API.
88 * Not cached -- tokens can rotate, so fetch fresh each time.
89 */
90 async function getStripeCredentials(): Promise<{ secretKey: string; webhookSecret?: string }> {
91 const hostname = process.env.REPLIT_CONNECTORS_HOSTNAME;
92 const xReplitToken = process.env.REPL_IDENTITY
93 ? "repl " + process.env.REPL_IDENTITY
94 : process.env.WEB_REPL_RENEWAL
95 ? "depl " + process.env.WEB_REPL_RENEWAL
96 : null;
97
98 if (!hostname || !xReplitToken) {
99 throw new Error(
100 'Missing Replit environment variables. ' +
101 'Ensure the Stripe integration is connected via the Integrations tab.'
102 );
103 }
104
105 const resp = await fetch(
106 `https://${hostname}/api/v2/connection?include_secrets=true&connector_names=stripe`,
107 {
108 headers: { Accept: "application/json", X_REPLIT_TOKEN: xReplitToken },
109 signal: AbortSignal.timeout(10_000),
110 }
111 );
112
113 if (!resp.ok) {
114 throw new Error(`Failed to fetch Stripe credentials: ${resp.status} ${resp.statusText}`);
115 }
116
117 const data = await resp.json();
118 const settings = data.items?.[0]?.settings;
119
120 if (!settings?.secret_key) {
121 throw new Error(
122 'Stripe integration not connected or missing secret key. ' +
123 'Connect Stripe via the Integrations tab first.'
124 );
125 }
126
127 return {
128 secretKey: settings.secret_key,
129 webhookSecret: settings.webhook_secret,
130 };
131 }
132
133 /**
134 * Returns a fresh authenticated Stripe client.
135 * Not cached -- fetches credentials on every call so rotated keys are picked up.
136 */
137 export async function getUncachableStripeClient(): Promise<Stripe> {
138 const { secretKey } = await getStripeCredentials();
139 return new Stripe(secretKey);
140 }
141
142 /**
143 * Returns a fresh StripeSync instance for webhook processing and data sync.
144 * Not cached -- fetches credentials on every call so rotated keys are picked up.
145 */
146 export async function getStripeSync(): Promise<StripeSync> {
147 const databaseUrl = process.env.DATABASE_URL;
148 if (!databaseUrl) {
149 throw new Error('DATABASE_URL environment variable is required');
150 }
151
152 const { secretKey, webhookSecret } = await getStripeCredentials();
153 return new StripeSync({
154 poolConfig: { connectionString: databaseUrl },
155 stripeSecretKey: secretKey,
156 stripeWebhookSecret: webhookSecret ?? '',
157 });
158 }
159 ```
160
161 Filename: webhookHandlers.ts (in the API server directory)
162
163 ```ts
164 import { getStripeSync } from './stripeClient';
165
166 export class WebhookHandlers {
167 static async processWebhook(payload: Buffer, signature: string): Promise<void> {
168 // Validate payload is a Buffer
169 if (!Buffer.isBuffer(payload)) {
170 throw new Error(
171 'STRIPE WEBHOOK ERROR: Payload must be a Buffer. ' +
172 'Received type: ' + typeof payload + '. ' +
173 'This usually means express.json() parsed the body before reaching this handler. ' +
174 'FIX: Ensure webhook route is registered BEFORE app.use(express.json()).'
175 );
176 }
177
178 const sync = await getStripeSync();
179 await sync.processWebhook(payload, signature);
180 }
181 }
182 ```
183
184 Filename: stripeService.ts (in the API server directory)
185
186 ```ts
187 import { storage } from './storage';
188 import { getUncachableStripeClient } from './stripeClient';
189
190 /**
191 * StripeService: Handles direct Stripe API operations
192 * Pattern: Use Stripe client for write operations, storage for read operations
193 */
194 export class StripeService {
195 // Create customer in Stripe
196 async createCustomer(email: string, userId: string) {
197 const stripe = await getUncachableStripeClient();
198 return await stripe.customers.create({
199 email,
200 metadata: { userId },
201 });
202 }
203
204 // Create checkout session
205 async createCheckoutSession(customerId: string, priceId: string, successUrl: string, cancelUrl: string) {
206 const stripe = await getUncachableStripeClient();
207 return await stripe.checkout.sessions.create({
208 customer: customerId,
209 payment_method_types: ['card'],
210 line_items: [{ price: priceId, quantity: 1 }],
211 mode: 'subscription',
212 success_url: successUrl,
213 cancel_url: cancelUrl,
214 });
215 }
216
217 // Create customer portal session
218 async createCustomerPortalSession(customerId: string, returnUrl: string) {
219 const stripe = await getUncachableStripeClient();
220 return await stripe.billingPortal.sessions.create({
221 customer: customerId,
222 return_url: returnUrl,
223 });
224 }
225
226 // Read operations - delegate to storage (queries PostgreSQL)
227 async getProduct(productId: string) {
228 return await storage.getProduct(productId);
229 }
230
231 async getSubscription(subscriptionId: string) {
232 return await storage.getSubscription(subscriptionId);
233 }
234 }
235
236 export const stripeService = new StripeService();
237 ```
238
239 Filename: storage.ts (in the API server directory)
240
241 ```ts
242 import { users } from '@shared/schema';
243 import { eq, sql } from 'drizzle-orm';
244 import { db } from './db';
245
246 /**
247 * Storage: Query Stripe data from PostgreSQL
248 * Pattern: Simple SQL queries to stripe.* tables
249 */
250 export class Storage {
251 // Query Stripe data from stripe.products table
252 async getProduct(productId: string) {
253 const result = await db.execute(
254 sql`SELECT * FROM stripe.products WHERE id = ${productId}`
255 );
256 return result.rows[0] || null;
257 }
258
259 async listProducts(active = true, limit = 20, offset = 0) {
260 const result = await db.execute(
261 sql`SELECT * FROM stripe.products WHERE active = ${active} LIMIT ${limit} OFFSET ${offset}`
262 );
263 return result.rows;
264 }
265
266 // Get products with their prices (with proper pagination)
267 // This paginates on products first, then joins only active prices
268 async listProductsWithPrices(active = true, limit = 20, offset = 0) {
269 const result = await db.execute(
270 sql`
271 WITH paginated_products AS (
272 SELECT id, name, description, metadata, active
273 FROM stripe.products
274 WHERE active = ${active}
275 ORDER BY id
276 LIMIT ${limit} OFFSET ${offset}
277 )
278 SELECT
279 p.id as product_id,
280 p.name as product_name,
281 p.description as product_description,
282 p.active as product_active,
283 p.metadata as product_metadata,
284 pr.id as price_id,
285 pr.unit_amount,
286 pr.currency,
287 pr.recurring,
288 pr.active as price_active,
289 pr.metadata as price_metadata
290 FROM paginated_products p
291 LEFT JOIN stripe.prices pr ON pr.product = p.id AND pr.active = true
292 ORDER BY p.id, pr.unit_amount
293 `
294 );
295 return result.rows;
296 }
297
298 // Query Stripe data from stripe.prices table
299 async getPrice(priceId: string) {
300 const result = await db.execute(
301 sql`SELECT * FROM stripe.prices WHERE id = ${priceId}`
302 );
303 return result.rows[0] || null;
304 }
305
306 async listPrices(active = true, limit = 20, offset = 0) {
307 const result = await db.execute(
308 sql`SELECT * FROM stripe.prices WHERE active = ${active} LIMIT ${limit} OFFSET ${offset}`
309 );
310 return result.rows;
311 }
312
313 // Get prices for a specific product
314 // Note: productId should be the product's id from stripe.products table
315 async getPricesForProduct(productId: string) {
316 const result = await db.execute(
317 sql`SELECT * FROM stripe.prices WHERE product = ${productId} AND active = true`
318 );
319 return result.rows;
320 }
321
322 // Query subscription
323 async getSubscription(subscriptionId: string) {
324 const result = await db.execute(
325 sql`SELECT * FROM stripe.subscriptions WHERE id = ${subscriptionId}`
326 );
327 return result.rows[0] || null;
328 }
329
330 // User operations
331 async getUser(id: string) {
332 const [user] = await db.select().from(users).where(eq(users.id, id));
333 return user;
334 }
335
336 async updateUserStripeInfo(userId: string, stripeInfo: {
337 stripeCustomerId?: string;
338 stripeSubscriptionId?: string;
339 }) {
340 const [user] = await db.update(users).set(stripeInfo).where(eq(users.id, userId)).returning();
341 return user;
342 }
343 }
344
345 export const storage = new Storage();
346 ```
347
348 Filename: routes/index.ts (in the API server directory)
349
350 ```ts
351 import { Router, type IRouter } from 'express';
352 import stripeRouter from './stripe';
353
354 const router: IRouter = Router();
355
356 router.use(stripeRouter);
357
358 export default router;
359 ```
360
361 Filename: routes/stripe.ts (in the API server directory)
362
363 ```ts
364 import { Router, type IRouter } from 'express';
365 import { storage } from '../storage';
366 import { stripeService } from '../stripeService';
367
368 const router: IRouter = Router();
369
370 // Get user subscription
371 router.get('/subscription', async (req: any, res) => {
372 const user = await storage.getUser(req.user.id);
373 if (!user?.stripeSubscriptionId) {
374 return res.json({ subscription: null });
375 }
376
377 const subscription = await storage.getSubscription(user.stripeSubscriptionId);
378 res.json({ subscription });
379 });
380
381 // Create checkout session
382 router.post('/checkout', async (req: any, res) => {
383 const user = await storage.getUser(req.user.id);
384 const { priceId } = req.body;
385
386 // Create or get customer
387 let customerId = user.stripeCustomerId;
388 if (!customerId) {
389 const customer = await stripeService.createCustomer(user.email, user.id);
390 await storage.updateUserStripeInfo(user.id, { stripeCustomerId: customer.id });
391 customerId = customer.id;
392 }
393
394 // Create checkout session
395 const session = await stripeService.createCheckoutSession(
396 customerId,
397 priceId,
398 `${req.protocol}://${req.get('host')}/checkout/success`,
399 `${req.protocol}://${req.get('host')}/checkout/cancel`
400 );
401
402 res.json({ url: session.url });
403 });
404
405 // List products
406 router.get('/products', async (_req, res) => {
407 const products = await storage.listProducts();
408 res.json({ data: products });
409 });
410
411 // List products with prices (joined)
412 router.get('/products-with-prices', async (_req, res) => {
413 const rows = await storage.listProductsWithPrices();
414
415 // Group prices by product
416 const productsMap = new Map();
417 for (const row of rows) {
418 if (!productsMap.has(row.product_id)) {
419 productsMap.set(row.product_id, {
420 id: row.product_id,
421 name: row.product_name,
422 description: row.product_description,
423 active: row.product_active,
424 prices: []
425 });
426 }
427 if (row.price_id) {
428 productsMap.get(row.product_id).prices.push({
429 id: row.price_id,
430 unit_amount: row.unit_amount,
431 currency: row.currency,
432 recurring: row.recurring,
433 active: row.price_active,
434 });
435 }
436 }
437
438 res.json({ data: Array.from(productsMap.values()) });
439 });
440
441 // List prices
442 router.get('/prices', async (_req, res) => {
443 const prices = await storage.listPrices();
444 res.json({ data: prices });
445 });
446
447 // Get prices for a specific product
448 router.get('/products/:productId/prices', async (req, res) => {
449 const { productId } = req.params;
450
451 // Validate product exists (productId should be the id from stripe.products)
452 const product = await storage.getProduct(productId);
453 if (!product) {
454 return res.status(404).json({ error: 'Product not found' });
455 }
456
457 const prices = await storage.getPricesForProduct(productId);
458 res.json({ data: prices });
459 });
460
461 export default router;
462 ```
463
464 Filename: app.ts (in the API server directory)
465
466 ```ts
467 import express, { type Express } from "express";
468 import cors from "cors";
469 import router from "./routes";
470 import { WebhookHandlers } from "./webhookHandlers";
471
472 const app: Express = express();
473
474 // Register Stripe webhook route BEFORE other middleware
475 // This is critical - webhook needs raw Buffer, not parsed JSON
476 app.post(
477 '/api/stripe/webhook',
478 express.raw({ type: 'application/json' }),
479 async (req, res) => {
480 const signature = req.headers['stripe-signature'];
481
482 if (!signature) {
483 return res.status(400).json({ error: 'Missing stripe-signature' });
484 }
485
486 try {
487 const sig = Array.isArray(signature) ? signature[0] : signature;
488
489 // Validate that req.body is a Buffer (not parsed JSON)
490 if (!Buffer.isBuffer(req.body)) {
491 const errorMsg = 'STRIPE WEBHOOK ERROR: req.body is not a Buffer. ' +
492 'This means express.json() ran before this webhook route. ' +
493 'FIX: Move this webhook route registration BEFORE app.use(express.json()) in your code.';
494 console.error(errorMsg);
495 return res.status(500).json({ error: 'Webhook processing error' });
496 }
497
498 await WebhookHandlers.processWebhook(req.body as Buffer, sig);
499
500 res.status(200).json({ received: true });
501 } catch (error: any) {
502 console.error('Webhook error:', error.message);
503
504 // Log helpful error message if it's the common "payload must be Buffer" error
505 if (error.message && error.message.includes('payload must be provided as a string or a Buffer')) {
506 const helpfulMsg = 'STRIPE WEBHOOK ERROR: Payload is not a Buffer. ' +
507 'This usually means express.json() parsed the body before the webhook handler. ' +
508 'FIX: Ensure the webhook route is registered BEFORE app.use(express.json()).';
509 console.error(helpfulMsg);
510 }
511
512 res.status(400).json({ error: 'Webhook processing error' });
513 }
514 }
515 );
516
517 // Apply middleware after webhook route
518 app.use(cors());
519 app.use(express.json());
520 app.use(express.urlencoded({ extended: true }));
521
522 // Mount routes under /api prefix
523 app.use("/api", router);
524
525 export default app;
526 ```
527
528 Filename: index.ts (in the API server directory)
529
530 ```ts
531 import { runMigrations } from 'stripe-replit-sync';
532 import { getStripeSync } from "./stripeClient";
533 import app from "./app";
534
535 /**
536 * Initialize Stripe schema and sync data on startup
537 */
538 async function initStripe() {
539 const databaseUrl = process.env.DATABASE_URL;
540
541 if (!databaseUrl) {
542 throw new Error(
543 'DATABASE_URL environment variable is required for Stripe integration. ' +
544 'Please create a PostgreSQL database first.'
545 );
546 }
547
548 try {
549 console.log('Initializing Stripe schema...');
550 await runMigrations({
551 databaseUrl,
552 schema: 'stripe'
553 });
554 console.log('Stripe schema ready');
555
556 const stripeSync = await getStripeSync();
557
558 console.log('Setting up managed webhook...');
559 const webhookBaseUrl = `https://${process.env.REPLIT_DOMAINS?.split(',')[0]}`;
560 const webhookResult = await stripeSync.findOrCreateManagedWebhook(
561 `${webhookBaseUrl}/api/stripe/webhook`);
562 console.log('Webhook configured:', JSON.stringify(webhookResult?.webhook?.url || 'setup complete'));
563
564 console.log('Syncing Stripe data...');
565 stripeSync.syncBackfill()
566 .then(() => {
567 console.log('Stripe data synced');
568 })
569 .catch((err) => {
570 console.error('Error syncing Stripe data:', err);
571 });
572 } catch (error) {
573 console.error('Failed to initialize Stripe:', error);
574 throw error;
575 }
576 }
577
578 const port = Number(process.env.PORT);
579 if (!port) {
580 throw new Error('PORT environment variable is required');
581 }
582
583 await initStripe();
584
585 app.listen(port, () => {
586 console.log(`Server running on port ${port}`);
587 });
588 ```