Skip to content

Setting up Stripe Billing (self-hosted)

This page is for self-hosters who want to enable subscription billing on their own Plan AI instance. If you're using the managed Plan AI cloud, you don't need this — billing is already configured for you.

Skip this if…

You're running Plan AI for your own internal team and don't plan to charge anyone. Leave STRIPE_SECRET_KEY unset — the backend will skip all subscription guards and the app works exactly like before, with no friction. This is the default and recommended OSS setup.

When to enable Stripe billing

Enable Stripe only if you're operating Plan AI as a paid service yourself (e.g. running it for an agency that bills clients, or as a hosted SaaS for your own customers under a commercial license from us — see License).

When STRIPE_SECRET_KEY is configured, the backend enforces:

  • Subscription required on every paid endpoint (transcripts, docs, slides, diagrams, chat, audio streaming)
  • Per-tier feature gating based on BYOK vs Managed track
  • Webhook-driven sync of subscription state from Stripe → workspace DB
  • 402 Payment Required responses surfaced to all three clients (web, recorder, mobile)

Prerequisites

  • Stripe account (dashboard.stripe.com)
  • Plan AI self-hosted instance running and reachable from the internet (or Stripe CLI for local development)
  • Access to your backend .env file

Step 1 — Create products in Stripe

In your Stripe Dashboard, create one product + recurring price per tier you want to offer:

ProductTrackPriceBilling
Plan AI Pro (BYOK)BYOK€6/seat/monthMonthly, Licensed (per seat)
Plan AI Pro (Managed)Managed€29/seat/monthMonthly, Licensed (per seat)
Plan AI Business (BYOK)BYOK€14/seat/monthMonthly, Licensed (per seat)
Plan AI Business (Managed)Managed€65/seat/monthMonthly, Licensed (per seat)

You don't need to create all four — only the ones you want to offer. The catalog endpoint (/api/billing/catalog) returns whichever price IDs are configured.

Copy each Price ID (starts with price_…).

Step 2 — Create a webhook endpoint

Plan AI ships a webhook receiver at POST /api/billing/webhook. Stripe sends subscription state changes here.

For production

  1. Stripe Dashboard → Developers → Webhooks → Add endpoint
  2. Endpoint URL: https://your-plan-ai-host.com/api/billing/webhook
  3. Events to send:
    • checkout.session.completed
    • customer.subscription.created
    • customer.subscription.updated
    • customer.subscription.deleted
    • invoice.payment_failed
  4. Save, then click into the endpoint and reveal the Signing secret (whsec_…).

For local development

bash
brew install stripe/stripe-cli/stripe
stripe login
stripe listen --forward-to localhost:8080/api/billing/webhook

The CLI prints a webhook signing secret on startup — use that.

Step 3 — Configure env vars

Add these to plan-ai/backend/.env:

bash
# Stripe API keys (Dashboard → Developers → API keys)
STRIPE_SECRET_KEY="sk_test_..."        # or sk_live_... for production
STRIPE_WEBHOOK_SECRET="whsec_..."      # from Step 2

# Price IDs from Step 1 — only include the tiers you offer
STRIPE_PRICE_PRO_BYOK="price_..."
STRIPE_PRICE_PRO_MANAGED="price_..."
STRIPE_PRICE_BUSINESS_BYOK="price_..."
STRIPE_PRICE_BUSINESS_MANAGED="price_..."

# Where Stripe redirects after checkout (relative to APP_URL)
STRIPE_CHECKOUT_SUCCESS_PATH="/billing?status=success"
STRIPE_CHECKOUT_CANCEL_PATH="/billing?status=canceled"

Restart the backend after editing.

Step 4 — Verify

Open the web app at http://localhost:3000/billing.

You should see:

  • The price catalog loaded from your configured Price IDs
  • No "active subscription" state for the workspace
  • The yellow subscription banner on top of paid pages (Chat, Slides, Docs, Diagrams, Recordings)

Try a test purchase:

  1. Click Subscribe on any tier → redirected to Stripe Checkout
  2. Pay with test card 4242 4242 4242 4242, any expiry, any CVC, any postcode
  3. Stripe redirects to /billing?status=success
  4. The page should show Current Plan: PRO · BYOK (or whichever tier you chose)
  5. In the database: SELECT id, name, "subscriptionStatus", tier FROM "Workspace"; should show your workspace as ACTIVE / PRO / BYOK
  6. The yellow banner disappears, AI features unlock

What happens when a payment fails

Stripe automatically retries failed payments (3 attempts over ~7 days by default).

  1. First attempt fails → invoice.payment_failed event → backend logs warning
  2. Stripe transitions subscription to past_due → backend keeps access (grace period)
  3. After all retries fail → Stripe transitions to unpaid then canceled → backend locks paid features

The user sees the yellow subscription banner with a "Re-subscribe" CTA, and can update their card via the Stripe Customer Portal (linked from /billing).

Self-host considerations

  • Database: All billing state lives in Workspace table columns (stripeCustomerId, subscriptionStatus, subscriptionTrack, subscriptionSeats, subscriptionCurrentPeriodEnd) plus a BillingEvent table for webhook idempotency. No new infrastructure required.
  • Workers: All 3 paid background workers (transcript generation, GitHub indexing, context document processing) re-check subscription state at job execution time. If a subscription lapses mid-flight, the worker bails cleanly without burning OpenRouter/Deepgram credit.
  • WebSocket: The live audio streaming WebSocket (/api/audio/stream) checks subscription before opening the Deepgram connection — protects against runaway transcription costs.
  • Disabling billing: Just unset STRIPE_SECRET_KEY. The backend reverts to the OSS behavior — all guards become no-ops, all subscription checks return active: true, all banners hide on the frontend. No code changes needed.

Commercial use

Important — read the license

Operating Plan AI as a paid SaaS that competes with the Plan AI hosted offering is not permitted under BSL 1.1. If you're running Stripe billing on a self-hosted Plan AI for your own internal team, you're fine. If you're reselling Plan AI as a service to third parties, you need a commercial license — reach out at hello@blueberrybytes.com.

Troubleshooting

SymptomCauseFix
STRIPE_SECRET_KEY is not configured on startupBackend can't read the env varRestart backend after editing .env; confirm no typos in key name
Webhook returns 400 "Invalid signature"Wrong STRIPE_WEBHOOK_SECRETRe-copy from Stripe CLI output or dashboard webhook page (must match the exact endpoint)
User pays but banner still shows "no subscription"Webhook delayed or failed to reach backendVerify Stripe CLI / dashboard shows successful 200 deliveries; check the BillingEvent table for the event ID
Subscribe button does nothingNo STRIPE_PRICE_* env vars setBackend /api/billing/catalog returns {prices: []} — fill in at least one tier's price ID
Customer Portal opens but changes don't sync backWebhook URL not reachable from StripeProduction webhook URL must be publicly accessible (no localhost)

Reference