SimpleWallet
A lightweight, drop-in wallet engine for Ruby on Rails apps. simple_wallet
adds a balance ledger to any of your ActiveRecord models — users, accounts,
organisations — letting you credit, debit, and query balances with minimal
setup.
Features
- Attach a wallet to any ActiveRecord model with a few lines of code
- Credit and debit operations backed by a double-entry-style transaction log
- Query current balance and full transaction history per wallet owner
- Database-backed — no external services required
- Rails engine: migrations and models are mounted directly into your app
Requirements
- Ruby >= 3.0
- Rails >= 7.0
- PostgreSQL
Installation
Add the gem to your Gemfile:
gem "simple_wallet"Install it:
bundle installCopy and run the migrations:
bin/rails simple_wallet_engine:install:migrations
bin/rails db:migrate db:test:prepareUsage
Attaching a wallet to a model
bin/rails g migration add_account_to_users simple_wallet_account:references:uniqTweak the migration to allow null values (if you have existing records):
class AddAccountToUsers < ActiveRecord::Migration[8.0]
def change
add_reference :users, :simple_wallet_account, index: {unique: true}, null: true, foreign_key: true
end
endRun migrations:
bin/rails db:migrate db:test:prepareAnd finally add Account reference to your model:
class User < ApplicationRecord
belongs_to :account,
class_name: "::SimpleWallet::Account",
optional: true,
foreign_key: :simple_wallet_account_id
before_create :create_simple_wallet_account
private
def create_simple_wallet_account
create_account!
end
endQuerying the balance
user = User.create(first_name: "Marcin", last_name: "Urbanski")
user.account
=>
#<SimpleWallet::Account:0x0000710ebe38cac8
id: 1,
balance: 0,
income: 0,
outcome: 0,
created_at: "2026-04-11 09:05:41.534180000 -0700",
updated_at: "2026-04-11 09:05:41.534180000 -0700">Crediting
SimpleWallet::AccountCreditingService.new(account: user.account, amount: 1_000, source: User.admins.first, note: "Bonus!").credit
user.reload.account
=>
#<SimpleWallet::Account:0x0000710ebfc29108
id: 1,
balance: 1000,
income: 1000,
outcome: 0,
created_at: "2026-04-11 09:11:57.198448000 -0700",
updated_at: "2026-04-11 09:11:57.198448000 -0700">Debiting
SimpleWallet::AccountDebitingService.new(account: user.account, amount: 200, source: User.admins.first, note: "AI model invoice").debit
user.reload.account
=>
#<SimpleWallet::Account:0x0000710ebd85d350
id: 1,
balance: 800,
income: 1000,
outcome: -200,
created_at: "2026-04-11 09:11:57.198448000 -0700",
updated_at: "2026-04-11 09:11:57.198448000 -0700">Insufficient funds
service = SimpleWallet::AccountDebitingService.new(account: user.account, amount: 1_000_000, source: User.admins.first, note: "AI model invoice")
service.debit
=> false
service.errors.messages
=> {:amount=>["exceeds available balance"]}
# Debit up to account balance:
service = SimpleWallet::AccountDebitingService.new(account: user.account, amount: 1_000_000, source: User.admins.first, note: "AI model invoice", up_to_account_balance: true)
service.debit
=> true
user.reload.account
=>
#<SimpleWallet::Account:0x0000710ebd8bc080
id: 5,
balance: 0,
income: 1000,
outcome: -1000,
created_at: "2026-04-11 09:11:57.198448000 -0700",
updated_at: "2026-04-11 09:11:57.198448000 -0700">
user.account.transactions
=>
[
# ...
#<SimpleWallet::Transaction::Debit:0x0000710ebd8ba3c0
id: 2,
type: "SimpleWallet::Transaction::Debit",
account_id: 1,
source_type: "User", # Admin
source_id: 11,
pre_account_balance: 800,
amount: -800, # Debited as much as we could.
note: "AI model invoice",
created_at: "2026-04-11 09:27:38.958150000 -0700",
updated_at: "2026-04-11 09:27:38.958150000 -0700">]Transfering between accounts
employer = User.create(first_name: "Rich", last_name: "Employer")
employee = User.create(first_name: "Marcin", last_name: "Urbanski")
SimpleWallet::AccountCreditingService.new(account: employer.account, amount: 1_000_000, note: "From venture capitalist").credit
SimpleWallet::TransferService.new(from: employer.reload.account, to: employee.reload.account, amount: 15, note: "We really appreciate your efforts!").transfer
employer.account.transactions.last
=>
#<SimpleWallet::Transaction::Debit:0x0000710ebf569d90
id: 6,
type: "SimpleWallet::Transaction::Debit",
account_id: 6,
source_type: nil,
source_id: nil,
pre_account_balance: 1000000,
amount: -15,
note: "We really appreciate your efforts!",
created_at: "2026-04-11 09:40:27.018567000 -0700",
updated_at: "2026-04-11 09:40:27.018567000 -0700">
employee.account.transactions.last
=>
#<SimpleWallet::Transaction::Credit:0x0000710ebf567f90
id: 7,
type: "SimpleWallet::Transaction::Credit",
account_id: 7,
source_type: nil,
source_id: nil,
pre_account_balance: 0,
amount: 15,
note: "We really appreciate your efforts!",
created_at: "2026-04-11 09:40:27.024512000 -0700",
updated_at: "2026-04-11 09:40:27.024512000 -0700">Transaction history
user.account.transactions
=>
[#<SimpleWallet::Transaction::Credit:0x0000710ebd85a510
id: 1,
type: "SimpleWallet::Transaction::Credit",
account_id: 1,
source_type: "User", # Admin
source_id: 11,
pre_account_balance: 0,
amount: 1000,
note: "Bonus!",
created_at: "2026-04-11 09:12:19.260240000 -0700",
updated_at: "2026-04-11 09:12:19.260240000 -0700">,
#<SimpleWallet::Transaction::Debit:0x0000710ebd85a3d0
id: 2,
type: "SimpleWallet::Transaction::Debit",
account_id: 1,
source_type: "User", # Admin
source_id: 11,
pre_account_balance: 1000,
amount: -200,
note: "AI model invoice",
created_at: "2026-04-11 09:15:16.248670000 -0700",
updated_at: "2026-04-11 09:15:16.248670000 -0700">]Contributing
Bug reports and pull requests are welcome on GitHub.
- Fork the repository
- Create a feature branch (
git checkout -b my-feature) - Commit your changes (
git commit -am 'Add my feature') - Push the branch (
git push origin my-feature) - Open a Pull Request
Please run rubocop before submitting — the project ships with a
.rubocop.yml.
License
The gem is available as open source under the terms of the MIT License.