ShipAddons

Stripe Setup

Subscriptions, webhooks, and billing portal configuration

ShipAddons uses Stripe for subscription management. This tutorial covers the complete setup process.

Subscription Model

The boilerplate supports:

  • Multiple pricing tiers (Basic, Essential, Pro)
  • Monthly recurring subscriptions
  • Upgrade and downgrade via billing portal
  • Webhook-driven status updates

1. Create Stripe Account

  1. Go to stripe.com and create an account
  2. For development, use Test Mode (toggle in the dashboard)
  3. [Optional] Complete account verification for production use

2. Get API Keys

From Developers > API keys, copy to your web/.env.local:

STRIPE_SECRET_KEY=sk_test_xxx

3. Configure Products

Products are defined in web/constants.ts:

export const plans: Record<Plan["id"], Plan> = {
  BASIC: {
    id: "BASIC",
    name: "Basic",
    description: "Basic plan",
    imageUrls: ["https://dummyimage.com/96x96/9/fff.png&text=Logo"],
    priceInCents: 0,
    mode: "subscription",
    features: ["basic_feature"],
  },
  ESSENTIAL: {
    id: "ESSENTIAL",
    name: "Essential",
    description: "Essential plan",
    imageUrls: ["https://dummyimage.com/96x96/9/fff.png&text=Logo"],
    priceInCents: 1000,
    mode: "subscription",
    features: ["essential_feature"],
    stripePriceId: "price_1SedRfRgqD2Y1UgSc0mp1Vcb",
  },
  PRO: {
    id: "PRO",
    name: "Pro",
    description: "Pro plan",
    imageUrls: ["https://dummyimage.com/96x96/9/fff.png&text=Logo"],
    priceInCents: 2000,
    mode: "subscription",
    features: ["pro_feature"],
    stripePriceId: "price_1SedRgRgqD2Y1UgSfiv60YIY",
  },
};

4. Sync Products to Stripe

The stripe-sync.ts script creates products and prices in Stripe:

cd web
npx tsx scripts/stripe-sync.ts sync

This command:

  1. Creates products in Stripe matching your constants.ts
  2. Creates prices for each product
  3. Updates constants.ts with stripePriceId values

Run sync whenever you modify packages.

5. Set Up Webhook

Webhooks sync subscription state from Stripe to your database.

For Production

  1. Go to Developers > Webhooks
  2. Add endpoint: https://your-domain.com/api/stripe/webhook
  3. Select events:
    • checkout.session.completed
    • customer.subscription.updated
    • customer.subscription.deleted
  4. Copy the signing secret to your .env.local:
STRIPE_WEBHOOK_SECRET=whsec_xxx

For Local Development

Use the Stripe CLI to forward webhooks:

stripe listen --forward-to localhost:3001/api/stripe/webhook

The CLI prints a webhook secret to use locally.

Webhook Handling

The webhook handler at /api/stripe/webhook processes three events:

checkout.session.completed

Triggered when a customer completes checkout:

async function handleCheckoutSessionCompleted(session: Stripe.Checkout.Session) {
  // Get user ID from session metadata
  const userId = session.client_reference_id;
  
  // Get subscription details
  const subscription = await stripe.subscriptions.retrieve(subscriptionId);
  
  // Upsert subscription in database
  await supabaseAdmin
    .from("subscriptions")
    .upsert({
      user_id: userId,
      stripe_customer_id: customerId,
      stripe_subscription_id: subscriptionId,
      status: subscription.status,
      plan_id: planId,
    });
}

customer.subscription.updated

Triggered when subscription changes (upgrade, downgrade, renewal):

async function handleSubscriptionUpdated(subscription: Stripe.Subscription) {
  await supabaseAdmin
    .from("subscriptions")
    .update({
      status: subscription.status,
      current_period_end: new Date(subscription.current_period_end * 1000),
    })
    .eq("stripe_subscription_id", subscription.id);
}

customer.subscription.deleted

Triggered when subscription is canceled:

async function handleSubscriptionDeleted(subscription: Stripe.Subscription) {
  await supabaseAdmin
    .from("subscriptions")
    .update({ status: "canceled" })
    .eq("stripe_subscription_id", subscription.id);
}

6. Configure Billing Portal

The billing portal allows customers to manage their subscription:

npx tsx scripts/stripe-sync.ts portal

This configures the portal to:

  • Show all subscription products
  • Allow plan switching (upgrades/downgrades)
  • Enable subscription cancellation

Copy the configuration ID to your .env.local:

STRIPE_PORTAL_CONFIG_ID=bpc_xxx

Checkout Flow

The add-on initiates checkout via the /api/stripe/checkout endpoint:

// Client-side (in add-on)
const response = await fetch(`${API_BASE_URL}/api/stripe/checkout`, {
  method: "POST",
  headers: {
    "Authorization": `Bearer ${accessToken}`,
    "Content-Type": "application/json",
  },
  body: JSON.stringify({ packageId: "PRO" }),
});

const { data: checkoutUrl } = await response.json();
window.open(checkoutUrl, "_blank");

The backend:

  1. Verifies the JWT
  2. Creates a Stripe checkout session
  3. Returns the checkout URL
  4. User completes payment on Stripe-hosted page
  5. Webhook updates subscription in database

Billing Portal

Customers access the billing portal to manage subscriptions:

// Client initiates portal session
const response = await fetch(`${API_BASE_URL}/api/stripe/billing_portal`, {
  method: "POST",
  headers: { "Authorization": `Bearer ${accessToken}` },
});

const { data: portalUrl } = await response.json();
window.open(portalUrl, "_blank");

The portal allows:

  • Viewing current subscription
  • Switching plans (prorated automatically)
  • Updating payment method
  • Canceling subscription

Script Commands

CommandDescription
npx tsx scripts/stripe-sync.ts syncCreate/update products and prices in Stripe
npx tsx scripts/stripe-sync.ts portalConfigure billing portal
npx tsx scripts/stripe-sync.ts listList all Stripe products
npx tsx scripts/stripe-sync.ts statusShow sync status

Testing

  1. Use Stripe test cards:
    • Success: 4242 4242 4242 4242
    • Decline: 4000 0000 0000 0002
  2. Check webhook delivery in Developers > Webhooks > Recent events
  3. Verify subscription in Supabase: Table Editor > subscriptions

Next Steps

On this page