Skip to main content

Documentation Index

Fetch the complete documentation index at: https://mintlify.com/revokslab/shipfree/llms.txt

Use this file to discover all available pages before exploring further.

ShipFree includes a flexible payment system that supports multiple payment providers through a unified adapter interface. Switch between providers without changing your application code.

Supported Providers

Stripe

Industry-leading payment processor with global reach

Polar

Developer-friendly payment platform for digital products

Lemon Squeezy

Merchant of record platform handling taxes and compliance

Architecture Overview

The payment system uses an adapter pattern that provides a consistent interface across all providers:
src/lib/payments/service.ts
import { StripeAdapter } from './providers/stripe'
import { PolarAdapter } from './providers/polar'
import { LemonSqueezyAdapter } from './providers/lemonsqueezy'

export function getPaymentAdapter(): PaymentAdapter {
  const provider = env.PAYMENT_PROVIDER || 'stripe'
  
  switch (provider) {
    case 'stripe':
      return new StripeAdapter()
    case 'polar':
      return new PolarAdapter()
    case 'lemonsqueezy':
      return new LemonSqueezyAdapter()
    default:
      throw new Error(`Unknown payment provider: ${provider}`)
  }
}
Set PAYMENT_PROVIDER in your .env file to choose your provider: stripe, polar, or lemonsqueezy

Plans & Pricing

Plans are centrally configured in src/config/payments.ts:
src/config/payments.ts
export const paymentConfig = {
  plans: {
    free: {
      name: 'Free',
      description: 'Perfect for getting started',
      isFree: true,
      prices: {},
      features: [
        'Up to 3 projects',
        'Basic analytics',
        'Community support',
        'Standard templates',
      ],
    },
    
    starter: {
      name: 'Starter',
      description: 'Great for small teams',
      prices: {
        stripe: [
          {
            productId: process.env.NEXT_PUBLIC_STRIPE_PRICE_STARTER_MONTHLY,
            interval: 'month',
            amount: 990, // $9.90
            currency: 'usd',
            trialPeriodDays: 14,
            type: 'recurring',
          },
          {
            productId: process.env.NEXT_PUBLIC_STRIPE_PRICE_STARTER_YEARLY,
            interval: 'year',
            amount: 9900, // $99
            currency: 'usd',
            trialPeriodDays: 14,
            type: 'recurring',
          },
        ],
      },
      features: [
        'Up to 10 projects',
        'Advanced analytics',
        'Email support',
        'Premium templates',
        'Custom integrations',
      ],
    },
    
    pro: {
      name: 'Pro',
      description: 'For growing businesses',
      recommended: true,
      prices: {
        stripe: [
          {
            productId: process.env.NEXT_PUBLIC_STRIPE_PRICE_PRO_MONTHLY,
            interval: 'month',
            amount: 2990, // $29.90
            currency: 'usd',
            seatBased: true,
            trialPeriodDays: 14,
            type: 'recurring',
          },
        ],
      },
      features: [
        'Unlimited projects',
        'Real-time analytics',
        'Priority support',
        'White-label options',
        'Advanced integrations',
        'Team collaboration',
      ],
      maxSeats: 50,
    },
    
    enterprise: {
      name: 'Enterprise',
      description: 'For large organizations',
      prices: {
        stripe: [
          {
            productId: process.env.NEXT_PUBLIC_STRIPE_PRICE_ENTERPRISE_MONTHLY,
            interval: 'month',
            amount: 9990, // $99.90
            currency: 'usd',
            seatBased: true,
            trialPeriodDays: 30,
            type: 'recurring',
          },
        ],
      },
      features: [
        'Everything in Pro',
        'Dedicated account manager',
        'Custom contracts',
        'SLA guarantees',
        'Advanced security',
        'Unlimited seats',
      ],
    },
  },
}

Accessing Plan Configuration

import { getPlanConfig, getPriceConfig } from '@/config/payments'

// Get plan details
const proPlan = getPlanConfig('pro')
console.log(proPlan.name) // "Pro"
console.log(proPlan.features) // Array of features

// Get prices for a specific provider
const stripePrices = getPriceConfig('pro', 'stripe')
console.log(stripePrices[0].amount) // 2990 (in cents)

Database Schema

The payment system uses these tables (defined in src/database/schema.ts):
src/database/schema.ts
export const customer = pgTable('customer', {
  id: text('id').primaryKey(),
  userId: text('user_id').notNull().references(() => user.id),
  provider: text('provider').notNull(), // 'stripe', 'polar', 'lemonsqueezy'
  providerCustomerId: text('provider_customer_id').notNull(),
  email: text('email'),
  createdAt: timestamp('created_at').defaultNow().notNull(),
  updatedAt: timestamp('updated_at').defaultNow().notNull(),
})

