Skip to main content

Documentation Index

Fetch the complete documentation index at: https://mintlify.com/MatthewSabia1/SubPirate-Pro/llms.txt

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

Overview

SubPirate uses Stripe for subscription billing. This integration handles:
  • Subscription checkout and payments
  • Plan upgrades and downgrades
  • Webhook events for subscription lifecycle
  • Customer portal for self-service billing management

Prerequisites

Before starting, you’ll need:
  • A Stripe account (sign up at dashboard.stripe.com)
  • Stripe CLI for local webhook testing (optional but recommended)
  • Supabase project with subscription tables migrated

Subscription Tiers

SubPirate offers three subscription tiers:
TierMonthly PriceAnnual PriceFeatures
Essentials$29/mo$290/year10 analyses/month, 1 Reddit account, 2 campaigns, 3 projects
Professional$79/mo$790/year50 analyses/month, 5 Reddit accounts, 10 campaigns, 10 projects, AI scheduler, advanced dashboard, team access
Agency$199/mo$1,990/yearUnlimited analyses, accounts, campaigns, projects, AI scheduler, advanced dashboard, team access

Stripe Setup

1

Create Stripe Account

If you don’t have one, create an account at https://dashboard.stripe.com.Start in Test Mode for development.
2

Create Products and Prices

Create three products in Stripe Dashboard → Products:

Essentials Product

  • Name: SubPirate Essentials
  • Metadata: tier = essentials
  • Prices:
    • Monthly: $29.00 recurring
    • Annual: $290.00 recurring

Professional Product

  • Name: SubPirate Professional
  • Metadata: tier = professional
  • Prices:
    • Monthly: $79.00 recurring
    • Annual: $790.00 recurring

Agency Product

  • Name: SubPirate Agency
  • Metadata: tier = agency
  • Prices:
    • Monthly: $199.00 recurring
    • Annual: $1,990.00 recurring
Add tier metadata to products and plan_tier metadata to prices. The webhook processor uses this to map Stripe subscriptions to SubPirate plans.
3

Copy Price IDs

After creating prices, copy each Price ID (format: price_xxxxxxxxxxxxx) from the Stripe Dashboard.You’ll need six Price IDs total (one monthly + one annual for each tier).
4

Get API Keys

Navigate to DevelopersAPI keys and copy:
  • Publishable key (starts with pk_test_ or pk_live_)
  • Secret key (starts with sk_test_ or sk_live_)
Never expose your Secret key to the browser or commit it to version control.

Environment Variables

Add these to your .env file:
.env
# Stripe Secret Key (server-only)
STRIPE_SECRET_KEY=sk_test_...

# Stripe Webhook Secret (from step "Configure Webhooks" below)
STRIPE_WEBHOOK_SECRET=whsec_...

# Price IDs (server-only)
STRIPE_PRICE_ESSENTIALS_MONTHLY=price_...
STRIPE_PRICE_ESSENTIALS_ANNUAL=price_...
STRIPE_PRICE_PROFESSIONAL_MONTHLY=price_...
STRIPE_PRICE_PROFESSIONAL_ANNUAL=price_...
STRIPE_PRICE_AGENCY_MONTHLY=price_...
STRIPE_PRICE_AGENCY_ANNUAL=price_...
All Stripe variables are server-only. Never prefix them with VITE_.

Configure Webhooks

Stripe uses webhooks to notify your app about subscription events (payments, cancellations, etc.).

Local Development

1

Install Stripe CLI

brew install stripe/stripe-cli/stripe
stripe login
2

Forward Webhooks to Localhost

stripe listen --forward-to localhost:8787/api/stripe/webhook
The CLI will print a webhook signing secret (whsec_...). Copy it to your .env:
STRIPE_WEBHOOK_SECRET=whsec_...
3

Keep CLI Running

Leave the stripe listen command running while testing. It will log all webhook events.

Production (Vercel)

1

Create Webhook Endpoint

In Stripe Dashboard → DevelopersWebhooks, click Add endpoint.Endpoint URL:
https://your-domain.com/api/stripe/webhook
2

Select Events

Add these events:
  • checkout.session.completed
  • invoice.paid
  • invoice.payment_failed
  • customer.subscription.updated
  • customer.subscription.deleted
3

Copy Signing Secret

After creating the endpoint, click to reveal the Signing secret.Add it to your Vercel environment variables as STRIPE_WEBHOOK_SECRET.

Webhook Processing

The webhook processor is implemented in api/_lib/stripeWebhookProcessor.js.

Idempotency

