Skip to main content

Overview

Deltalytix uses Stripe for secure payment processing and subscription management. This integration handles both recurring subscriptions and one-time lifetime purchases.

Configuration

Environment Variables

Add the following variables to your .env file:
# Stripe Configuration
NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY='pk_test_...'
STRIPE_SECRET_KEY='sk_test_...'
STRIPE_WEBHOOK_SECRET='whsec_...'
Never commit your Stripe secret keys to version control. Always use environment variables.

Stripe Client Initialization

The Stripe client is initialized in server/stripe.ts:
server/stripe.ts
import Stripe from "stripe";

export const stripe = new Stripe(process.env.STRIPE_SECRET_KEY as string, {
  apiVersion: "2025-10-29.clover",
});

Subscription Management

Subscription Types

Deltalytix supports two subscription models:
  1. Recurring Subscriptions - Monthly, quarterly, or yearly billing
  2. Lifetime Plans - One-time payment with permanent access

Creating Checkout Sessions

Checkout sessions are created in app/api/stripe/create-checkout-session/route.ts:
// Fetch the price by lookup key
const prices = await stripe.prices.list({
  lookup_keys: [lookup_key],
  expand: ['data.product'],
});

const price = prices.data[0];
const isLifetimePlan = price.type === 'one_time';

// Configure session based on plan type
const sessionConfig: any = {
  customer: customerId,
  metadata: {
    plan: lookup_key,
    ...(referral && { referral_code: referral }),
  },
  line_items: [
    {
      price: price.id,
      quantity: 1,
    },
  ],
  success_url: `${websiteURL}dashboard?success=true`,
  cancel_url: `${websiteURL}pricing?canceled=true`,
  allow_promotion_codes: true,
};

if (isLifetimePlan) {
  sessionConfig.mode = 'payment';
} else {
  sessionConfig.mode = 'subscription';
}

const session = await stripe.checkout.sessions.create(sessionConfig);
The system automatically detects if a customer already exists and reuses their customer ID to maintain payment history.

Retrieving Subscription Data

The getSubscriptionData() function in server/billing.ts handles subscription retrieval with this priority:
  1. Local database lifetime subscriptions (highest priority)
  2. Active Stripe subscriptions
  3. Trialing Stripe subscriptions
export async function getSubscriptionData() {
  const supabase = await createClient();
  const { data: { user } } = await supabase.auth.getUser();
  
  if (!user?.email) throw new Error('User not found');

  // Check local database for lifetime subscription
  const localSubscription = await prisma.subscription.findUnique({
    where: { email: user.email },
  });

  if (localSubscription && 
      localSubscription.status === 'ACTIVE' && 
      localSubscription.interval === 'lifetime') {
    return createLifetimeSubscriptionData(localSubscription, invoices.data);
  }

  // Check Stripe for active subscriptions
  const customers = await stripe.customers.list({
    email: user.email,
    limit: 1,
  });

  const subscriptions = await stripe.subscriptions.list({
    customer: customer.id,
    status: 'active',
    expand: ['data.plan', 'data.items.data.price', 'data.discounts.coupon'],
    limit: 1,
  });

  // Return subscription data with invoices
  return {
    id: subscription.id,
    status: subscription.status,
    current_period_end: getCurrentPeriodEnd(subscription),
    plan: {
      id: price.id,
      name: subscriptionPlan,
      amount: price.unit_amount || 0,
      interval: price.recurring?.interval || 'month',
    },
    invoices: invoices.data,
  };
}

Updating Subscriptions

Users can pause, resume, or cancel their subscriptions:
export async function updateSubscription(
  action: 'pause' | 'resume' | 'cancel',
  subscriptionId: string
) {
  if (action === 'pause' || action === 'cancel') {
    await stripe.subscriptions.update(subscriptionId, {
      cancel_at_period_end: true
    });
  } else if (action === 'resume') {
    await stripe.subscriptions.update(subscriptionId, {
      cancel_at_period_end: false
    });
  }
  return { success: true };
}