Creating Checkout Sessions

Create a checkout session to accept payments:
src/app/actions/checkout.ts
'use server'

import { getPaymentAdapter } from '@/lib/payments/service'
import { paymentConfig } from '@/config/payments'

export async function createCheckoutSession(plan: PlanName) {
  const adapter = getPaymentAdapter()
  
  const session = await adapter.createCheckout({
    plan,
    userId: user.id,
    email: user.email,
    successUrl: paymentConfig.providers.successUrl,
    cancelUrl: paymentConfig.providers.cancelUrl,
    trialDays: 14,
  })
  
  return { url: session.url }
}

Customer Portal

Let users manage their subscriptions via the provider’s hosted portal:
src/app/actions/portal.ts
'use server'

import { getPaymentAdapter } from '@/lib/payments/service'
import { db } from '@/database'
import { customer } from '@/database/schema'
import { eq } from 'drizzle-orm'

export async function createPortalSession(userId: string) {
  // Get customer record
  const customerRecord = await db.query.customer.findFirst({
    where: eq(customer.userId, userId),
  })
  
  if (!customerRecord) {
    throw new Error('No customer found')
  }
  
  const adapter = getPaymentAdapter()
  const portal = await adapter.createPortal(
    customerRecord.providerCustomerId,
    `${process.env.NEXT_PUBLIC_APP_URL}/dashboard`
  )
  
  return { url: portal.url }
}

Webhook Handling

1

Configure Webhook Endpoint

Set up webhook endpoints in your payment provider dashboard pointing to:
  • Stripe: https://yourdomain.com/api/webhooks/stripe
  • Polar: https://yourdomain.com/api/webhooks/polar
  • Lemon Squeezy: https://yourdomain.com/api/webhooks/lemonsqueezy
2

Process Events

Webhooks automatically update subscription and payment records in your database.
3

Handle Failures

Failed payments trigger status updates and can send notification emails.
Always verify webhook signatures to ensure requests are legitimate. Each adapter includes signature validation.

Subscription Management

Check Active Subscription

import { db } from '@/database'
import { subscription } from '@/database/schema'
import { eq, and } from 'drizzle-orm'

export async function getUserSubscription(userId: string) {
  return await db.query.subscription.findFirst({
    where: and(
      eq(subscription.userId, userId),
      eq(subscription.status, 'active')
    ),
  })
}

Cancel Subscription

src/app/actions/subscription.ts
'use server'

import { getPaymentAdapter } from '@/lib/payments/service'

export async function cancelSubscription(
  providerSubscriptionId: string,
  cancelAtPeriodEnd = true
) {
  const adapter = getPaymentAdapter()
  
  await adapter.cancelSubscription(
    providerSubscriptionId,
    cancelAtPeriodEnd
  )
  
  return { success: true }
}

Payment Adapter Interface

All payment providers implement this interface:
src/lib/payments/types.ts
export interface PaymentAdapter {
  readonly provider: PaymentProvider
  
  createCheckout(options: CheckoutOptions): Promise<CheckoutResult>
  createCustomer(userId: string, email?: string): Promise<CustomerData>
  getSubscription(providerSubscriptionId: string): Promise<SubscriptionData | null>
  cancelSubscription(providerSubscriptionId: string, cancelAtPeriodEnd?: boolean): Promise<void>
  createPortal(customerId: string, returnUrl?: string): Promise<PortalResult>
  processWebhook(event: WebhookEvent): Promise<WebhookResult>
  validateWebhook(rawBody: string, signature: string): Promise<boolean>
}
This unified interface makes it easy to switch between payment providers without changing your application code.

Environment Variables

.env
PAYMENT_PROVIDER=stripe
STRIPE_SECRET_KEY=sk_test_...
STRIPE_WEBHOOK_SECRET=whsec_...

# Public price IDs
NEXT_PUBLIC_STRIPE_PRICE_STARTER_MONTHLY=price_...
NEXT_PUBLIC_STRIPE_PRICE_STARTER_YEARLY=price_...
NEXT_PUBLIC_STRIPE_PRICE_PRO_MONTHLY=price_...

Testing Payments

Use test mode credentials during development. Each provider offers test cards and environments.

Stripe Testing

Use test cards like 4242 4242 4242 4242

Polar Testing

Polar provides a sandbox environment

Lemon Squeezy

Enable test mode in your store settings

Further Reading

Stripe Docs

Complete Stripe API documentation

Polar Docs

Polar developer documentation

Lemon Squeezy

Lemon Squeezy API reference

Webhook Best Practices

Learn about webhook security and reliability

Build docs developers (and LLMs) love