Skip to content

Stripe Payments & Subscriptions ​

ClawPlate includes a complete subscription system with Stripe integration. Users can subscribe to plans, manage billing, and payments are automatically synchronized with your database.

How it works ​

1. Database Structure ​

Two main tables handle subscriptions:

sql
-- Available plans (Free, Pro, Enterprise)
CREATE TABLE plans (
  id TEXT PRIMARY KEY,
  name TEXT NOT NULL,
  price INTEGER NOT NULL,        -- in cents (0, 2900, 9900)
  interval TEXT NOT NULL,        -- 'month'
  features JSONB,
  stripe_product_id TEXT,
  stripe_price_id TEXT,
  is_active BOOLEAN DEFAULT true
);

-- User subscriptions
CREATE TABLE user_subscriptions (
  id UUID PRIMARY KEY,
  user_id UUID REFERENCES user_profiles(id),
  plan_id TEXT REFERENCES plans(id),
  stripe_subscription_id TEXT UNIQUE,
  stripe_customer_id TEXT,
  status TEXT CHECK (status IN ('active', 'canceled', 'trialing', 'past_due')),
  current_period_start TIMESTAMP,
  current_period_end TIMESTAMP,
  cancel_at_period_end BOOLEAN DEFAULT false
);

2. Subscription Flow ​

  1. User selects a plan on pricing page
  2. Stripe Checkout opens with payment form
  3. Payment processed by Stripe
  4. Webhook fired to sync with database
  5. User gets access to subscribed features

Core Implementation ​

Plans API ​

typescript
// server/api/plans.get.ts
export default defineEventHandler(async (event) => {
  const { data: plans } = await supabaseAdmin
    .from('plans')
    .select('*')
    .eq('is_active', true)
    .order('price', { ascending: true })

  return { success: true, plans: plans || [] }
})

Subscription Composable ​

typescript
// composables/useSubscription.ts
export const useSubscription = () => {
  const supabase = useSupabaseClient()
  const user = useSupabaseUser()

  const getCurrentSubscription = async () => {
    const { data } = await supabase
      .from('user_subscriptions')
      .select(`
        *,
        plans:plan_id (name, price, interval, features)
      `)
      .eq('user_id', user.value.id)
      .in('status', ['active', 'trialing'])
      .single()

    return data
  }

  const createCheckoutSession = async (priceId: string) => {
    const response = await $fetch('/api/stripe/create-checkout-session', {
      method: 'POST',
      body: { 
        priceId, 
        userId: user.value.id, 
        userEmail: user.value.email 
      }
    })
    return response.data
  }

  return { getCurrentSubscription, createCheckoutSession }
}

Checkout Session Creation ​

typescript
// server/api/stripe/create-checkout-session.post.ts
export default defineEventHandler(async (event) => {
  const { priceId, userId, userEmail } = await readBody(event)
  
  const session = await stripe.checkout.sessions.create({
    payment_method_types: ['card'],
    line_items: [{
      price: priceId,
      quantity: 1,
    }],
    mode: 'subscription',
    success_url: `${process.env.SITE_URL}/dashboard?success=true`,
    cancel_url: `${process.env.SITE_URL}/pricing`,
    customer_email: userEmail,
    metadata: {
      userId: userId
    }
  })

  return { success: true, data: { url: session.url } }
})

Webhook Handler ​

typescript
// server/api/stripe/webhooks.post.ts
export default defineEventHandler(async (event) => {
  const body = await readRawBody(event)
  const signature = getHeader(event, 'stripe-signature')
  
  const stripeEvent = stripe.webhooks.constructEvent(
    body, signature, process.env.STRIPE_WEBHOOK_SECRET
  )
  
  switch (stripeEvent.type) {
    case 'customer.subscription.created':
      await handleSubscriptionCreated(stripeEvent.data.object)
      break
    case 'customer.subscription.updated':
      await handleSubscriptionUpdated(stripeEvent.data.object)
      break
    case 'customer.subscription.deleted':
      await handleSubscriptionDeleted(stripeEvent.data.object)
      break
  }

  return { received: true }
})

async function handleSubscriptionCreated(subscription) {
  // Get user by email
  const customer = await stripe.customers.retrieve(subscription.customer)
  const { data: userProfile } = await supabaseAdmin
    .from('user_profiles')
    .select('id')
    .eq('email', customer.email)
    .single()

  // Get plan by price ID
  const { data: plan } = await supabaseAdmin
    .from('plans')
    .select('id')
    .eq('stripe_price_id', subscription.items.data[0]?.price.id)
    .single()

  // Create subscription in database
  await supabaseAdmin
    .from('user_subscriptions')
    .upsert({
      stripe_subscription_id: subscription.id,
      stripe_customer_id: subscription.customer,
      user_id: userProfile.id,
      plan_id: plan.id,
      status: subscription.status,
      current_period_start: new Date(subscription.current_period_start * 1000),
      current_period_end: new Date(subscription.current_period_end * 1000)
    })
}

