Spree Loyalty
A POS-agnostic points loyalty program for Spree Commerce, an open
source e-commerce platform built with Ruby on Rails. Customers earn points on completed orders
and redeem them for real Spree::StoreCredit — spendable immediately through the store's
existing Store Credit payment method, with no new checkout/payment code required.
Why POS-agnostic?
Most restaurant/retail loyalty platforms (Punchh, Square Loyalty, and similar) issue points as a program tied to one POS or one brand — a point earned under one system generally isn't redeemable under another, even across brands owned by the same company (Yum! Brands keeps Taco Bell Rewards, KFC's program, and Pizza Hut Rewards entirely separate, for example). This extension takes the opposite approach on purpose: loyalty lives entirely in Spree, independent of whatever payment gateway or point-of-sale system a store also happens to use.
What this does
- Points accrue automatically on order completion —
SpreeLoyalty::OrderCompletedSubscriberreacts to Spree's ownorder.completedevent; no coupling to any specific payment or POS integration. - Only logged-in customers earn points. Guest checkouts don't accrue — loyalty requires an account, the same way most real loyalty programs identify a customer.
- Points earn on
item_total, not the order total — delivery/shipping fees are excluded from the earn calculation by design. - Redemption grants real
Spree::StoreCredit, not a bespoke discount/coupon mechanism — reuses Spree's own mature, already-checkout-integrated store credit system instead of building a parallel one. - A full ledger, not just a running balance — every earn, redemption, and manual adjustment is
a
SpreeLoyalty::Transactionrow, so a balance is always reconstructable and auditable. - Admin visibility — read-only Loyalty Accounts and Loyalty Transactions pages, plus a manual points-adjustment action for service recovery (comping or correcting a balance).
- A small Store API surface for the storefront —
GET /api/v3/store/loyalty_accountandPOST /api/v3/store/loyalty_account/redeem, authenticated the same way any other customer Store API endpoint is.
Installation
-
Add this extension to your Gemfile with this line:
bundle add spree_loyalty -
Run the install generator
bundle exec rails g spree_loyalty:install -
Restart your server
If your server was running, restart it so that it can find the assets properly.
Configuration
SpreeLoyalty::Config (a Spree::Preferences::Configuration, settable the usual Spree way —
e.g. in a config/initializers/spree.rb block):
| Preference | Default | Meaning |
|---|---|---|
points_per_dollar |
1 |
Points earned per whole dollar of order.item_total. |
point_value_cents |
1 |
Cents of store credit granted per point redeemed (default: 100 points = $1.00). |
minimum_redemption_points |
100 |
Smallest redemption a customer can make in one request. |
SpreeLoyalty::Config.points_per_dollar = 2
SpreeLoyalty::Config.minimum_redemption_points = 50
How redemption reaches checkout
Redeeming points calls Spree::StoreCredit.create!(user:, store:, amount:, category: "Loyalty Rewards", ...) directly — the same model Spree's own gift card system uses. The store's existing
Spree::PaymentMethod::StoreCredit payment method (present in any standard Spree install) then
picks it up automatically at checkout. No new payment gateway, no new checkout step, no
storefront changes beyond a page to let the customer trigger the redemption.
Storefront integration
The two Store API endpoints this extension adds are enough to build a full "My Rewards" account
page: fetch the balance/config with a GET, submit a redemption with a POST. See this
extension's own reference implementation in the companion spree_storefront_web Next.js app
(/account/rewards) for the full pattern — a balance display, a redemption form, and an
order-confirmation "You earned N points on this order!" message computed from item_total * points_per_dollar (client-side, using the same math the real accrual runs server-side, since the
subscriber that actually credits the account runs asynchronously and may not have finished by the
time the confirmation page renders).
Data model
SpreeLoyalty::Account— one per[user, store],points_balanceandlifetime_points_earned.SpreeLoyalty::Transaction— the ledger.kindisearned,redeemed, oradjusted;sourceis a polymorphic reference to the originatingSpree::Order(earn) orSpree::StoreCredit(redemption),nilfor a manual admin adjustment.
Releasing a new version
bundle exec gem bump -p -t
bundle exec gem release
For more options please see gem-release README
Contributing
If you'd like to contribute, please take a look at the instructions for installing dependencies and crafting a good pull request.