Payments

Shipkit supports three payment providers behind a unified PaymentService. Each provider is feature-flagged and disabled when its env vars aren't set.

Providers

ProviderFeature FlagRequired Env Vars
Lemon SqueezyNEXT_PUBLIC_FEATURE_LEMONSQUEEZY_ENABLEDLEMONSQUEEZY_API_KEY, LEMONSQUEEZY_STORE_ID
StripeNEXT_PUBLIC_FEATURE_STRIPE_ENABLEDSTRIPE_SECRET_KEY, NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY
PolarNEXT_PUBLIC_FEATURE_POLAR_ENABLEDPOLAR_ACCESS_TOKEN

Feature flags are auto-set when the required env vars are present. No manual flag configuration needed.

How It Works

All providers implement a common interface (src/server/providers/base-provider.ts):

  • getPaymentStatus(userId) - Check if user has paid
  • hasUserActiveSubscription(userId) - Check subscription status
  • getUserPurchasedProducts(userId) - List user's purchases
  • createCheckoutUrl(options) - Generate a checkout link
  • handleWebhookEvent(event) - Process webhook events
  • importPayments() - Bulk sync payments from provider API

The PaymentService aggregates results across all enabled providers.

Checkout Flow

Lemon Squeezy

Configure products in your Lemon Squeezy dashboard and store variant IDs in src/config/site-config.ts:

store: {
  id: "your-store-id",
  products: {
    bones: "variant-id-for-bones",
    brains: "variant-id-for-brains",
  }
}

Stripe

Checkout route at /integrations/stripe/checkout. Accepts price_id, mode (payment/subscription), success_url, and cancel_url. Requires authentication.

STRIPE_SECRET_KEY=sk_...
NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY=pk_...
STRIPE_WEBHOOK_SECRET=whsec_...

Polar

Uses the @polar-sh/nextjs SDK. Checkout route at /integrations/polar/checkout.

Webhooks

Each provider has a webhook endpoint:

ProviderEndpointSecret Env Var
Lemon Squeezy/webhooks/lemonsqueezyLEMONSQUEEZY_WEBHOOK_SECRET
Stripe/webhooks/stripeSTRIPE_WEBHOOK_SECRET
Polar/webhooks/polar-

What Webhooks Handle

  • Order created: Creates payment record + user (if new)
  • Order refunded: Updates payment status to "refunded"
  • Subscription created/updated/cancelled: Tracks subscription lifecycle
  • Payment succeeded/failed: Records recurring payment events

All webhook handlers verify signatures (HMAC-SHA256 for Lemon Squeezy, Stripe's built-in verification) and check for idempotency to prevent duplicate processing.

Checkout Success Page

After payment, users land on /checkout/success. This page:

  1. Extracts order info from query params (Lemon Squeezy) or session (Stripe/Polar)
  2. Creates a payment record in the database
  3. Creates the user if they don't exist
  4. Shows a confirmation with download access if applicable

Database Schema

Payments are stored in the payment table:

{
  id: serial,
  userId: string,        // Who paid
  orderId: string,       // Internal order ID
  processorOrderId: string, // Provider's order ID
  amount: integer,       // Cents
  status: string,        // "paid", "refunded", "pending"
  processor: string,     // "lemonsqueezy", "stripe", "polar"
  productName: text,
  isFreeProduct: boolean,
  metadata: text,        // JSON
  purchasedAt: timestamp,
}

Admin Features

Admin users can:

  • View all payments across providers (PaymentService.getPaymentsWithUsers())
  • Import payments from provider APIs
  • View user payment status and subscription history
  • Delete and refresh payment data

Admin actions are rate-limited.

Checking Payment Status

import { PaymentService } from "@/server/services/payment-service"

// Check if user has any active payment
const status = await PaymentService.getUserPaymentStatus(userId)

// Check specific product purchase
const hasPurchased = await PaymentService.hasUserPurchasedProduct(userId, productId)

// Check active subscription
const isSubscribed = await PaymentService.hasUserActiveSubscription(userId)

Key Files

FilePurpose
src/server/services/payment-service.tsUnified payment orchestration
src/server/providers/lemonsqueezy-provider.tsLemon Squeezy implementation
src/server/providers/stripe-provider.tsStripe implementation
src/server/providers/polar-provider.tsPolar implementation
src/server/actions/payments.tsPayment server actions
src/app/(app)/webhooks/Webhook route handlers
src/app/(app)/checkout/success/page.tsxPost-checkout page