Switching Plans

The switchSubscriptionPlan() function handles plan upgrades and downgrades:
export async function switchSubscriptionPlan(newLookupKey: string) {
  // Get new price
  const prices = await stripe.prices.list({
    lookup_keys: [newLookupKey],
    expand: ['data.product'],
  });

  const newPrice = prices.data[0];

  // Update subscription with prorating
  const updatedSubscription = await stripe.subscriptions.update(
    currentSubscription.id,
    {
      items: [{
        id: currentSubscriptionItem.id,
        price: newPrice.id,
      }],
      proration_behavior: 'create_prorations',
    }
  );

  return { success: true, subscription: updatedSubscription };
}

Webhook Handling

Webhook Endpoint

Webhooks are handled in app/api/stripe/webhooks/route.ts. This endpoint receives real-time events from Stripe.

Webhook Signature Verification

Always verify webhook signatures to ensure requests come from Stripe:
export async function POST(req: Request) {
  let event: Stripe.Event;
  
  try {
    event = stripe.webhooks.constructEvent(
      await (await req.blob()).text(),
      req.headers.get("stripe-signature") as string,
      process.env.STRIPE_WEBHOOK_SECRET as string,
    );
  } catch (err) {
    return NextResponse.json(
      { message: `Webhook Error: ${err}` },
      { status: 400 },
    );
  }

  // Process event
  console.log("✅ Success:", event.id);
}
Never process webhook events without signature verification. This protects against malicious requests.

Supported Events

Deltalytix handles the following Stripe events:
const permittedEvents: string[] = [
  "checkout.session.completed",
  "payment_intent.succeeded",
  "payment_intent.payment_failed",
  "customer.subscription.deleted",
  "customer.subscription.updated",
  "customer.subscription.created",
  "invoice.payment_failed",
  "customer.subscription.trial_will_end"
];

Event Handler Examples

Checkout Session Completed

case "checkout.session.completed":
  data = event.data.object as Stripe.Checkout.Session;

  if (data.mode === 'subscription' && data.subscription) {
    // Handle recurring subscription
    const subscription = await stripe.subscriptions.retrieve(
      data.subscription as string
    );

    const priceId = subscriptionItems.data[0]?.price.id;
    const price = await stripe.prices.retrieve(priceId, {
      expand: ['product'],
    });

    const productName = (price.product as Stripe.Product).name;
    const subscriptionPlan = productName.toUpperCase();
    const interval = price.recurring?.interval_count === 3 
      ? 'quarter' 
      : price.recurring?.interval || 'month';

    await prisma.subscription.upsert({
      where: { email: data.customer_details?.email as string },
      update: {
        plan: subscriptionPlan,
        endDate: new Date(currentPeriodEnd * 1000),
        status: 'ACTIVE',
        interval: interval,
      },
      create: {
        email: data.customer_details?.email as string,
        plan: subscriptionPlan,
        endDate: new Date(currentPeriodEnd * 1000),
        status: 'ACTIVE',
        interval: interval,
      }
    });
  } else if (data.mode === 'payment') {
    // Handle one-time payment (lifetime plan)
    const lifetimeEndDate = new Date();
    lifetimeEndDate.setFullYear(lifetimeEndDate.getFullYear() + 100);

    await prisma.subscription.upsert({
      where: { email: data.customer_details?.email as string },
      update: {
        plan: subscriptionPlan,
        endDate: lifetimeEndDate,
        status: 'ACTIVE',
        interval: 'lifetime',
      },
      create: {
        email: data.customer_details?.email as string,
        plan: subscriptionPlan,
        endDate: lifetimeEndDate,
        status: 'ACTIVE',
        interval: 'lifetime',
      }
    });
  }
  break;

Subscription Updated

