copilot/create-zip-file-again
Stripe Code Templates
Code Files
Filename: seed-products.ts (in the scripts directory)
import { getUncachableStripeClient } from './stripeClient';
/**
* Script to create products and prices in Stripe
*
* This script is idempotent - it checks if products exist before creating them.
* You can run it multiple times safely.
*
* Modify this script to add the products and prices you need.
*
* Run with: npx tsx seed-products.ts
* Or after compiling: node seed-products.js
*/
async function createProducts() {
try {
const stripe = await getUncachableStripeClient();
console.log('Creating products and prices in Stripe...');
// Check if Pro Plan product already exists
const existingProducts = await stripe.products.search({
query: "name:'Pro Plan' AND active:'true'"
});
if (existingProducts.data.length > 0) {
console.log('Pro Plan product already exists. Skipping creation.');
console.log(`Existing product ID: ${existingProducts.data[0].id}`);
return;
}
// Create a Pro Plan product
const proProduct = await stripe.products.create({
name: 'Pro Plan',
description: 'Professional subscription with advanced features',
});
console.log(`Created product: ${proProduct.name} (${proProduct.id})`);
// Create a monthly price for the Pro Plan
const proMonthlyPrice = await stripe.prices.create({
product: proProduct.id,
unit_amount: 2900, // $29.00
currency: 'usd',
recurring: {
interval: 'month',
},
});
console.log(`Created monthly price: $29.00/month (${proMonthlyPrice.id})`);
// Create a yearly price for the Pro Plan
const proYearlyPrice = await stripe.prices.create({
product: proProduct.id,
unit_amount: 29000, // $290.00 (save ~17%)
currency: 'usd',
recurring: {
interval: 'year',
},
});
console.log(`Created yearly price: $290.00/year (${proYearlyPrice.id})`);
console.log('✓ Products and prices created successfully!');
console.log('Webhooks will sync this data to your database automatically.');
} catch (error: any) {
console.error('Error creating products:', error.message);
process.exit(1);
}
}
// Run the function
createProducts();
Filename: stripeClient.ts (in the API server directory; also copy to the scripts directory)
import Stripe from 'stripe';
import { StripeSync } from 'stripe-replit-sync';
/**
* Fetches Stripe credentials from the Replit connection API.
* Not cached -- tokens can rotate, so fetch fresh each time.
*/
async function getStripeCredentials(): Promise<{ secretKey: string; webhookSecret?: string }> {
const hostname = process.env.REPLIT_CONNECTORS_HOSTNAME;
const xReplitToken = process.env.REPL_IDENTITY
? "repl " + process.env.REPL_IDENTITY
: process.env.WEB_REPL_RENEWAL
? "depl " + process.env.WEB_REPL_RENEWAL
: null;
if (!hostname || !xReplitToken) {
throw new Error(
'Missing Replit environment variables. ' +
'Ensure the Stripe integration is connected via the Integrations tab.'
);
}
const resp = await fetch(
`https://${hostname}/api/v2/connection?include_secrets=true&connector_names=stripe`,
{
headers: { Accept: "application/json", X_REPLIT_TOKEN: xReplitToken },
signal: AbortSignal.timeout(10_000),
}
);
if (!resp.ok) {
throw new Error(`Failed to fetch Stripe credentials: ${resp.status} ${resp.statusText}`);
}
const data = await resp.json();
const settings = data.items?.[0]?.settings;
if (!settings?.secret_key) {
throw new Error(
'Stripe integration not connected or missing secret key. ' +
'Connect Stripe via the Integrations tab first.'
);
}
return {
secretKey: settings.secret_key,
webhookSecret: settings.webhook_secret,
};
}
/**
* Returns a fresh authenticated Stripe client.
* Not cached -- fetches credentials on every call so rotated keys are picked up.
*/
export async function getUncachableStripeClient(): Promise<Stripe> {
const { secretKey } = await getStripeCredentials();
return new Stripe(secretKey);
}
/**
* Returns a fresh StripeSync instance for webhook processing and data sync.
* Not cached -- fetches credentials on every call so rotated keys are picked up.
*/
export async function getStripeSync(): Promise<StripeSync> {
const databaseUrl = process.env.DATABASE_URL;
if (!databaseUrl) {
throw new Error('DATABASE_URL environment variable is required');
}
const { secretKey, webhookSecret } = await getStripeCredentials();
return new StripeSync({
poolConfig: { connectionString: databaseUrl },
stripeSecretKey: secretKey,
stripeWebhookSecret: webhookSecret ?? '',
});
}
Filename: webhookHandlers.ts (in the API server directory)
import { getStripeSync } from './stripeClient';
export class WebhookHandlers {
static async processWebhook(payload: Buffer, signature: string): Promise<void> {
// Validate payload is a Buffer
if (!Buffer.isBuffer(payload)) {
throw new Error(
'STRIPE WEBHOOK ERROR: Payload must be a Buffer. ' +
'Received type: ' + typeof payload + '. ' +
'This usually means express.json() parsed the body before reaching this handler. ' +
'FIX: Ensure webhook route is registered BEFORE app.use(express.json()).'
);
}
const sync = await getStripeSync();
await sync.processWebhook(payload, signature);
}
}
Filename: stripeService.ts (in the API server directory)
import { storage } from './storage';
import { getUncachableStripeClient } from './stripeClient';
/**
* StripeService: Handles direct Stripe API operations
* Pattern: Use Stripe client for write operations, storage for read operations
*/
export class StripeService {
// Create customer in Stripe
async createCustomer(email: string, userId: string) {
const stripe = await getUncachableStripeClient();
return await stripe.customers.create({
email,
metadata: { userId },
});
}
// Create checkout session
async createCheckoutSession(customerId: string, priceId: string, successUrl: string, cancelUrl: string) {
const stripe = await getUncachableStripeClient();
return await stripe.checkout.sessions.create({
customer: customerId,
payment_method_types: ['card'],
line_items: [{ price: priceId, quantity: 1 }],
mode: 'subscription',
success_url: successUrl,
cancel_url: cancelUrl,
});
}
// Create customer portal session
async createCustomerPortalSession(customerId: string, returnUrl: string) {
const stripe = await getUncachableStripeClient();
return await stripe.billingPortal.sessions.create({
customer: customerId,
return_url: returnUrl,
});
}
// Read operations - delegate to storage (queries PostgreSQL)
async getProduct(productId: string) {
return await storage.getProduct(productId);
}
async getSubscription(subscriptionId: string) {
return await storage.getSubscription(subscriptionId);
}
}
export const stripeService = new StripeService();
Filename: storage.ts (in the API server directory)
import { users } from '@shared/schema';
import { eq, sql } from 'drizzle-orm';
import { db } from './db';
/**
* Storage: Query Stripe data from PostgreSQL
* Pattern: Simple SQL queries to stripe.* tables
*/
export class Storage {
// Query Stripe data from stripe.products table
async getProduct(productId: string) {
const result = await db.execute(
sql`SELECT * FROM stripe.products WHERE id = ${productId}`
);
return result.rows[0] || null;
}
async listProducts(active = true, limit = 20, offset = 0) {
const result = await db.execute(
sql`SELECT * FROM stripe.products WHERE active = ${active} LIMIT ${limit} OFFSET ${offset}`
);
return result.rows;
}
// Get products with their prices (with proper pagination)
// This paginates on products first, then joins only active prices
async listProductsWithPrices(active = true, limit = 20, offset = 0) {
const result = await db.execute(
sql`
WITH paginated_products AS (
SELECT id, name, description, metadata, active
FROM stripe.products
WHERE active = ${active}
ORDER BY id
LIMIT ${limit} OFFSET ${offset}
)
SELECT
p.id as product_id,
p.name as product_name,
p.description as product_description,
p.active as product_active,
p.metadata as product_metadata,
pr.id as price_id,
pr.unit_amount,
pr.currency,
pr.recurring,
pr.active as price_active,
pr.metadata as price_metadata
FROM paginated_products p
LEFT JOIN stripe.prices pr ON pr.product = p.id AND pr.active = true
ORDER BY p.id, pr.unit_amount
`
);
return result.rows;
}
// Query Stripe data from stripe.prices table
async getPrice(priceId: string) {
const result = await db.execute(
sql`SELECT * FROM stripe.prices WHERE id = ${priceId}`
);
return result.rows[0] || null;
}
async listPrices(active = true, limit = 20, offset = 0) {
const result = await db.execute(
sql`SELECT * FROM stripe.prices WHERE active = ${active} LIMIT ${limit} OFFSET ${offset}`
);
return result.rows;
}
// Get prices for a specific product
// Note: productId should be the product's id from stripe.products table
async getPricesForProduct(productId: string) {
const result = await db.execute(
sql`SELECT * FROM stripe.prices WHERE product = ${productId} AND active = true`
);
return result.rows;
}
// Query subscription
async getSubscription(subscriptionId: string) {
const result = await db.execute(
sql`SELECT * FROM stripe.subscriptions WHERE id = ${subscriptionId}`
);
return result.rows[0] || null;
}
// User operations
async getUser(id: string) {
const [user] = await db.select().from(users).where(eq(users.id, id));
return user;
}
async updateUserStripeInfo(userId: string, stripeInfo: {
stripeCustomerId?: string;
stripeSubscriptionId?: string;
}) {
const [user] = await db.update(users).set(stripeInfo).where(eq(users.id, userId)).returning();
return user;
}
}
export const storage = new Storage();
Filename: routes/index.ts (in the API server directory)
import { Router, type IRouter } from 'express';
import stripeRouter from './stripe';
const router: IRouter = Router();
router.use(stripeRouter);
export default router;
Filename: routes/stripe.ts (in the API server directory)
import { Router, type IRouter } from 'express';
import { storage } from '../storage';
import { stripeService } from '../stripeService';
const router: IRouter = Router();
// Get user subscription
router.get('/subscription', async (req: any, res) => {
const user = await storage.getUser(req.user.id);
if (!user?.stripeSubscriptionId) {
return res.json({ subscription: null });
}
const subscription = await storage.getSubscription(user.stripeSubscriptionId);
res.json({ subscription });
});
// Create checkout session
router.post('/checkout', async (req: any, res) => {
const user = await storage.getUser(req.user.id);
const { priceId } = req.body;
// Create or get customer
let customerId = user.stripeCustomerId;
if (!customerId) {
const customer = await stripeService.createCustomer(user.email, user.id);
await storage.updateUserStripeInfo(user.id, { stripeCustomerId: customer.id });
customerId = customer.id;
}
// Create checkout session
const session = await stripeService.createCheckoutSession(
customerId,
priceId,
`${req.protocol}://${req.get('host')}/checkout/success`,
`${req.protocol}://${req.get('host')}/checkout/cancel`
);
res.json({ url: session.url });
});
// List products
router.get('/products', async (_req, res) => {
const products = await storage.listProducts();
res.json({ data: products });
});
// List products with prices (joined)
router.get('/products-with-prices', async (_req, res) => {
const rows = await storage.listProductsWithPrices();
// Group prices by product
const productsMap = new Map();
for (const row of rows) {
if (!productsMap.has(row.product_id)) {
productsMap.set(row.product_id, {
id: row.product_id,
name: row.product_name,
description: row.product_description,
active: row.product_active,
prices: []
});
}
if (row.price_id) {
productsMap.get(row.product_id).prices.push({
id: row.price_id,
unit_amount: row.unit_amount,
currency: row.currency,
recurring: row.recurring,
active: row.price_active,
});
}
}
res.json({ data: Array.from(productsMap.values()) });
});
// List prices
router.get('/prices', async (_req, res) => {
const prices = await storage.listPrices();
res.json({ data: prices });
});
// Get prices for a specific product
router.get('/products/:productId/prices', async (req, res) => {
const { productId } = req.params;
// Validate product exists (productId should be the id from stripe.products)
const product = await storage.getProduct(productId);
if (!product) {
return res.status(404).json({ error: 'Product not found' });
}
const prices = await storage.getPricesForProduct(productId);
res.json({ data: prices });
});
export default router;
Filename: app.ts (in the API server directory)
import express, { type Express } from "express";
import cors from "cors";
import router from "./routes";
import { WebhookHandlers } from "./webhookHandlers";
const app: Express = express();
// Register Stripe webhook route BEFORE other middleware
// This is critical - webhook needs raw Buffer, not parsed JSON
app.post(
'/api/stripe/webhook',
express.raw({ type: 'application/json' }),
async (req, res) => {
const signature = req.headers['stripe-signature'];
if (!signature) {
return res.status(400).json({ error: 'Missing stripe-signature' });
}
try {
const sig = Array.isArray(signature) ? signature[0] : signature;
// Validate that req.body is a Buffer (not parsed JSON)
if (!Buffer.isBuffer(req.body)) {
const errorMsg = 'STRIPE WEBHOOK ERROR: req.body is not a Buffer. ' +
'This means express.json() ran before this webhook route. ' +
'FIX: Move this webhook route registration BEFORE app.use(express.json()) in your code.';
console.error(errorMsg);
return res.status(500).json({ error: 'Webhook processing error' });
}
await WebhookHandlers.processWebhook(req.body as Buffer, sig);
res.status(200).json({ received: true });
} catch (error: any) {
console.error('Webhook error:', error.message);
// Log helpful error message if it's the common "payload must be Buffer" error
if (error.message && error.message.includes('payload must be provided as a string or a Buffer')) {
const helpfulMsg = 'STRIPE WEBHOOK ERROR: Payload is not a Buffer. ' +
'This usually means express.json() parsed the body before the webhook handler. ' +
'FIX: Ensure the webhook route is registered BEFORE app.use(express.json()).';
console.error(helpfulMsg);
}
res.status(400).json({ error: 'Webhook processing error' });
}
}
);
// Apply middleware after webhook route
app.use(cors());
app.use(express.json());
app.use(express.urlencoded({ extended: true }));
// Mount routes under /api prefix
app.use("/api", router);
export default app;
Filename: index.ts (in the API server directory)
import { runMigrations } from 'stripe-replit-sync';
import { getStripeSync } from "./stripeClient";
import app from "./app";
/**
* Initialize Stripe schema and sync data on startup
*/
async function initStripe() {
const databaseUrl = process.env.DATABASE_URL;
if (!databaseUrl) {
throw new Error(
'DATABASE_URL environment variable is required for Stripe integration. ' +
'Please create a PostgreSQL database first.'
);
}
try {
console.log('Initializing Stripe schema...');
await runMigrations({
databaseUrl,
schema: 'stripe'
});
console.log('Stripe schema ready');
const stripeSync = await getStripeSync();
console.log('Setting up managed webhook...');
const webhookBaseUrl = `https://${process.env.REPLIT_DOMAINS?.split(',')[0]}`;
const webhookResult = await stripeSync.findOrCreateManagedWebhook(
`${webhookBaseUrl}/api/stripe/webhook`);
console.log('Webhook configured:', JSON.stringify(webhookResult?.webhook?.url || 'setup complete'));
console.log('Syncing Stripe data...');
stripeSync.syncBackfill()
.then(() => {
console.log('Stripe data synced');
})
.catch((err) => {
console.error('Error syncing Stripe data:', err);
});
} catch (error) {
console.error('Failed to initialize Stripe:', error);
throw error;
}
}
const port = Number(process.env.PORT);
if (!port) {
throw new Error('PORT environment variable is required');
}
await initStripe();
app.listen(port, () => {
console.log(`Server running on port ${port}`);
});