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
- User selects a plan on pricing page
- Stripe Checkout opens with payment form
- Payment processed by Stripe
- Webhook fired to sync with database
- 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.comStripe Setup
- Create Products in Stripe Dashboard
- Set up Webhooks pointing to
/api/stripe/webhooks - Configure Plans in your database with Stripe IDs
Webhook Events
Enable these webhook events in Stripe:
customer.subscription.createdcustomer.subscription.updatedcustomer.subscription.deletedinvoice.payment_succeededinvoice.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.