Narrative flows for every billing touchpoint a user encounters on LogicSpike.
Finalized Decisions
| # | Decision | Detail |
|---|---|---|
| 1 | Monthly / Yearly toggle | Pricing page has two tabs. Yearly discount % is customizable per plan (stored in plans.yearly_discount_pct). |
| 2 | Free trial | 1-month free trial of Pro on first upgrade. Card/mandate captured upfront (Razorpay). Cancellable anytime during trial. |
| 3 | Predefined plans + Coin system | Fixed plans (Free / Starter / Pro / Business). Tenants can top-up LogicSpike Coins to buy extras (storage, seats, email sends) without upgrading the whole plan. |
| 4 | Currency | INR (Razorpay, current). USD/international via Stripe (Phase 8). |
| 5 | Coupon / promo codes | Razorpay: create coupons as subscription discounts. Stripe: Promotion Codes at checkout (Phase 8). |
| 6 | Hybrid seat model | Free & Starter = flat with fixed seats. Pro = flat base + ₹400/extra seat/mo. Business = per-seat (₹1,000/seat/mo). |
| 7 | Tax | Razorpay GST (configured in Dashboard). Stripe Tax (automatic_tax: true) when Stripe is added (Phase 8). |
| 8 | Invoice customization | v1: Company Name + Tax ID (VAT/GST) in billing settings → provider puts on invoice PDF. v2: billing address, PO number for enterprise. |
Personas
| Name | Role | Context |
|---|---|---|
| Ayva | Owner of "TechStartup" | Free tier, growing team, ready to upgrade |
| Raj | Admin of "AgencyHub" | Pro plan, managing multiple clients |
| Sam | New signup | Just created an account, first workspace |
Plans & Pricing
Pricing Table (Monthly / Yearly Toggle)
| Free | Starter ($12/mo) | Pro ($29/mo) | Business ($79/mo) | |
|---|---|---|---|---|
| Yearly price | Free | $10/mo (billed $120/yr) | $24/mo (billed $288/yr) | $65/mo (billed $780/yr) |
| Savings | — | 17% off | 17% off | 18% off |
| Blog posts | 10 | 50 | Unlimited | Unlimited |
| Media storage | 500 MB | 5 GB | 25 GB | 100 GB |
| Team seats (included) | 2 | 5 | 10 | 25 |
| Extra seat cost | ❌ | ❌ | $5/seat/mo | $12/seat/mo (pure per-seat) |
| Custom roles | ❌ | ❌ | ✅ | ✅ |
| API keys | 1 | 3 | 10 | Unlimited |
| Custom domain | ❌ | ❌ | ✅ | ✅ |
| Priority support | ❌ | ❌ | ❌ | ✅ |
| Free trial | — | — | ✅ 1 month | ✅ 1 month |
LogicSpike Coins (Top-Up System)
Workspace owners can purchase coins to buy extras without upgrading their plan:
| Coin Pack | Price | Coins |
|---|---|---|
| Small | $5 | 500 coins |
| Medium | $20 | 2,200 coins (10% bonus) |
| Large | $50 | 6,000 coins (20% bonus) |
| Extra | Coin Cost | Equivalent |
|---|---|---|
| +1 GB storage | 100 coins | ~$1 |
| +1 team seat (monthly) | 250 coins | ~$2.50 |
| +100 email sends | 50 coins | ~$0.50 |
| +10 blog posts | 75 coins | ~$0.75 |
| Custom domain add-on | 500 coins/mo | ~$5/mo |
Design: Coins never expire. Coins belong to the workspace, not to individual users. Seat/domain add-ons purchased with coins are monthly recurring (deducted automatically). When coins run out → add-on pauses, workspace owner notified to top-up.
Journey 1 — New User Gets a Free Plan Automatically
Persona: Sam
Trigger: Created first workspace "SamBlog".
- System auto-provisions on Free plan. No card required.
- Dashboard shows "Free Plan" badge in sidebar.
- Limits active from Day 1 (10 posts, 500 MB, 2 seats).
- Subtle "Upgrade" button in sidebar (not aggressive).
System:
tenants.plan_id = 'free',subscriptionscreated withstatus: 'active',current_period_end: null.tenant_servicespopulated fromplan_service_limits.
Journey 2 — Owner Explores Pricing (Monthly / Yearly Toggle)
Persona: Ayva
Trigger: Hits a limit → toast: "Your Free plan allows 2 members. Upgrade to add more."
- Toast has "View Plans" button →
/dashboard/settings/billing. - Plans tab shows pricing cards with Monthly / Yearly toggle at the top.
- Switching to Yearly shows discounted prices + "Save X%" badges (% read from
plans.yearly_discount_pct). - Current plan highlighted with "Current Plan" badge.
- Pro & Business show "Start Free Trial" (if never trialed before) instead of "Upgrade".
- Ayva clicks "Start Free Trial" on Pro.
Journey 3 — Free Trial + Checkout (Card Required Upfront)
Persona: Ayva (continued)
Trigger: Clicked "Start Free Trial" on Pro.
- Info modal: "Try Pro free for 30 days. You won't be charged until the trial ends. Cancel anytime."
- Backend calls
POST /billing/checkout→ returns Razorpay modal params (subscription_id,key_id). - Razorpay Checkout modal opens on the same page (no redirect). Ayva sees:
- Plan description + amount
- UPI / Card / Net Banking options
- Trial notice
- Ayva pays (or sets up mandate for deferred trial charge).
- Frontend calls
POST /billing/payment/verifywithrazorpay_signature→ backend verifies HMAC and activates subscription. - Webhook
subscription.activatedfires asynchronously (idempotent). - Backend:
subscriptionsrow:status: 'trialing',trial_end: <30 days out>,plan_id: 'pro'.tenant_servicesrebuilt with Pro limits.
- UI: "Pro Plan (Trial)" badge, "Trial ends Feb 27" notice in billing settings.
- Day 28: System shows banner: "Your trial ends in 2 days. You'll be charged ₹2,499/mo after."
- Day 30: Razorpay auto-charges →
subscription.chargedwebhook →status: 'active'. Seamless.
Trial Cancellation:
- Ayva cancels during trial → webhook
subscription.cancelled→ downgrade to Free immediately. - No charge. Mandate released.
Phase 8 (Stripe): For international customers,
POST /billing/checkoutwill return{ checkout_url }instead. Frontend redirects to Stripe, then returns.
Journey 4 — Coin Top-Up
Persona: Ayva
Trigger: Ayva is on Pro but needs 5 GB more storage without upgrading to Business.
- Goes to billing → "Coins & Add-ons" tab.
- Sees current coin balance: 0 coins.
- Clicks "Buy Coins" → coin pack selector (Small / Medium / Large).
- Selects Medium (2,200 coins for $20) → Stripe Checkout (one-time payment).
- Webhook
checkout.session.completed→ backend credits2,200 coinstotenant_coins.balance. - Back to Coins tab → clicks "Buy Extra" → "+5 GB storage (500 coins)".
- Confirms → 500 coins deducted →
tenant_servicesstorage limit increased by 5 GB. - Balance: 1,700 coins remaining.
System: New table
tenant_coinstracks balance + transaction history.coin_transactionstable logs every credit/debit with reason.
Journey 5 — Monthly Renewal (Happy Path)
Persona: Raj
Trigger: Monthly auto-charge by Stripe.
- Webhook
invoice.payment_succeededfires. - Backend updates
subscriptions.current_period_end. - No UI interruption — seamless.
- Invoice available in billing → "Invoices" tab (fetched from Stripe).
- Invoice includes Company Name + Tax ID if set by tenant.
Journey 6 — Payment Fails (Past Due)
Persona: Raj
Trigger: Card expired, Stripe retry fails.
- Webhook
invoice.payment_failed→subscriptions.status = 'past_due'. - Sitewide warning banner:
⚠️ "Your payment failed. Update your payment method to avoid service interruption." [Update Payment →]
- Grace period: 7 days. All features remain active.
- "Update Payment" → Stripe Customer Portal (
billing_portal/sessions). - Stripe retries 3 times over 7 days.
- Resolved:
invoice.payment_succeeded→ status back to'active'→ banner gone. - Not resolved: → Journey 7 (downgrade).
Journey 7 — Involuntary Downgrade
Trigger: Stripe exhausts retries, subscription canceled.
- Webhook
customer.subscription.deletedfires. - Backend:
subscriptions.status = 'canceled',tenants.plan_id = 'free',tenant_servicesrebuilt. - Red banner:
🔴 "Your subscription was canceled due to payment failure. Workspace downgraded to Free."
- Soft enforcement — NO data deleted:
- 50 posts but Free allows 10 → existing preserved, can't create new.
- 8 members but Free allows 2 → existing stay, can't invite more.
- Coin-purchased add-ons paused until re-subscription or coin top-up.
- Re-subscribe button available.
Principle: We NEVER delete user data on downgrade. Only block new creation.
Journey 8 — Voluntary Plan Change
8a — Upgrade (Starter → Pro)
- Billing settings → "Change Plan" → selects Pro.
- Stripe prorates — charges difference for remaining cycle.
- Webhook →
tenant_servicesrebuilt with Pro limits. Immediate effect.
8b — Downgrade (Pro → Starter)
- Billing settings → "Change Plan" → selects Starter.
- Warning: "Your plan changes to Starter at the end of this billing period. No refund for unused time."
subscriptions.cancel_at_period_end = true,pending_plan_id = 'starter'.- At period end → Stripe webhook → plan changes → limits adjusted. Soft enforcement applies.
8c — Switch Billing Cycle (Monthly ↔ Yearly)
- Billing settings → "Switch to Yearly" / "Switch to Monthly".
- Yearly → immediate charge of discounted annual amount (prorated credit for unused monthly).
- Monthly → changes at end of current annual period.
Journey 9 — Billing Dashboard
Persona: Ayva at /dashboard/settings/billing.
Tab 1 — Overview
- Current plan + badge (Free / Starter / Pro / Business)
- Billing cycle: Monthly or Yearly
- Next billing date + amount
- Usage meters (posts used/limit, storage, members, API keys)
- "Manage Subscription" → Stripe Customer Portal
- "Change Plan" → plan comparison with Monthly/Yearly toggle
Tab 2 — Plans
- Pricing cards rendered from
GET /billing/plansresponse - Monthly / Yearly toggle — shows
yearly_discount_pctas "Save X%" badge - Current plan highlighted with "Current Plan" badge
- "Start Free Trial" vs "Upgrade" CTA logic (based on
has_used_trial) - Click "Upgrade" →
POST /billing/checkout→ backend returns Razorpay modal params - Razorpay checkout modal opens inline (no redirect)
- After payment →
POST /billing/payment/verify→ subscription activated - Downgrade → warning modal →
POST /billing/change-plan
Tab 3 — Invoices
- Table: Date, Amount, Status (Paid/Failed/Pending), Tax, Download PDF
- Fetched from Stripe API — PDF hosted by Stripe
- Tenant's Company Name + Tax ID shown on invoices
Tab 4 — Coins & Add-ons
- Current coin balance (workspace-level)
- "Buy Coins" button → coin packs
- Active add-ons list (extra storage, seats, etc.)
- "Buy Extra" button → add-on marketplace
- Transaction history: credits (purchases) and debits (add-ons)
Tab 5 — Billing Info
- Company Name (editable)
- Tax ID / VAT / GST number (editable)
- Saved to Stripe Customer object → appears on invoices automatically
- v2: Billing address, PO number
Journey 10 — Usage Limit Enforcement (Soft Walls)
Strategy: Soft Walls, Not Hard Blocks
| Scenario | Behavior |
|---|---|
| Create 11th post on Free (limit: 10) | Modal: "Post limit reached. Upgrade or buy extra with coins." |
| Invite 3rd member on Free (limit: 2) | Toast: "Seat limit reached. Upgrade or buy a seat with coins." |
| Storage at 95%+ | Warning banner: "Storage almost full (480 MB / 500 MB)." |
| Storage at 100% | Upload blocked. Existing files untouched. |
| 2nd API key on Free (limit: 1) | Block with upgrade prompt. |
Technical enforcement:
- Service checks
tenant_services.limitsbefore allowing action. - Counts current usage (
SELECT COUNT(*) FROM blog_posts WHERE tenant_id = ?). - If
current >= limit→ return{ error: "PLAN_LIMIT_REACHED", limit, current, upgrade_url }. - Frontend catches this → shows upgrade prompt with plan comparison + coin purchase option.
Journey 11 — Promo Code at Checkout
Trigger: Ayva has a promo code "LAUNCH50".
- During Stripe Checkout → "Add promotion code" field (enabled via
allow_promotion_codes: true). - Enters "LAUNCH50" → Stripe validates → shows "50% off first 3 months".
- Completes checkout → discount applied automatically.
- Promo codes managed entirely in Stripe Dashboard — no backend code needed.
Journey 12 — Non-Owner Billing Access
Trigger: Team member navigates to /dashboard/settings/billing.
| Permission | Access |
|---|---|
| No billing permissions | 🚫 Access Denied page |
billing:invoices.read |
View-only: see invoices + current plan |
billing:plans.read |
View-only: see plans comparison |
| Owner | Full access: upgrade, downgrade, manage subscription, buy coins |
Journey 13 — Re-subscription After Cancellation
Persona: Raj
Trigger: Raj's subscription was canceled (involuntary downgrade via Journey 7). He wants to re-subscribe.
- Billing dashboard shows 🔴 banner: "Your workspace was downgraded to Free. Re-subscribe to restore your plan."
- Clicks "Re-subscribe" → Plans tab → selects Pro again.
- System reuses the existing
stripe_customer_id(no new Stripe Customer created). - Trial eligibility:
has_used_trial = true→ no free trial offered. Goes straight to paid checkout. - Stripe Checkout → new
sub_...created → newsubscriptionsrow. - Webhook
checkout.session.completed→rebuildEntitlements()→ Pro limits restored. - Paused add-ons: Remain paused. Raj must manually re-enable them from the Coins & Add-ons tab (coins must be sufficient).
System notes:
- Old subscription ID is kept in
subscriptionswithstatus: 'canceled'(historical record).- New subscription gets a new row with
status: 'active'.- Existing data (posts, files, members) is immediately accessible again once limits are restored.
Journey 14 — Email Notifications (via Communication Service)
Billing events trigger emails to the workspace owner (not all members).
| Trigger | Timing | |
|---|---|---|
| Successful checkout | "Welcome to [Plan Name]!" | Immediate |
| Trial started | "Your free trial of [Plan] has started. You have 30 days." | Immediate |
| Trial ending | "Your trial ends in 3 days. You'll be charged [amount]." | 3 days before trial end |
| Payment succeeded | (none — handled by Stripe receipt) | — |
| Payment failed | "Your payment failed. Update your payment method." | Immediate |
| Subscription canceled | "Your workspace has been downgraded to Free." | Immediate |
| Coin balance low | "Your coin balance is below 100. Top up to keep add-ons active." | When balance < 100 |
| Add-on paused | "Your [addon] add-on has been paused due to insufficient coins." | Immediate |
Implementation: Stripe sends its own dunning emails for payment failures. LogicSpike emails are supplementary and sent via the Communication service.
DB Schema
All billing tables (subscriptions, tenant_services, tenant_coins, coin_transactions, tenant_addons, processed_stripe_events, plans, services, service_limits, plan_service_limits, coin_packs, addon_catalog) are fully defined in architecture.md.