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
| Provider | Feature Flag | Required Env Vars |
|---|---|---|
| Lemon Squeezy | NEXT_PUBLIC_FEATURE_LEMONSQUEEZY_ENABLED | LEMONSQUEEZY_API_KEY, LEMONSQUEEZY_STORE_ID |
| Stripe | NEXT_PUBLIC_FEATURE_STRIPE_ENABLED | STRIPE_SECRET_KEY, NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY |
| Polar | NEXT_PUBLIC_FEATURE_POLAR_ENABLED | POLAR_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 paidhasUserActiveSubscription(userId)- Check subscription statusgetUserPurchasedProducts(userId)- List user's purchasescreateCheckoutUrl(options)- Generate a checkout linkhandleWebhookEvent(event)- Process webhook eventsimportPayments()- 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:
| Provider | Endpoint | Secret Env Var |
|---|---|---|
| Lemon Squeezy | /webhooks/lemonsqueezy | LEMONSQUEEZY_WEBHOOK_SECRET |
| Stripe | /webhooks/stripe | STRIPE_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:
- Extracts order info from query params (Lemon Squeezy) or session (Stripe/Polar)
- Creates a payment record in the database
- Creates the user if they don't exist
- 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
| File | Purpose |
|---|---|
src/server/services/payment-service.ts | Unified payment orchestration |
src/server/providers/lemonsqueezy-provider.ts | Lemon Squeezy implementation |
src/server/providers/stripe-provider.ts | Stripe implementation |
src/server/providers/polar-provider.ts | Polar implementation |
src/server/actions/payments.ts | Payment server actions |
src/app/(app)/webhooks/ | Webhook route handlers |
src/app/(app)/checkout/success/page.tsx | Post-checkout page |