Pricing Page ​

vue
<template>
  <div class="pricing-grid">
    <div 
      v-for="plan in pricingPlans" 
      :key="plan.id"
      class="pricing-card"
    >
      <h3>{{ plan.title }}</h3>
      <p class="price">{{ plan.price }}/{{ plan.period }}</p>
      
      <ul class="features">
        <li v-for="feature in plan.features" :key="feature">
          {{ feature }}
        </li>
      </ul>
      
      <button @click="handleSubscribe(plan)">
        Subscribe
      </button>
    </div>
  </div>
</template>

<script setup>
const { createCheckoutSession } = useSubscription()
const { data: plansData } = await useFetch('/api/plans')

const pricingPlans = computed(() => {
  return plansData.value.plans.map(plan => ({
    id: plan.id,
    title: plan.name,
    price: plan.price === 0 ? 'Free' : `$${plan.price / 100}`,
    period: plan.interval,
    features: plan.features,
    priceId: plan.stripe_price_id
  }))
})

const handleSubscribe = async (plan) => {
  if (plan.price === 'Free') return
  
  const result = await createCheckoutSession(plan.priceId)
  if (result?.url) {
    window.location.href = result.url
  }
}
</script>

Advanced Features ​

Billing Portal ​

Let users manage their billing:

typescript
// server/api/stripe/create-portal-session.post.ts
export default defineEventHandler(async (event) => {
  const { userId } = await readBody(event)
  
  // Get user's Stripe customer ID
  const { data: subscription } = await supabaseAdmin
    .from('user_subscriptions')
    .select('stripe_customer_id')
    .eq('user_id', userId)
    .single()

  const portalSession = await stripe.billingPortal.sessions.create({
    customer: subscription.stripe_customer_id,
    return_url: `${process.env.SITE_URL}/dashboard`,
  })

  return { success: true, data: { url: portalSession.url } }
})

Subscription Management ​

typescript
// Cancel subscription
const cancelSubscription = async (subscriptionId: string) => {
  await $fetch('/api/subscription/cancel', {
    method: 'POST',
    body: { subscriptionId }
  })
}

// Resume subscription
const resumeSubscription = async (subscriptionId: string) => {
  await $fetch('/api/subscription/resume', {
    method: 'POST',
    body: { subscriptionId }
  })
}

Configuration ​

Environment Variables ​

bash
# Stripe
STRIPE_SECRET_KEY=sk_test_...
STRIPE_PUBLISHABLE_KEY=pk_test_...
STRIPE_WEBHOOK_SECRET=whsec_...

# Supabase
SUPABASE_URL=https://...
SUPABASE_SERVICE_ROLE_KEY=eyJ...

# Site
SITE_URL=https://yoursite.com

Stripe Setup ​

  1. Create Products in Stripe Dashboard
  2. Set up Webhooks pointing to /api/stripe/webhooks
  3. Configure Plans in your database with Stripe IDs

Webhook Events ​

Enable these webhook events in Stripe:

  • customer.subscription.created
  • customer.subscription.updated
  • customer.subscription.deleted
  • invoice.payment_succeeded
  • invoice.payment_failed

Security ​

Webhook Verification ​

Always verify webhook signatures:

typescript
const stripeEvent = stripe.webhooks.constructEvent(
  body, signature, webhookSecret
)

Database Security ​

Use Row Level Security (RLS) in Supabase:

sql
-- Users can only see their own subscriptions
CREATE POLICY "Users can view own subscriptions" ON user_subscriptions
FOR SELECT USING (auth.uid() = user_id);

Best Practices ​

Error Handling ​

typescript
try {
  const session = await createCheckoutSession(priceId)
  window.location.href = session.url
} catch (error) {
  toast.error('Payment failed. Please try again.')
}

Status Management ​

Handle all subscription statuses:

typescript
const getSubscriptionStatus = (status: string) => {
  switch (status) {
    case 'active': return 'Active'
    case 'trialing': return 'Trial'
    case 'past_due': return 'Payment Due'
    case 'canceled': return 'Canceled'
    default: return 'Unknown'
  }
}

Testing ​

Use Stripe's test mode and test cards:

javascript
// Test card numbers
const testCards = {
  success: '4242424242424242',
  declined: '4000000000000002',
  requiresAuth: '4000002500003155'
}

Monitoring ​

Track these metrics:

  • Subscription creation/cancellation rates
  • Failed payment attempts
  • Webhook delivery success
  • Revenue trends

The payment system handles everything automatically once configured. Users can subscribe, manage billing, and you get real-time synchronization between Stripe and your database.

Built with love by mhdevfr