Building Multi-Tier SaaS Plans: Roles, Permissions, and Billing Logic
Building Multi-Tier SaaS Plans: Roles, Permissions, and Billing Logic
Multi-tier plans are the backbone of SaaS pricing. But connecting plan tiers to feature access and user roles requires careful design. Get it wrong and users either get too much access (revenue loss) or too little (support tickets). Here's how to build it right.
Plan Architecture
Define Your Tiers
| Tier | Target User | Pricing | Key Differentiators |
|---|---|---|---|
| Free | Individual exploring | $0/mo | Basic features, limited usage |
| Starter | Solo professional | $19/mo | Core features, standard limits |
| Pro | Growing team | $49/mo | Advanced features, higher limits, team seats |
| Business | Scaling company | $99/mo | All features, priority support, custom limits |
Feature Matrix
| Feature | Free | Starter | Pro | Business |
|---|---|---|---|---|
| Core feature | 10/mo | 100/mo | 1,000/mo | Unlimited |
| Export to CSV | No | Yes | Yes | Yes |
| API access | No | No | Yes | Yes |
| Team members | 1 | 1 | 5 | 25 |
| Custom branding | No | No | No | Yes |
| Priority support | No | No | No | Yes |
Permission System
Feature Flags Approach
The simplest way to control access is a feature flag system tied to the user's plan:
Plan configuration object:
PLAN_FEATURES = {
free: { maxProjects: 3, maxTeamMembers: 1, hasApiAccess: false, hasExport: false },
starter: { maxProjects: 10, maxTeamMembers: 1, hasApiAccess: false, hasExport: true },
pro: { maxProjects: 50, maxTeamMembers: 5, hasApiAccess: true, hasExport: true },
business: { maxProjects: -1, maxTeamMembers: 25, hasApiAccess: true, hasExport: true },
}
Checking Permissions
On every request that involves a gated feature:
- Get the user's current plan from the database
- Look up the plan's permissions in the config
- Allow or deny the action
Server-side (critical): Always check plan permissions on the backend. Client-side checks are for UX only (showing/hiding UI elements).
Client-side (UX): Hide or disable features the user can't access. Show an upgrade prompt instead.
Team Roles (Within a Plan)
| Role | Permissions | Use Case |
|---|---|---|
| Owner | Everything + billing + delete account | Account creator |
| Admin | Everything except billing and account deletion | Team leads |
| Member | Use features, no settings changes | Regular team members |
| Viewer | Read-only access | Stakeholders, clients |
Implementation
Usertable has arolecolumnTeamMemberjoin table connects users to teams with a role- Middleware checks both plan permissions AND user role
Billing Logic
Connecting Plans to Stripe
| Your App | Stripe |
|---|---|
| Plan name (free, starter, pro, business) | Product ID |
| Monthly/annual | Price ID |
| User's active plan | Subscription status + Price ID |
Enforcement Flow
User tries to access a feature
→ Check user's subscription status (active? trialing?)
→ Check user's plan (which price ID?)
→ Map price ID to plan tier
→ Check plan tier's permissions
→ Allow or deny + show upgrade prompt
Upgrade Prompts
When a user hits a plan limit, show a contextual upgrade prompt:
Good: "You've used 10/10 projects on the Free plan. Upgrade to Starter for up to 100 projects." Bad: "Upgrade to unlock more features." (too vague)
Best placement:
- Inline, at the point of action (user tries to create 11th project)
- Dashboard widget showing usage (8/10 projects used)
- Settings/billing page
Setting up billing? Read How to Build SaaS Billing With Stripe.
Need a boilerplate with plans? Browse on MVPHub.