All webhook events are logged in the stripe_webhook_events table to prevent duplicate processing:
const claim = await claimEventLedger(supabase, event);
if (!claim.shouldProcess) {
  return { statusCode: 200, body: { received: true, duplicate: true } };
}
If an event fails processing, it’s marked as failed and can be retried.

Event Handlers

checkout.session.completed

Processed when a user completes Stripe Checkout:
async function processCheckoutCompleted(event, supabase, stripe) {
  const subscription = await stripe.subscriptions.retrieve(subscriptionId);
  
  // Resolve user identity from Stripe customer or metadata
  const identity = await resolveWebhookIdentity({ event, supabase });
  
  // Upsert subscription record
  await supabase.from('subscriptions').upsert({
    user_id: identity.userId,
    stripe_customer_id: session.customer,
    stripe_subscription_id: subscription.id,
    plan_id: planId,
    tier: tier,
    status: 'active'
  });
}

invoice.paid

Processed on successful recurring payments:
async function processInvoicePaid(event, supabase, stripe) {
  // Update subscription status and period dates
  await supabase.from('subscriptions').upsert({
    status: 'active',
    current_period_start: new Date(subscription.current_period_start * 1000),
    current_period_end: new Date(subscription.current_period_end * 1000),
    grace_period_end: null
  });
  
  // Create new usage period for quota tracking
  await supabase.from('subscription_usage').upsert({
    user_id: userId,
    period_start: periodStart,
    period_end: periodEnd,
    analyses_count: 0
  });
}

invoice.payment_failed

Processed when a payment fails:
async function processInvoicePaymentFailed(event, supabase, stripe) {
  const gracePeriodEnd = new Date();
  gracePeriodEnd.setDate(gracePeriodEnd.getDate() + 7); // 7-day grace period
  
  await supabase.from('subscriptions').upsert({
    status: 'past_due',
    grace_period_end: gracePeriodEnd.toISOString()
  });
}

customer.subscription.updated

Processed when a subscription is modified (plan changes, cancellations):
async function processSubscriptionUpdated(event, supabase) {
  // Detect downgrades and flag for admin review
  const isDowngrade = tierRank[newTier] < tierRank[currentTier];
  
  const updateData = {
    tier: newTier,
    plan_id: planId,
    billing_interval: interval,
    cancel_at_period_end: Boolean(subscription.cancel_at_period_end)
  };
  
  if (isDowngrade) {
    updateData.downgrade_action_required = {
      previous_tier: currentTier,
      new_tier: newTier,
      flagged_at: new Date().toISOString()
    };
  }
}

customer.subscription.deleted

Processed when a subscription is canceled:
async function processSubscriptionDeleted(event, supabase) {
  await supabase.from('subscriptions').upsert({
    status: 'canceled',
    canceled_at: new Date(subscription.canceled_at * 1000),
    stripe_subscription_id: '', // Clear subscription ID
    grace_period_end: null
  });
}

Identity Resolution

The webhook processor resolves user identity through multiple fallback strategies:
  1. By Stripe Subscription ID: Query subscriptions table
  2. By Stripe Customer ID: Query subscriptions table
  3. By Metadata: Check subscription.metadata.user_id
If identity cannot be resolved, the event is logged to stripe_webhook_unmatched for manual review.

Customer Portal

Stripe Customer Portal allows users to self-manage their subscriptions.
1

Enable Customer Portal

Navigate to SettingsBillingCustomer Portal.
2

Configure Features

Enable:
  • Cancel subscription
  • Switch plans (upgrade/downgrade with proration)
  • Update payment method
  • View invoices
Disallow immediate cancellations. Instead, set subscriptions to cancel at period end.
3

Save Configuration

Click Save to activate the portal.
Users access the portal via the “Manage Subscription” button in SubPirate’s settings page.

Dynamic Payment Methods

Stripe automatically selects optimal payment methods based on customer location.
1

Enable Dynamic Payment Methods

In SettingsPayment methods, enable dynamic payment methods.
2

Remove Hardcoded Types

Ensure your checkout code does not hardcode payment_method_types. Let Stripe decide:
// Good: Let Stripe choose
const session = await stripe.checkout.sessions.create({
  mode: 'subscription',
  line_items: [{ price: priceId, quantity: 1 }]
});

// Bad: Hardcoded payment methods
const session = await stripe.checkout.sessions.create({
  mode: 'subscription',
  payment_method_types: ['card'], // Don't do this
  line_items: [{ price: priceId, quantity: 1 }]
});

Email Notifications

Configure Stripe to send automatic emails:
1

Navigate to Email Settings

Go to SettingsEmails.
2

Enable Notifications

