{"id":30415464,"url":"https://github.com/rameerez/pricing_plans","last_synced_at":"2026-02-15T21:05:21.378Z","repository":{"id":310691721,"uuid":"1033904581","full_name":"rameerez/pricing_plans","owner":"rameerez","description":"💵 Define and enforce pricing plan limits in your Rails SaaS (entitlements, quotas, feature gating)","archived":false,"fork":false,"pushed_at":"2025-08-19T17:54:01.000Z","size":741,"stargazers_count":49,"open_issues_count":2,"forks_count":2,"subscribers_count":1,"default_branch":"main","last_synced_at":"2025-10-05T22:43:31.214Z","etag":null,"topics":["business","gem","gumroad","lemonsqueezy","limits","paddle","payment","payments","pricing","pricing-table","quota","rails","ruby","ruby-gem","ruby-on-rails","saas","saas-tools","stripe","stripe-api","stripe-payments"],"latest_commit_sha":null,"homepage":"","language":"Ruby","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/rameerez.png","metadata":{"files":{"readme":"README.md","changelog":"CHANGELOG.md","contributing":null,"funding":null,"license":"LICENSE.txt","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null}},"created_at":"2025-08-07T14:25:07.000Z","updated_at":"2025-09-16T17:03:29.000Z","dependencies_parsed_at":"2025-08-19T18:17:24.923Z","dependency_job_id":"334e226f-abe2-4f89-9aba-5040e23e8a34","html_url":"https://github.com/rameerez/pricing_plans","commit_stats":null,"previous_names":["rameerez/pricing_plans"],"tags_count":1,"template":false,"template_full_name":null,"purl":"pkg:github/rameerez/pricing_plans","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/rameerez%2Fpricing_plans","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/rameerez%2Fpricing_plans/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/rameerez%2Fpricing_plans/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/rameerez%2Fpricing_plans/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/rameerez","download_url":"https://codeload.github.com/rameerez/pricing_plans/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/rameerez%2Fpricing_plans/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":278881399,"owners_count":26062175,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","status":"online","status_checked_at":"2025-10-08T02:00:06.501Z","response_time":56,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["business","gem","gumroad","lemonsqueezy","limits","paddle","payment","payments","pricing","pricing-table","quota","rails","ruby","ruby-gem","ruby-on-rails","saas","saas-tools","stripe","stripe-api","stripe-payments"],"created_at":"2025-08-22T04:02:20.949Z","updated_at":"2026-02-15T21:05:21.372Z","avatar_url":"https://github.com/rameerez.png","language":"Ruby","readme":"# 💵 `pricing_plans` - Define and enforce pricing plan limits in your Rails app (SaaS entitlements)\n\n[![Gem Version](https://badge.fury.io/rb/pricing_plans.svg)](https://badge.fury.io/rb/pricing_plans) [![Build Status](https://github.com/rameerez/pricing_plans/workflows/Tests/badge.svg)](https://github.com/rameerez/pricing_plans/actions)\n\n\u003e [!TIP]\n\u003e **🚀 Ship your next Rails app 10x faster!** I've built **[RailsFast](https://railsfast.com/?ref=pricing_plans)**, a production-ready Rails boilerplate template that comes with everything you need to launch a software business in days, not weeks. Go [check it out](https://railsfast.com/?ref=pricing_plans)!\n\n`pricing_plans` allows you to enforce pricing plan limits with one-liners that read like plain English. Avoid scattering and entangling pricing logic everywhere in your Rails SaaS.\n\nFor example, this is how you define pricing plans and their entitlements:\n```ruby\nplan :pro do\n  allows :api_access      # Features: blocked by default unless explicitly allowed\n  limits :projects, to: 5 # Limits: 0 by default unless a limit is set explicitly\nend\n```\n\nPlans are **secure by default**: features are disabled and limits are set to 0 unless explicitly configured.\n\nYou can then gate features in your controllers:\n```ruby\nbefore_action :enforce_api_access!, only: [:create]\n```\n\nDo one-liner checks to hide / show conditional UI:\n\n```ruby\n\u003c% if current_user.within_plan_limits?(:projects) %\u003e\n  ...\n\u003c% end %\u003e\n```\n\nOr check limits and feature access anywhere in your app:\n\n```ruby\n@user.plan_allows_api_access?  # =\u003e true / false\n@user.projects_remaining       # =\u003e 2\n```\n\n`pricing_plans` is your single source of truth for pricing plans, so you can use it to [build pricing pages and paywalls](/docs/04-views.md) too.\n\n![pricing_plans Ruby on Rails gem - pricing table features](/docs/images/pricing_plans_ruby_rails_gem_pricing_table.jpg)\n\n\nThe gem works standalone, and it also plugs nicely into popular gems: it works seamlessly out of the box if you're already using [`pay`](https://github.com/pay-rails/pay) or [`usage_credits`](https://github.com/rameerez/usage_credits/). More info [here](/docs/06-gem-compatibility.md).\n\n## Quickstart\n\nAdd this to your Gemfile:\n\n```ruby\ngem \"pricing_plans\"\n```\n\nThen install the gem:\n\n```bash\nbundle install\n```\n\nAfter that, generate and run [the required migration](#why-the-models):\n\n```bash\nrails g pricing_plans:install\nrails db:migrate\n```\n\nThis will also create a `config/initializers/pricing_plans.rb` file where you need to [define your pricing plans](/docs/01-define-pricing-plans.md).\n\nThen, just add the model mixin to the plan owner, that is: the actual model on which limits should be enforced (`User`, `Organization`, etc.):\n\n```ruby\nclass User \u003c ApplicationRecord\n  include PricingPlans::PlanOwner\nend\n```\n\nThis mixin will automatically give your plan owner model the [model helpers and methods](/docs/03-model-helpers.md) you can use to consistently check and enforce limits:\n```ruby\nclass User \u003c ApplicationRecord\n  include PricingPlans::PlanOwner\n\n  has_many :projects, limited_by_pricing_plans: { error_after_limit: \"Too many projects for your plan!\" }, dependent: :destroy\nend\n```\n\nYou also get [controller helpers](/docs/02-controller-helpers.md):\n\n```ruby\nbefore_action { gate_feature!(:api_access) }\n\n# or with syntactic sugar:\n\nbefore_action :enforce_api_access!\n```\n\nAnd you also get a lot of [view helpers and methods](/docs/04-views.md) to check limits in your views for conditional UI, and to build usage meters, usage warnings, and a handful of other useful UI components.\n\n![pricing_plans Ruby on Rails gem - pricing plan usage meter](/docs/images/pricing_plans_ruby_rails_gem_usage_meter.jpg)\n\nYou can also display upgrade alerts to prompt users into upgrading to the next plan when they're near their plan limits:\n\n![pricing_plans Ruby on Rails gem - pricing plan upgrade prompt](/docs/images/pricing_plans_ruby_rails_gem_usage_alert_upgrade.jpg)\n\nYou can attach arbitrary plan `metadata` for UI/presentation needs (icons, colors, badges) directly in the initializer:\n\n```ruby\nplan :hobby do\n  metadata icon: \"rocket\", color: \"bg-red-500\"\nend\n\nplan.metadata[:icon] # =\u003e \"rocket\"\n```\n\nYou can also grandfather users into old plans (hidden to other users), assign plans manually without requiring a payment (for testing, gifts, or employees), and much more!\n\n## 🤓 Read the docs!\n\n\u003e [!IMPORTANT]  \n\u003e This gem has extensive docs. Please 👉 [read the docs here](/docs/01-define-pricing-plans.md) 👈\n\n## What `pricing_plans` does and doesn't do\n\n`pricing_plans` handles pricing plan entitlements; that is: what a user can and can't access based on their current SaaS plan.\n\nSome other features you may like:\n - Grace periods (hard \u0026 soft caps for limits)\n - Customizable downgrade behavior for overage handling\n - Row-level locks to prevent race conditions on quota enforcement\n\nHere's what `pricing_plans` **does not** handle:\n  - Payment processing / billing (that's [`pay`](https://github.com/pay-rails/pay) or Stripe's responsibility)\n  - Price definition / currency handling (that's Stripe / payment processor)\n  - Usage credits / metered usage (that's [`usage_credits`](https://github.com/rameerez/usage_credits/)'s responsibility)\n  - Feature flags for A/B testing or staged rollouts (that's `flipper`)\n  - User roles, authorization, or per-user permissions (that's `cancancan` or `pundit`)\n\n## 🤔 Why this gem exists\n\nIf you've ever had to implement pricing plan limits, you probably found yourself writing code like this everywhere in your app:\n\n```ruby\nif user_signed_in? \u0026\u0026 current_user.payment_processor\u0026.subscription\u0026.processor_plan == \"pro\" \u0026\u0026 current_user.projects.count \u003c= 5\n  # ...\nelsif user_signed_in? \u0026\u0026 current_user.payment_processor\u0026.subscription\u0026.processor_plan == \"premium\" \u0026\u0026 current_user.projects.count \u003c= 10\n  # ...\nend\n```\n\nYou end up duplicating this kind of snippet for every plan and feature, and for every view and controller.\n\nThis code is brittle, tends to be full of magical numbers and nested convoluted logic; and plan enforcement tends to be scattered across the entire codebase. If you change something in your pricing table, it's highly likely you'll have to change the same magical number or logic in many different places, leading to bugs, inconsistencies, customer support tickets, and maintenance hell.\n\nEnforcing pricing plan limits in code (through entitlements, usage quotas, and feature gating) is tedious and painful plumbing. Every SaaS needs to check whether users can perform an action based on the plan they're currently subscribed to, but it often leads to brittle, scattered, unmaintainable pricing logic that gets entangled with core application code, opening gaps for under-enforcement and leaving money on the table.\n\nIntegrating payment processing (Stripe, `pay`, etc.) is relatively straightforward, but enforcing actual plan limits (ensure users only get the features and usage their tier allows) is a whole different task. It's the kind of plumbing no one wants to do. Founders often put their focus on capturing the payment, and then default to a \"poor man's\" implementation of per-plan entitlements. Maintaining these in-house DIY solutions is a huge time sink, and engineers often can't keep up with constant pricing or packaging changes.\n\n`pricing_plans` aims to offer a centralized, single-source-of-truth way of defining \u0026 handling pricing plans, so you can enforce plan limits with reusable helpers that read like plain English.\n\n\n## Why the models?\n\nThe `pricing_plans` gem needs three new models in the schema in order to work: `Assignment`, `EnforcementState`, and `Usage`. Why are they needed?\n\n- `PricingPlans::Assignment` allow manual plan overrides independent of billing system (or before you wire up Stripe/Pay). Great for admin toggles, trials, demos.\n  - What: The arbitrary `plan_key` and a `source` label (default \"manual\"). Unique per plan_owner.\n  - How it's used: `PlanResolver` checks manual assignment → Pay → default plan. Manual assignments (admin overrides) take precedence over subscription-based plans. You can call `assign_pricing_plan!` and `remove_pricing_plan!` on the plan_owner.\n\n- `PricingPlans::EnforcementState` tracks per-plan_owner per-limit enforcement state for persistent caps and per-period allowances (grace/warnings/block state) in a race-safe way.\n  - What: `exceeded_at`, `blocked_at`, last warning info, and a small JSON `data` column where we persist plan-derived parameters like grace period seconds.\n  - How it’s used: When you exceed a limit, we upsert/read this row under row-level locking to start grace, compute when it ends, flip to blocked, and to ensure idempotent event emission (`on_warning`, `on_grace_start`, `on_block`).\n\n- `PricingPlans::Usage` tracks per-period allowances (e.g., “3 projects per month”). Persistent caps don’t need a table because they are live counts.\n  - What: `period_start`, `period_end`, and a monotonic `used` counter with a last-used timestamp.\n  - How it’s used: On create of the metered model, we increment or upsert the usage for the current window (based on `PeriodCalculator`). Reads power `remaining`, `percent_used`, and warning thresholds.\n\n## Gem features\n\nEnforcing pricing plans is one of those boring plumbing problems that look easy from a distance but get complex when you try to engineer them for production usage. The poor man's implementation of nested ifs shown in the example above only get you so far, you soon start finding edge cases to consider. Here's some of what we've covered in this gem:\n\n- Safe under load: we use row locks and retries when setting grace/blocked/warning state, and we avoid firing the same event twice. See [grace_manager.rb](lib/pricing_plans/grace_manager.rb).\n\n- Accurate counting: persistent limits count live current rows (using `COUNT(*)`, make sure to index your foreign keys to make it fast at scale); per‑period limits record usage for the current window only. You can filter what counts with `count_scope` (Symbol/Hash/Proc/Array), and plan settings override model defaults. See [limitable.rb](lib/pricing_plans/limitable.rb) and [limit_checker.rb](lib/pricing_plans/limit_checker.rb).\n\n- Clear rules: default is to block when you hit the cap; grace periods are opt‑in. In status/UI, 0 of 0 isn’t shown as blocked. See [plan.rb](lib/pricing_plans/plan.rb), [grace_manager.rb](lib/pricing_plans/grace_manager.rb), and [view_helpers.rb](lib/pricing_plans/view_helpers.rb).\n\n- Simple controllers: one‑liners to guard actions, predictable redirect order (per‑call → per‑controller → global → pricing_path), and an optional central handler. See [controller_guards.rb](lib/pricing_plans/controller_guards.rb).\n\n- Billing‑aware periods: supports billing cycle (when Pay is present), calendar month/week/day, custom time windows, and durations. See [period_calculator.rb](lib/pricing_plans/period_calculator.rb).\n\n\n## Downgrades and overages\n\nWhen a customer moves to a lower plan (via Stripe/Pay or manual assignment), the new plan’s limits start applying immediately. Existing resources are never auto‑deleted by the gem; instead:\n\n- **Persistent caps** (e.g., `:projects, to: 3`): We count live rows. If the account is now over the new cap, creations will be blocked (or put into grace/warn depending on `after_limit`). Users must remediate by deleting/archiving until under cap.\n- \n- **Per‑period allowances** (e.g., `:custom_models, to: 3, per: :month`): The current window’s usage remains as is. Further creations in the same window respect the downgraded allowance and `after_limit` policy. At the next window, the allowance resets.\n\nUse `OverageReporter` to present a clear remediation UX before or after applying a downgrade:\n\n```ruby\nreport = PricingPlans::OverageReporter.report_with_message(org, :free)\nif report.items.any?\n  flash[:alert] = report.message\n  # report.items -\u003e [#\u003cOverageItem limit_key:, kind: :persistent|:per_period, current_usage:, allowed:, overage:, grace_active:, grace_ends_at:\u003e]\nend\n```\n\nExample human message:\n- \"Over target plan on: projects: 12 \u003e 3 (reduce by 9), custom_models: 5 \u003e 0 (reduce by 5). Grace active — projects grace ends at 2025-01-06T12:00:00Z.\"\n\nNotes:\n- If you provide a `config.message_builder`, it’s used to customize copy for the `:overage_report` context.\n- This reporter works regardless of whether any controller/model action has been hit; it reads live counts and current period usage.\n\n### Override checks\n\nSome times you'll want to override plan limits / feature gating checks. A common use case is if you're responding to a webhook (like Stripe), you'll want to process the webhook correctly (bypassing the check) and maybe later handle the limit manually.\n\nTo do that, you can use `require_plan_limit!`. An example to proceed but mark downstream:\n\n```ruby\ndef webhook_create\n  result = require_plan_limit!(:projects, plan_owner: current_organization, allow_system_override: true)\n\n  # Your custom logic here.\n  # You could proceed to create; inspect result.grace?/warning? and result.metadata[:system_override]\n  Project.create!(metadata: { created_during_grace: result.grace? || result.warning?, system_override: result.metadata[:system_override] })\n\n  head :ok\nend\n```\n\nNote: model validations will still block creation even with `allow_system_override` -- it's just intended to bypass the block on controllers.\n\n## Testing\n\nWe use Minitest for testing. Run the test suite with:\n\n```bash\nbundle exec rake test\n```\n\n## Development\n\nAfter checking out the repo, run `bin/setup` to install dependencies. Then, run `rake test` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.\n\nTo install this gem onto your local machine, run `bundle exec rake install`.\n\n## Contributing\n\nBug reports and pull requests are welcome on GitHub at https://github.com/rameerez/pricing_plans. Our code of conduct is: just be nice and make your mom proud of what you do and post online.\n\n## License\n\nThe gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).\n","funding_links":[],"categories":["Ruby"],"sub_categories":[],"project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Frameerez%2Fpricing_plans","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Frameerez%2Fpricing_plans","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Frameerez%2Fpricing_plans/lists"}