Multi-App Entitlement System for HeroSuite

Resolved 💬 2 comments Opened Jan 3, 2026 by rich-afd Closed Jan 3, 2026

Overview

Implement an entitlement system that allows subscription-based access to HeroSuite applications (Books, Sales) with tiered pricing based on user counts and contact limits.

Business Requirements

Subscription Tiers

| Tier | Users | Contact Limit | Apps | Price |
|------|-------|---------------|------|-------|
| Free | 1 | 10 | Books OR Sales | £0 |
| Explore | 2-5 | Unlimited | Books, Sales, or Both | TBD |
| Essential | 5-15 | Unlimited | Books, Sales, or Both | TBD |
| Elite | Unlimited | Unlimited | Books + Sales | TBD |

Upgrade Triggers

  • Free user adds 2nd user → Prompt for Explore tier
  • Explore exceeds 5 users → Prompt for Essential
  • Essential exceeds 15 users → Prompt for Elite
  • Any tier wants 2nd app → Prompt for bundle

---

Completed Work ✅

Phase 1: Foundation (Done)

Database:

  • [x] plans table with name, user_limit, contact_limit, features (JSONB), stripe_price_id
  • [x] Added plan_id and apps_enabled (array) to tenants table
  • [x] Seeder for Explore, Essential, Elite plans

Models:

  • [x] Plan model with feature checks (allows_app?, allows_books?, unlimited_users?)
  • [x] Tenant model methods:
  • free_tier? / current_plan_name
  • has_access_to?(app) / has_books_access? / has_sales_access?
  • user_limit / at_user_limit? / users_remaining
  • contact_limit / at_contact_limit? / contacts_remaining

Tests:

  • [x] 13 specs for Plan model
  • [x] 28 specs for Tenant entitlement methods
  • [x] Factories for plans and tenant traits

Files created/modified:

  • db/migrate/20260103150001_create_plans.rb
  • db/migrate/20260103150002_add_plan_to_tenants.rb
  • app/models/plan.rb
  • app/models/tenant.rb (extended)
  • db/seeds/plans.rb
  • spec/models/plan_spec.rb
  • spec/models/tenant_spec.rb
  • spec/factories/plans.rb
  • spec/factories/tenants.rb

---

Remaining Work

Phase 2: Controller Enforcement

  • [ ] Create EntitlementEnforcement concern

``ruby
module EntitlementEnforcement
def requires_entitlement(feature, **options)
before_action(**options) do
unless current_tenant.has_access_to?(feature)
redirect_to upgrade_path, alert: "Please upgrade to access this feature"
end
end
end
end
``

  • [ ] Apply to Books controllers (all current controllers)
  • [ ] Add user limit check when creating/inviting users
  • [ ] Add contact limit check when creating customers/vendors (free tier only)
  • [ ] Create upgrade prompt partial

Phase 3: Stripe Integration

  • [ ] Uncomment and configure pay gem in Gemfile
  • [ ] Create config/initializers/pay.rb
  • [ ] Set up Stripe products and prices for each tier
  • [ ] Implement webhook handler for subscription changes
  • [ ] Create Stripe::SyncSubscriptionService to sync plan changes
  • [ ] Add billing portal link for subscription management

Phase 4: UI Components

  • [ ] Upgrade prompt modal (shown when hitting limits)
  • [ ] Plan comparison page (/pricing or /plans)
  • [ ] Current plan indicator in navigation/settings
  • [ ] User limit warning in user management
  • [ ] Contact limit warning for free tier users

Phase 5: Sales App Foundation (Future)

  • [ ] Create apps/sales Rails engine structure
  • [ ] Implement shared Contact model (replaces separate Customer in Books)
  • [ ] Add CRM-specific ContactProfile extension
  • [ ] Apply entitlement enforcement to Sales controllers

---

Technical Notes

Data Model

# Free tier is implicit (plan_id = nil)
# Contact limit only applies to free tier
# apps_enabled defaults to ['books'] for all existing tenants

class Tenant
  FREE_TIER_USER_LIMIT = 1
  FREE_TIER_CONTACT_LIMIT = 10
end

Shared Contact Strategy (for Phase 5)

When Sales is built, the Customer and Vendor models will be unified into a Contact model with app-specific profile extensions:

Contact (shared)
├── Core: name, email, phone, address
├── has_one :crm_profile (Sales-specific)
└── has_one :accounting_profile (Books-specific)

---

Labels

  • enhancement
  • architecture
  • subscription

View original on GitHub ↗

This issue has 2 comments on GitHub. Read the full discussion on GitHub ↗