Shipkit
DocsFeaturesPricing
DocsFeaturesPricing

Shipkit DocumentationBuild memory optimizationDevelopment GuideEnvironment VariablesError HandlingFile StructureMiddlewareRebranding Guide

Vercel Deployment Checks

Development GuideCLIDependency UpdatesDevToolsFeature FlagsAI PromptsSetup RequirementsSupply chaintRPCWeb Workers

FeaturesAIAnalytics IntegrationAuthenticationCMSDatabaseEmailHapticsPaymentsComponent RegistryStorageUIVisual BuilderWaitlist Feature

Quick StartDeploymentEnvironment VariablesFile StructureSetup Wizard

Caching & Rate LimitingContent ManagementError HandlingMarkdown and MDXMiddlewareProxy-Safe Server ActionsRebranding GuideWebhook Security Implementation GuideMulti-Zone ArchitectureVercel Deployment

IntegrationsPayload CMSDataFast Analytics IntegrationGoogle Analytics IntegrationGoogle Tag Manager IntegrationPostHogStatSig IntegrationUmami Analytics IntegrationAuth.js IntegrationBetter AuthClerkStack Auth IntegrationSupabase Authentication IntegrationBuilder.io IntegrationPayload CMS IntegrationAWS S3 IntegrationResend Email IntegrationUpstash Redis IntegrationVercel Blob IntegrationLemon SqueezyPolarStripe

API RoutesComponentsAPI Route SnippetsAuthentication SnippetsComponent SnippetsForm Handling SnippetsSnippets Introduction

    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