๐ณ 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
- A logged-in user without an active subscription is redirected to
/pricing. - They choose a plan and are sent to Stripe Checkout.
- After payment they return to the app, the subscription is activated, and they land directly in the dashboard (
/client). - 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/pricingif 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):
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_KEYand the price IDs are exposed to the frontend (viaGET /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
- 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 intoSTRIPE_MONTHLY_PRICE_ID. - Annual โ recurring price
$79 / yearโ copy its Price ID intoSTRIPE_YEARLY_PRICE_ID. - Lifetime โ one-time price
$199โ copy its Price ID intoSTRIPE_LIFETIME_PRICE_ID.
- Monthly โ recurring price
- Copy your API keys into
STRIPE_SECRET_KEYandSTRIPE_PUBLISHABLE_KEY. - Configure the webhook (see below) and copy its signing secret into
STRIPE_WEBHOOK_SECRET. - (Recommended) Run the database migration to backfill subscription fields:
Bash
cd database npx migrate-mongo up - Set
SAAS=trueand 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
POST {SERVER_URL}/api/stripe/webhook
Subscribe to at least these events:
checkout.session.completedcustomer.subscription.createdcustomer.subscription.updatedcustomer.subscription.deleted
๐งช Development / testing
Use Stripe test mode (keys starting with sk_test_ / pk_test_).
- Set the test keys and price IDs in
.env, withSAAS=true. - Forward webhooks to your local server with the Stripe CLI:
BashThe CLI prints a
stripe login stripe listen --forward-to localhost:9002/api/stripe/webhookwhsec_...signing secret โ put it inSTRIPE_WEBHOOK_SECRETand restart the app. - Start the app:
Bash
npm start - 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
- Card:
- 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
/clientredirects to/pricingafter expiration. - Upgrade Monthly to Lifetime โ the recurring subscription is canceled automatically after Lifetime payment activates.
- Lifetime โ
๐ก To test admin/demo bypass, log in with the
ADMIN_*orUSER_DEMO_*credentials โ they always have full access regardless of subscription.
๐ Production
- Switch to live keys (
sk_live_/pk_live_) and live Price IDs. - Create a live webhook endpoint pointing at
{SERVER_URL}/api/stripe/webhookand use its live signing secret inSTRIPE_WEBHOOK_SECRET. - Run the migration (
npx migrate-mongo up) before enablingSAAS=true. - Set
SAAS=trueand 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=falseinstantly disables all subscription checks โ useful for self-hosted installs.