case "customer.subscription.updated":
  data = event.data.object as Stripe.Subscription;
  const updatedCustomerData = await stripe.customers.retrieve(
    data.customer as string
  ) as Stripe.Customer;

  if (data.cancel_at_period_end) {
    // Subscription scheduled for cancellation
    await prisma.subscription.update({
      where: { email: updatedCustomerData.email },
      data: {
        status: "SCHEDULED_CANCELLATION",
        endDate: new Date(currentPeriodEnd * 1000)
      }
    });

    // Save cancellation feedback if provided
    const cancellationDetails = 
      data.cancellation_details as Stripe.Subscription.CancellationDetails;

    if (cancellationDetails?.feedback || cancellationDetails?.comment) {
      await prisma.subscriptionFeedback.create({
        data: {
          email: updatedCustomerData.email,
          event: "SCHEDULED_CANCELLATION",
          cancellationReason: cancellationDetails.feedback || null,
          feedback: cancellationDetails.comment || null
        }
      });
    }
  }
  break;

Subscription Deleted

case "customer.subscription.deleted":
  data = event.data.object as Stripe.Subscription;
  const customerData = await stripe.customers.retrieve(
    data.customer as string
  ) as Stripe.Customer;

  if (customerData.email) {
    await prisma.subscription.update({
      where: { email: customerData.email },
      data: {
        plan: 'FREE',
        status: "CANCELLED",
        endDate: new Date(data.ended_at! * 1000)
      }
    });
  }
  break;

Payment Failed

case "invoice.payment_failed":
  data = event.data.object as Stripe.Invoice;
  const customerEmail = (await stripe.customers.retrieve(
    data.customer as string
  ) as Stripe.Customer).email;

  if (customerEmail) {
    await prisma.subscription.update({
      where: { email: customerEmail },
      data: { status: "PAYMENT_FAILED" }
    });
  }
  break;

Security Best Practices

API Key Security

Store all Stripe keys in environment variables, never in code:
STRIPE_SECRET_KEY='sk_live_...'
STRIPE_WEBHOOK_SECRET='whsec_...'
Use different keys for development and production:
# Development
STRIPE_SECRET_KEY='sk_test_...'

# Production
STRIPE_SECRET_KEY='sk_live_...'
Create restricted API keys in the Stripe Dashboard with only the permissions you need.

Webhook Security

  1. Always verify webhook signatures using stripe.webhooks.constructEvent()
  2. Use HTTPS for your webhook endpoint in production
  3. Implement idempotency to handle duplicate events
  4. Log all webhook events for debugging and audit trails

PCI Compliance

Deltalytix never handles raw card data. Stripe Checkout handles all payment information, keeping your application PCI compliant.

Testing

Test Mode

Use Stripe test mode during development:
# Test keys start with sk_test_ and pk_test_
NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY='pk_test_...'
STRIPE_SECRET_KEY='sk_test_...'

Webhook Testing

Test webhooks locally using the Stripe CLI:
# Install Stripe CLI
brew install stripe/stripe-cli/stripe

# Login to your Stripe account
stripe login

# Forward webhooks to your local server
stripe listen --forward-to localhost:3000/api/stripe/webhooks

# Trigger test events
stripe trigger checkout.session.completed
stripe trigger customer.subscription.updated

Test Cards

Use Stripe’s test card numbers:
  • Success: 4242 4242 4242 4242
  • Requires authentication: 4000 0025 0000 3155
  • Declined: 4000 0000 0000 0002

Common Issues

  • Ensure STRIPE_WEBHOOK_SECRET matches the webhook endpoint secret in Stripe Dashboard
  • Check that you’re using raw body, not parsed JSON
  • Verify the webhook endpoint URL is correctly configured in Stripe
  • Ensure webhook handler is processing checkout.session.completed events
  • Check database connection in webhook handler
  • Verify email is being correctly extracted from checkout session
  • Check subscription status in Stripe Dashboard
  • Verify customer.subscription.updated webhook is being received
  • Look for payment failures or expired cards

Additional Resources

Stripe API Documentation

Official Stripe API reference

Stripe Webhooks Guide

Complete guide to webhook implementation

Stripe Testing

Test cards and testing strategies

Stripe Dashboard

Manage customers and subscriptions

Build docs developers (and LLMs) love