Guidelines for using Stripe to integrate payments in mobile apps and any subsequent CRUD operations related to any Stripe entities
Replit offers a native integration with Stripe that allows users to implement payments in their applications
These packages must be installed in the workspace root package.json:
stripe - Official Stripe SDK for API operationsstripe-replit-sync - Handles webhook processing and database sync. Documentation: https://www.npmjs.com/package/stripe-replit-syncIf they are not installed, use npm to install them: $ cd /home/runner/workspace && npm install <packages>
The Stripe integration must be connected to the repl. You can do this by proposing the integration. Reference the integrations skill if necessary.
Once connected, create stripeClient.ts using the template from the code-templates reference -- this file fetches Stripe credentials from the Replit connection API and provides the authenticated client via .
getUncachableStripeClient()Create it in the server/ directory.
Ensure a PostgreSQL database exists. If you don't have one yet, use the tool to create a PostgreSQL database. Never use memory database for storing Stripe data.
You are required to ensure these requirements are met before setting up or using Stripe.
scripts/ at the workspace root (e.g., scripts/seed-products.ts)npx tsx scripts/<script>.tsserver/client/Reference files use these terms. Map them to these concrete paths.
Ensure prerequisites are met. Do not proceed until you have done so.
Follow these steps in order when implementing Stripe integration for the user. Create a task list to track implementation progress.
Read the code-templates reference file for full code file templates to use during these steps.
Set Up Database
Create the users table with Stripe ID references:
CREATE TABLE users (
id TEXT PRIMARY KEY,
email TEXT,
stripe_customer_id TEXT,
stripe_subscription_id TEXT,
created_at TIMESTAMP DEFAULT NOW()
);
Important: stripe-replit-sync creates the stripe schema automatically. Create application tables (users, orders, etc.) in the public schema, not in the stripe schema.
Create Webhook Handler
In the API server create webhookHandlers.ts - use the template provided in the code-templates reference. It handles webhook processing by calling getStripeSync() from stripeClient.
Initialize Stripe on Startup
Add this initialization function to the API server's index.ts. Important order:
import { runMigrations } from 'stripe-replit-sync';
import { getStripeSync } from './stripeClient';
async function initStripe() {
const databaseUrl = process.env.DATABASE_URL;
if (!databaseUrl) throw new Error('DATABASE_URL required');
// 1. Create stripe schema and tables
await runMigrations({ databaseUrl });
// 2. Get StripeSync instance (needed for webhook setup and backfill)
const stripeSync = await getStripeSync();
// 3. Set up managed webhook
const webhookBaseUrl = `https://${process.env.REPLIT_DOMAINS?.split(',')[0]}`;
const { webhook } = await stripeSync.findOrCreateManagedWebhook(
`${webhookBaseUrl}/api/stripe/webhook`
);
// 4. Sync all existing Stripe data
await stripeSync.syncBackfill();
}
await initStripe();
Register Webhook Route in index.ts
Critical: Register the webhook route BEFORE express.json() middleware. See the indexTemplate for the correct pattern:
/api/stripe/webhookexpress.raw() to get Buffer in req.bodyapp.use(express.json())stripe schema automatically - DO NOT create any tables in the stripe schemastripe_customer_id TEXT, stripe_subscription_id TEXT)runMigrations() creates the stripe schema and all Stripe tables (idempotent, safe to run on every startup)syncBackfill() syncs ALL existing Stripe data from Stripe API to the local PostgreSQL database on startupstripe-replit-syncstripe schema (fast, no API calls needed)Do NOT Create Product Tables or Maintain Any Other Source of Truth for Stripe Data
Bad -- creates duplicate product storage:
export const products = pgTable("products", {
id: varchar("id").primaryKey(),
name: text("name"),
price: integer("price"),
// ... custom fields
});
Correct Approach:
stripe.products and stripe.pricesNEVER insert data directly into the stripe schema tables. Instead:
stripe schema tables using standard SQL queriesCreate products in Stripe with metadata (use API or script):
await stripe.products.create({
name: "Product Name",
description: "...",
images: ["https://..."],
metadata: {
category: "electronics",
customField1: "value1",
featured: "true",
}
});
await stripe.prices.create({
product: product.id,
unit_amount: 9999,
currency: 'usd',
});
Query from synced stripe tables:
const result = await db.execute(sql`
SELECT
p.id,
p.name,
p.metadata,
pr.id as price_id,
pr.unit_amount
FROM stripe.products p
JOIN stripe.prices pr ON pr.product = p.id
WHERE p.active = true
`);
Use real Stripe price IDs in checkout:
// CORRECT
{ price: "price_1ABC...", quantity: 1 }
// WRONG - Never use price_data
{
price_data: {
unit_amount: 9999,
currency: 'usd',
product_data: { name: "..." }
}
}
stripe.products/stripe.pricesstripe.customersstripe.subscriptionsKey Principle: "If it exists in Stripe, it belongs in Stripe"
Creating products:
Write a seed script that calls Stripe API (stripe.products.create(), stripe.prices.create()). Run this script in development to create your products. Webhooks automatically sync them to the database. Replit handles deployment automatically.
Workflow:
# Development:
# - Run seed script to create products in Stripe (test mode)
# - syncBackfill() syncs them to local database
# Deployment (handled by Replit):
# - Replit copies products/prices from dev Stripe to prod Stripe
# - Your code runs unchanged, syncBackfill() syncs prod database
# Production:
# - Modify products via Stripe Dashboard (scripts can't run in deployments)
# - Webhooks automatically sync changes to database
To publish the app with working Stripe payments:
pk_live_ and sk_live_)Placeholder Keys (User-Requested Only):
Only offer placeholder keys if the user explicitly asks to publish without live Stripe keys and understands the consequences. Do NOT use these for any other purpose.
If the user confirms they want to proceed without live keys, suggest the following values to them:
pk_live_abcdefsk_live_abcdefConsequences the user must understand:
To remove the Stripe integration from the project:
DO:
stripe schema tables (products, prices, customers, subscriptions, etc.)stripe_customer_id TEXT)processWebhook with payload and signature)syncBackfill() sync existing Stripe data on startupexpress.json() middlewareABSOLUTELY DO NOT:
stripe schema - stripe-replit-sync manages this automaticallystripe schema tables - only query from themprocessWebhookrunMigrations() or syncBackfill()runMigrations()Database - DO NOT create Stripe tables:
-- WRONG - Creating Stripe tables manually
CREATE TABLE products (
id TEXT PRIMARY KEY,
name TEXT,
price INTEGER
);
-- CORRECT - Create application tables, store Stripe IDs
CREATE TABLE users (
id TEXT PRIMARY KEY,
stripe_customer_id TEXT
);
-- stripe-replit-sync creates stripe.products, stripe.prices, etc. automatically
Data Creation - Use Stripe API Scripts, NOT SQL:
-- WRONG - Inserting Stripe data with SQL
INSERT INTO stripe.products (id, name, description)
VALUES ('prod_123', 'Pro Plan', 'Professional subscription');
// CORRECT - Use Stripe API in a script
const stripe = await getUncachableStripeClient();
const product = await stripe.products.create({
name: 'Pro Plan',
description: 'Professional subscription',
});
// Webhook syncs this to stripe.products automatically
Initialization Order:
// WRONG - Creating StripeSync before migrations
const stripeSync = await getStripeSync();
await runMigrations({ databaseUrl });
// CORRECT - Migrations first, then StripeSync
await runMigrations({ databaseUrl });
const stripeSync = await getStripeSync();
await stripeSync.findOrCreateManagedWebhook(...);
await stripeSync.syncBackfill();
Frontend - Must parse JSON responses:
// WRONG - returns Response object
const response = await apiRequest('POST', '/api/checkout', { priceId });
return response;
// CORRECT - parse to get data
const response = await apiRequest('POST', '/api/checkout', { priceId });
return await response.json();
Backend - Webhook route ordering:
// WRONG - Webhook after express.json()
app.use(express.json());
app.post('/api/stripe/webhook', ...); // Too late! Body already parsed
// CORRECT - Webhook BEFORE express.json()
app.post('/api/stripe/webhook', express.raw({ type: 'application/json' }),
async (req, res) => {
await WebhookHandlers.processWebhook(req.body, sig);
}
);
app.use(express.json()); // Now apply to other routes
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 signature' });
const sig = Array.isArray(signature) ? signature[0] : signature;
await WebhookHandlers.processWebhook(req.body as Buffer, sig);
res.status(200).json({ received: true });
}
);
app.use(express.json()); // Now apply to other routes
Create Storage Layer
Create storage.ts - use the template provided in the code-templates reference. Query Stripe data from PostgreSQL stripe schema using standard SQL queries.
Set Up Other Routes
Create routes.ts - use the template provided in the code-templates reference for products, prices, checkout, etc. These routes are registered AFTER express.json() so they get parsed JSON.
Create Products Script (Recommended)
Create seed-products.ts - a script to add products and prices to Stripe via the API. This is the recommended way to create products. Run it manually when creating products.
Use the seed-products template from the code-templates reference as a starting point:
node seed-products.jsimport { getUncachableStripeClient } from './stripeClient';
async function createProducts() {
const stripe = await getUncachableStripeClient();
// Create product
const product = await stripe.products.create({
name: 'Pro Plan',
description: 'Professional subscription',
});
// Create prices for the product
const monthlyPrice = await stripe.prices.create({
product: product.id,
unit_amount: 2900, // $29.00
currency: 'usd',
recurring: { interval: 'month' },
});
console.log('Created:', product.id, monthlyPrice.id);
}
createProducts();
Usage: Run this script in development when you need to create or add products.
Optional idempotency: To make the script safe to run multiple times, check if specific products exist first:
const products = await stripe.products.search({ query: "name:'Pro Plan'" });
if (products.data.length > 0) {
console.log('Pro Plan already exists');
return;
}