Skip to content

๐Ÿ’ณ SaaS Mode (Stripe)

MiroTalk WebRTC can run in two modes, controlled by a single environment variable:

SAAS Behavior
false (default) Self-hosted mode. No subscription checks, no Stripe. The platform behaves exactly as it always has.
true Paid SaaS mode. Registered users must have an active subscription to use the platform.

When SAAS=true, demo and admin accounts are always exempt from payment checks, so you can keep administering the platform normally.


๐Ÿงฉ How it works

  1. A logged-in user without an active subscription is redirected to /pricing.
  2. They choose a plan and are sent to Stripe Checkout.
  3. After payment they return to the app, the subscription is activated, and they land directly in the dashboard (/client).
  4. Renewals and cancellations are kept in sync through Stripe webhooks.

The pricing page reads the amount and currency from the configured Stripe Price objects. Active customers see their current plan instead of another purchase prompt, and duplicate subscriptions are rejected server-side.

Plan changes only move upward: Monthly can upgrade to Annual or Lifetime, Annual can upgrade to Lifetime, and Lifetime cannot move back to a recurring plan. Monthly-to-Annual upgrades take effect immediately and Stripe invoices the prorated difference. In Stripe Dashboard โ†’ Customer portal settings, keep subscription plan switching disabled so the general billing portal remains limited to payment methods, invoices, and cancellation; plan upgrades are controlled by the app.

Plans

Plan Price Stripe type Access
Monthly $9 / month Subscription While subscriptionStatus = active and not expired
Annual $79 / year Subscription While subscriptionStatus = active and not expired
Lifetime $199 once One-time payment Permanent (subscriptionExpiresAt = null)

Protected when SAAS=true

  • The dashboard page GET /client โ†’ redirects to /pricing if no active subscription.
  • Protected APIs return HTTP 403: room create/update/delete, room invitations, recurring invitations, SFU token generation, and SMS.
  • Login, account, billing, and the pricing/checkout flow remain accessible.

Activation is webhook-independent

After checkout, the success page calls GET /api/v1/stripe/verify?session_id=..., which activates the subscription immediately from the Checkout Session. The webhook is still used to keep renewals/cancellations in sync, but activation does not depend on the webhook arriving first (helpful in local dev).

When paid access is activated, MiroTalk sends one transactional email confirming the plan and renewal date (or no recurring charges for Lifetime) with a link to the dashboard. Stripe remains responsible for receipts and payment notifications. The Checkout Session or subscription ID prevents the verification fallback and webhook from sending the same activation message twice.


โš™๏ธ Environment variables

Add these to your .env (see .env.template):

Bash
SAAS=true                          # master switch: true | false
STRIPE_SECRET_KEY=sk_...           # Stripe secret key (sk_live_... / sk_test_...)
STRIPE_PUBLISHABLE_KEY=pk_...      # Stripe publishable key (pk_live_... / pk_test_...)
STRIPE_WEBHOOK_SECRET=whsec_...    # Signing secret of your webhook endpoint
STRIPE_MONTHLY_PRICE_ID=price_...  # Price ID of the $9/month recurring price
STRIPE_YEARLY_PRICE_ID=price_...   # Price ID of the $79/year recurring price
STRIPE_LIFETIME_PRICE_ID=price_... # Price ID of the $199 one-time price

โš ๏ธ Only STRIPE_PUBLISHABLE_KEY and the price IDs are exposed to the frontend (via GET /config). The secret key and webhook secret are read server-side only and are never sent to the browser.

Variable Where to find it in Stripe
STRIPE_SECRET_KEY Developers โ†’ API keys โ†’ Secret key
STRIPE_PUBLISHABLE_KEY Developers โ†’ API keys โ†’ Publishable key
STRIPE_WEBHOOK_SECRET Developers โ†’ Webhooks โ†’ your endpoint โ†’ Signing secret
STRIPE_MONTHLY_PRICE_ID Product catalog โ†’ your monthly product โ†’ Price ID
STRIPE_YEARLY_PRICE_ID Product catalog โ†’ your annual product โ†’ Price ID
STRIPE_LIFETIME_PRICE_ID Product catalog โ†’ your lifetime product โ†’ Price ID