Turn on:
  • Successful payment receipts
  • Failed payment notifications
  • Upcoming trial ending (3 days before)
  • Subscription canceled confirmation

Testing

Test Cards

Use these cards in Test Mode:
Card NumberScenario
4242 4242 4242 4242Successful payment
4000 0000 0000 0341Immediate charge failure
4000 0025 0000 3155Requires 3D Secure authentication
4000 0000 0000 9995Insufficient funds
Use any future expiry date and any 3-digit CVC.

Test Checklist

  • Complete checkout with test card 4242 4242 4242 4242
  • Verify checkout.session.completed webhook fires
  • Confirm subscription appears in database with status active
  • Check /settings page shows correct plan
  • Click “Manage Subscription” and verify portal opens
  • Test plan upgrade (Starter → Creator)
  • Test plan downgrade and verify downgrade_action_required is set
  • Cancel subscription and verify status updates to canceled
  • Test failed payment with card 4000 0000 0000 0341
  • Verify grace period is set and user can still access features

Database Schema

subscriptions Table

CREATE TABLE subscriptions (
  user_id UUID PRIMARY KEY REFERENCES profiles(user_id),
  stripe_customer_id TEXT NOT NULL,
  stripe_subscription_id TEXT,
  plan_id UUID REFERENCES plan_definitions(id),
  tier TEXT NOT NULL,
  status TEXT NOT NULL,
  billing_interval TEXT,
  current_period_start TIMESTAMPTZ,
  current_period_end TIMESTAMPTZ,
  cancel_at_period_end BOOLEAN DEFAULT false,
  canceled_at TIMESTAMPTZ,
  grace_period_end TIMESTAMPTZ,
  downgrade_action_required JSONB,
  stripe_last_event_created_at TIMESTAMPTZ,
  created_at TIMESTAMPTZ DEFAULT now(),
  updated_at TIMESTAMPTZ DEFAULT now()
);

stripe_webhook_events Table

Logs all processed webhook events for idempotency:
CREATE TABLE stripe_webhook_events (
  event_id TEXT PRIMARY KEY,
  event_type TEXT NOT NULL,
  event_created_at TIMESTAMPTZ NOT NULL,
  status TEXT NOT NULL, -- 'processing' | 'processed' | 'failed'
  error_message TEXT,
  processed_at TIMESTAMPTZ,
  created_at TIMESTAMPTZ DEFAULT now()
);

stripe_webhook_unmatched Table

Stores events where user identity couldn’t be resolved:
CREATE TABLE stripe_webhook_unmatched (
  event_id TEXT PRIMARY KEY,
  event_type TEXT NOT NULL,
  stripe_customer_id TEXT,
  stripe_subscription_id TEXT,
  payload_excerpt JSONB,
  created_at TIMESTAMPTZ DEFAULT now()
);

Go-Live Checklist

Before switching to Live Mode:
  • Review Stripe Go-Live Checklist
  • Create live-mode Products and Prices
  • Update all 6 Price ID environment variables with live-mode IDs
  • Switch STRIPE_SECRET_KEY from sk_test_ to sk_live_
  • Create live-mode webhook endpoint
  • Update STRIPE_WEBHOOK_SECRET with live-mode webhook secret
  • Test a live-mode checkout with a real card (then refund)
  • Verify webhook events are processed correctly in production
  • Enable fraud detection in Stripe Radar
  • Set up billing email notifications

Troubleshooting

”No signature found” Error

Cause: Missing or incorrect STRIPE_WEBHOOK_SECRET. Solution: Verify the webhook secret matches the one shown in Stripe Dashboard (or Stripe CLI output).

Webhook Event Not Processing

Cause: Webhook endpoint unreachable or returning errors. Solution:
  1. Check Stripe Dashboard → Webhooks → click your endpoint → view event logs
  2. Verify your server is publicly accessible (use ngrok for local testing)
  3. Check server logs for errors in webhook handler

Subscription Not Found in Database

Cause: Identity resolution failed or webhook processing error. Solution:
  1. Check stripe_webhook_unmatched table for the event
  2. Verify metadata.user_id is set when creating checkout sessions
  3. Review logs in stripe_webhook_events table

Security Considerations

Webhook Signature Verification: Always verify Stripe webhook signatures before processing events. This prevents attackers from forging webhook requests.
Environment Separation: Use separate Stripe accounts (or at minimum, separate API keys) for test and live modes. Never use live-mode keys in development.
PCI Compliance: Never handle raw card numbers in your application. Always use Stripe Checkout or Stripe Elements for payment collection.

Next Steps

Build docs developers (and LLMs) love