๐Ÿ› ๏ธ One-time Stripe setup

  1. Create three prices in the Stripe Dashboard (they may belong to one product or separate products):
    • Monthly โ†’ recurring price $9 / month โ†’ copy its Price ID into STRIPE_MONTHLY_PRICE_ID.
    • Annual โ†’ recurring price $79 / year โ†’ copy its Price ID into STRIPE_YEARLY_PRICE_ID.
    • Lifetime โ†’ one-time price $199 โ†’ copy its Price ID into STRIPE_LIFETIME_PRICE_ID.
  2. Copy your API keys into STRIPE_SECRET_KEY and STRIPE_PUBLISHABLE_KEY.
  3. Configure the webhook (see below) and copy its signing secret into STRIPE_WEBHOOK_SECRET.
  4. (Recommended) Run the database migration to backfill subscription fields:
    Bash
    cd database
    npx migrate-mongo up
    
  5. Set SAAS=true and restart the app.

Stripe Payment Links are not required. The application creates a customer-specific Checkout Session for the selected Price ID and uses its metadata to activate the correct plan.

Webhook endpoint

Text Only
POST {SERVER_URL}/api/stripe/webhook

Subscribe to at least these events:

  • checkout.session.completed
  • customer.subscription.created
  • customer.subscription.updated
  • customer.subscription.deleted

๐Ÿงช Development / testing

Use Stripe test mode (keys starting with sk_test_ / pk_test_).

  1. Set the test keys and price IDs in .env, with SAAS=true.
  2. Forward webhooks to your local server with the Stripe CLI:
    Bash
    stripe login
    stripe listen --forward-to localhost:9002/api/stripe/webhook
    
    The CLI prints a whsec_... signing secret โ€” put it in STRIPE_WEBHOOK_SECRET and restart the app.
  3. Start the app:
    Bash
    npm start
    
  4. Log in as a normal (non-admin, non-demo) user, go to /pricing, and pay with a test card:
    • Card: 4242 4242 4242 4242
    • Expiry: any future date ยท CVC: any 3 digits ยท ZIP: any
  5. Verify the flow:
    • Lifetime โ†’ subscriptionType = lifetime, permanent access.
    • Monthly โ†’ subscriptionType = monthly, renewal date shown in Account โ†’ Billing.
    • Annual โ†’ subscriptionType = yearly, annual renewal date shown in Account โ†’ Billing.
    • Upgrade Monthly to Annual โ†’ the existing subscription changes to Annual and the prorated difference is invoiced.
    • Cancel via Manage Subscription (Stripe Billing Portal) โ†’ the account shows Cancels on while access remains available through the paid period, then /client redirects to /pricing after expiration.
    • Upgrade Monthly to Lifetime โ†’ the recurring subscription is canceled automatically after Lifetime payment activates.

๐Ÿ’ก To test admin/demo bypass, log in with the ADMIN_* or USER_DEMO_* credentials โ€” they always have full access regardless of subscription.


๐Ÿš€ Production

  1. Switch to live keys (sk_live_ / pk_live_) and live Price IDs.
  2. Create a live webhook endpoint pointing at {SERVER_URL}/api/stripe/webhook and use its live signing secret in STRIPE_WEBHOOK_SECRET.
  3. Run the migration (npx migrate-mongo up) before enabling SAAS=true.
  4. Set SAAS=true and deploy/restart.

Notes

  • The migration is recommended but not strictly required: missing subscription fields are treated as "no active subscription", so existing users are safely gated until they subscribe.
  • Switching back to SAAS=false instantly disables all subscription checks โ€” useful for self-hosted installs.