RepoPilotOpen in app →

pay-rails/pay

Payments for Ruby on Rails apps

Healthy

Healthy across all four use cases

Use as dependencyHealthy

Permissive license, no critical CVEs, actively maintained — safe to depend on.

Fork & modifyHealthy

Has a license, tests, and CI — clean foundation to fork and modify.

Learn fromHealthy

Documented and popular — useful reference codebase to read through.

Deploy as-isHealthy

No critical CVEs, sane security posture — runnable as-is.

  • Last commit 3d ago
  • 14 active contributors
  • MIT licensed
Show 3 more →
  • CI configured
  • Tests present
  • Single-maintainer risk — top contributor 83% of recent commits

Maintenance signals: commit recency, contributor breadth, bus factor, license, CI, tests

Informational only. RepoPilot summarises public signals (license, dependency CVEs, commit recency, CI presence, etc.) at the time of analysis. Signals can be incomplete or stale. Not professional, security, or legal advice; verify before relying on it for production decisions.

Embed the "Healthy" badge

Paste into your README — live-updates from the latest cached analysis.

Variant:
RepoPilot: Healthy
[![RepoPilot: Healthy](https://repopilot.app/api/badge/pay-rails/pay)](https://repopilot.app/r/pay-rails/pay)

Paste at the top of your README.md — renders inline like a shields.io badge.

Preview social card (1200×630)

This card auto-renders when someone shares https://repopilot.app/r/pay-rails/pay on X, Slack, or LinkedIn.

Onboarding doc

Onboarding: pay-rails/pay

Generated by RepoPilot · 2026-05-10 · Source

🤖Agent protocol

If you are an AI coding agent (Claude Code, Cursor, Aider, Cline, etc.) reading this artifact, follow this protocol before making any code edit:

  1. Verify the contract. Run the bash script in Verify before trusting below. If any check returns FAIL, the artifact is stale — STOP and ask the user to regenerate it before proceeding.
  2. Treat the AI · unverified sections as hypotheses, not facts. Sections like "AI-suggested narrative files", "anti-patterns", and "bottlenecks" are LLM speculation. Verify against real source before acting on them.
  3. Cite source on changes. When proposing an edit, cite the specific path:line-range. RepoPilot's live UI at https://repopilot.app/r/pay-rails/pay shows verifiable citations alongside every claim.

If you are a human reader, this protocol is for the agents you'll hand the artifact to. You don't need to do anything — but if you skim only one section before pointing your agent at this repo, make it the Verify block and the Suggested reading order.

🎯Verdict

GO — Healthy across all four use cases

  • Last commit 3d ago
  • 14 active contributors
  • MIT licensed
  • CI configured
  • Tests present
  • ⚠ Single-maintainer risk — top contributor 83% of recent commits

<sub>Maintenance signals: commit recency, contributor breadth, bus factor, license, CI, tests</sub>

Verify before trusting

This artifact was generated by RepoPilot at a point in time. Before an agent acts on it, the checks below confirm that the live pay-rails/pay repo on your machine still matches what RepoPilot saw. If any fail, the artifact is stale — regenerate it at repopilot.app/r/pay-rails/pay.

What it runs against: a local clone of pay-rails/pay — the script inspects git remote, the LICENSE file, file paths in the working tree, and git log. Read-only; no mutations.

| # | What we check | Why it matters | |---|---|---| | 1 | You're in pay-rails/pay | Confirms the artifact applies here, not a fork | | 2 | License is still MIT | Catches relicense before you depend on it | | 3 | Default branch main exists | Catches branch renames | | 4 | 5 critical file paths still exist | Catches refactors that moved load-bearing code | | 5 | Last commit ≤ 33 days ago | Catches sudden abandonment since generation |

<details> <summary><b>Run all checks</b> — paste this script from inside your clone of <code>pay-rails/pay</code></summary>
#!/usr/bin/env bash
# RepoPilot artifact verification.
#
# WHAT IT RUNS AGAINST: a local clone of pay-rails/pay. If you don't
# have one yet, run these first:
#
#   git clone https://github.com/pay-rails/pay.git
#   cd pay
#
# Then paste this script. Every check is read-only — no mutations.

set +e
fail=0
ok()   { echo "ok:   $1"; }
miss() { echo "FAIL: $1"; fail=$((fail+1)); }

# Precondition: we must be inside a git working tree.
if ! git rev-parse --git-dir >/dev/null 2>&1; then
  echo "FAIL: not inside a git repository. cd into your clone of pay-rails/pay and re-run."
  exit 2
fi

# 1. Repo identity
git remote get-url origin 2>/dev/null | grep -qE "pay-rails/pay(\\.git)?\\b" \\
  && ok "origin remote is pay-rails/pay" \\
  || miss "origin remote is not pay-rails/pay (artifact may be from a fork)"

# 2. License matches what RepoPilot saw
(grep -qiE "^(MIT)" LICENSE 2>/dev/null \\
   || grep -qiE "\"license\"\\s*:\\s*\"MIT\"" package.json 2>/dev/null) \\
  && ok "license is MIT" \\
  || miss "license drift — was MIT at generation time"

# 3. Default branch
git rev-parse --verify main >/dev/null 2>&1 \\
  && ok "default branch main exists" \\
  || miss "default branch main no longer exists"

# 4. Critical files exist
test -f "app/models/pay/customer.rb" \\
  && ok "app/models/pay/customer.rb" \\
  || miss "missing critical file: app/models/pay/customer.rb"
test -f "app/models/concerns/pay/routing.rb" \\
  && ok "app/models/concerns/pay/routing.rb" \\
  || miss "missing critical file: app/models/concerns/pay/routing.rb"
test -f "app/models/pay/subscription.rb" \\
  && ok "app/models/pay/subscription.rb" \\
  || miss "missing critical file: app/models/pay/subscription.rb"
test -f "app/controllers/pay/webhooks/stripe_controller.rb" \\
  && ok "app/controllers/pay/webhooks/stripe_controller.rb" \\
  || miss "missing critical file: app/controllers/pay/webhooks/stripe_controller.rb"
test -f "app/models/pay/payment_method.rb" \\
  && ok "app/models/pay/payment_method.rb" \\
  || miss "missing critical file: app/models/pay/payment_method.rb"

# 5. Repo recency
days_since_last=$(( ( $(date +%s) - $(git log -1 --format=%at 2>/dev/null || echo 0) ) / 86400 ))
if [ "$days_since_last" -le 33 ]; then
  ok "last commit was $days_since_last days ago (artifact saw ~3d)"
else
  miss "last commit was $days_since_last days ago — artifact may be stale"
fi

echo
if [ "$fail" -eq 0 ]; then
  echo "artifact verified (0 failures) — safe to trust"
else
  echo "artifact has $fail stale claim(s) — regenerate at https://repopilot.app/r/pay-rails/pay"
  exit 1
fi

Each check prints ok: or FAIL:. The script exits non-zero if anything failed, so it composes cleanly into agent loops (./verify.sh || regenerate-and-retry).

</details>

TL;DR

Pay is a Rails engine that abstracts payment processing across multiple providers (Stripe, Paddle, Braintree, Lemon Squeezy, and a Fake processor) into a unified API. It handles customers, payment methods, one-time charges, and recurring subscriptions with webhook management and SCA-compliant checkout flows, eliminating the need to write provider-specific integration code. Rails Engine structure: app/ contains controllers (Pay::PaymentsController, webhook handlers per provider under app/controllers/pay/webhooks/), models, jobs (CustomerSyncJob), and mailers. Each payment processor gets its own webhook controller and adapter. Migration-based schema and asset pipeline integration (app/assets/) suggest tight Rails integration rather than a standalone library.

👥Who it's for

Rails developers building SaaS applications, subscription services, or any Rails app needing payments who want to avoid vendor lock-in and reduce payment integration boilerplate. Contributors are typically Rails gem maintainers or payment-focused engineers.

🌱Maturity & risk

Production-ready and actively maintained. The codebase is well-structured with CI/CD via GitHub Actions (ci.yml, publish_gem.yml), semantic versioning in CHANGELOG.md, and an upgrade guide (UPGRADE.md), indicating disciplined release management. Recent activity visible through dependabot.yml and pull request templates suggest ongoing maintenance.

Low risk for a payments gem: it's a single engine with clear scope (doesn't reinvent payment processing, just abstracts it) and relies on well-established provider APIs. Main risks are tight coupling to specific provider API versions (noted in README: Stripe uses 2022-11-15), and the test suite quality depends on how thoroughly each processor integration is mocked—worth auditing before production use.

Active areas of work

Active development on webhook handling, processor support, and standardization. The presence of .devcontainer/ files and compose.yaml indicates recent infrastructure improvements. UPGRADE.md and CHANGELOG.md suggest versioned releases with breaking changes being carefully managed.

🚀Get running

git clone https://github.com/pay-rails/pay.git && cd pay && bundle install && bundle exec rails db:create db:migrate && bundle exec rails test

Daily commands: bundle exec rails server (runs the dummy app in spec/dummy if present). For tests: bundle exec rails test or use the GitHub Actions CI (see .github/workflows/ci.yml).

🗺️Map of the codebase

  • app/models/pay/customer.rb — Core Customer model that serves as the primary abstraction for payment processor customers across Stripe, Paddle, Braintree, and Lemon Squeezy
  • app/models/concerns/pay/routing.rb — Routing concern that dynamically dispatches to processor-specific implementations based on processor type—essential for understanding the polymorphic pattern
  • app/models/pay/subscription.rb — Core Subscription model managing recurring billing across all supported processors with unified interface
  • app/controllers/pay/webhooks/stripe_controller.rb — Primary webhook handler demonstrating the event-driven architecture and processor integration pattern used throughout
  • app/models/pay/payment_method.rb — Payment method abstraction supporting multiple processors; foundational for payment processing flows
  • app/models/pay/charge.rb — Charge model representing individual transactions—core entity for payment tracking and reconciliation

🛠️How to make changes

Add support for a new payment processor

  1. Create processor directory with core models (app/models/pay/newprocessor/customer.rb)
  2. Implement subscription, payment_method, and charge models following the pattern from app/models/pay/stripe/ (app/models/pay/newprocessor/subscription.rb)
  3. Create webhook controller to handle processor events (app/controllers/pay/webhooks/newprocessor_controller.rb)
  4. Add processor API client integration and method implementations matching the routing interface (app/models/pay/newprocessor/customer.rb)
  5. Register merchant configuration in documentation and add to processor list (README.md)

Add a new webhook event handler

  1. Identify the event type in processor webhook controller (app/controllers/pay/webhooks/stripe_controller.rb)
  2. Create private method to handle event and update corresponding model (app/controllers/pay/webhooks/stripe_controller.rb)
  3. Update the model (Subscription, Customer, Charge) to reflect the event state (app/models/pay/subscription.rb)
  4. Trigger any required mailer notifications or background jobs (app/mailers/pay/user_mailer.rb)

Add a new customer capability (e.g., refund support)

  1. Add method to core Customer model with processor delegation (app/models/pay/customer.rb)
  2. Implement processor-specific refund logic in each adapter (app/models/pay/stripe/customer.rb)
  3. Create tests for new functionality across all processors (test/models/pay/customer_test.rb)
  4. Document in README and UPGRADE guide if breaking changes (README.md)

Add a new email notification

  1. Define mailer method in user_mailer.rb (app/mailers/pay/user_mailer.rb)
  2. Create HTML and text email templates (app/views/pay/user_mailer/notification_name.html.erb)
  3. Trigger mailer from webhook controller when event occurs (app/controllers/pay/webhooks/stripe_controller.rb)

🔧Why these technologies

  • Ruby on Rails 6.0+ engine — Provides conventions for models, controllers, migrations, and seamless integration into Rails apps; mountable engine architecture allows drop-in payment functionality
  • Stripe, Paddle, Braintree, Lemon Squeezy SDKs — Each processor requires native SDK for secure API communication, webhook verification, and feature completeness
  • Polymorphic models with routing concern — Allows single unified API (Customer, Subscription, Charge) that delegates to processor-specific implementations without code duplication
  • Background jobs (ActiveJob) — Enables async customer sync from processors without blocking request/response cycle
  • Webhook handlers — Receives real-time payment events from processors for state synchronization and triggering downstream actions

⚖️Trade-offs already made

  • Polymorphic routing through concerns vs. separate gem per processor

    • Why: Single gem provides unified API across processors, reducing user cognitive load and code duplication
    • Consequence: Adds complexity to routing layer; new processor requires multiple model implementations; some features may not be equally supported across all processors
  • Unified Charge/Subscription/Customer models with processor-specific subclasses

    • Why: Enables queries and business logic across all payment sources; single source of truth for user records
    • Consequence: Schema must accommodate lowest common denominator; processor-specific fields require flexible columns or serialization
  • Webhook-driven state synchronization vs. polling

    • Why: Near-real-time updates with minimal API calls; aligns with payment processor event standards
    • Consequence: Requires secure webhook signature verification; must handle duplicate/out-of-order events; webhook failures can leave app out of sync
  • Mountable Rails engine vs. standalone gem

    • Why: Integrates directly into host app's database and routing; automatic view/mailer/controller inclusion
    • Consequence: Tight coupling to Rails; requires Rails 6.0+; migrations are host app's responsibility

🚫Non-goals (don't propose these)

  • Does not handle customer authentication or identity verification
  • Does not provide accounting/bookkeeping features (tax, reconciliation)
  • Does not handle 3D Secure/SCA UI rendering directly (delegates to processor forms)
  • Does not support real-time reporting dashboards (metrics collection only)
  • Does not provide mobile native SDKs (web/server-side only)

🪤Traps & gotchas

Each processor requires API keys set as environment variables (e.g., STRIPE_PRIVATE_KEY, PADDLE_API_KEY)—check docs/2_configuration.md. The Fake Processor is for trials/free subscriptions only, not production payments. Webhook endpoints must be publicly routable and match provider settings. Database migrations are stored alongside the gem, not in the host app—migrations run automatically via Rails loading. Test database choice impacts CI—see .devcontainer notes about DATABASE_URL for PostgreSQL/MySQL/SQLite testing.

🏗️Architecture

💡Concepts to learn

  • Strong Customer Authentication (SCA) / PSD2 Compliance — Pay abstracts SCA handling across processors (Stripe, Paddle, Braintree with different SCA flows); understanding 3D Secure, redirect-based auth, and payment intents is critical for subscription/charge features
  • Adapter Pattern (Payment Processors) — Each processor (Stripe, Paddle, Braintree, Lemon Squeezy) implements a consistent interface in Pay—understanding this pattern is essential to add new processors or modify existing adapters
  • Webhook Idempotency & Event Deduplication — Payment processors may retry webhooks; Pay's webhook controllers must safely handle duplicate events (e.g., customer.subscription.updated fired twice)—critical for data consistency
  • Rails Concerns (Mixins) — Pay uses Concerns in app/models/concerns/ to share processor-specific logic across models; understanding Rails Concerns is required to extend or debug Pay's model behavior
  • Background Jobs & Job Queues (ActiveJob) — Pay uses app/jobs/pay/customer_sync_job.rb for async operations; understanding Rails job queues is essential for production deployments and handling rate limits from payment APIs
  • Zero-Decimal vs Decimal Currency Handling — Different currencies (JPY, GBP) are stored differently in payment APIs; Pay abstracts this in app/helpers/pay/currency_helper.rb—incorrect handling causes off-by-factor-of-100 charges
  • Rails Engine Architecture — Pay is distributed as a Rails Engine (not a gem library); understanding Rails Engines, automatic loading, migration inclusion, and asset pipeline integration is prerequisite for understanding the codebase structure
  • stripe/stripe-ruby — Official Stripe SDK that Pay wraps; understanding this API is prerequisite for hacking on Pay's Stripe adapter
  • rails/rails — Pay is a Rails Engine; deep Rails knowledge required for modifications to ActiveRecord models, controllers, and jobs
  • jumpstart-rails/jumpstart — Official Rails starter kit with Pay pre-integrated; demonstrates real-world usage patterns and serves as the canonical example app
  • heartcombo/devise — Companion gem for user authentication that Pay pairs with; many Pay users implement user accounts via Devise first
  • bullet-train-co/bullet_train — Higher-level Rails framework that bundles Pay for SaaS features; shows how Pay integrates into larger Rails ecosystems

🪄PR ideas

To work on one of these in Claude Code or Cursor, paste: Implement the "<title>" PR idea from CLAUDE.md, working through the checklist as the task list.

Add webhook integration tests for all payment processors

The repo has webhook controllers for 5+ payment processors (Stripe, Paddle Billing, Paddle Classic, Braintree, Lemon Squeezy) in app/controllers/pay/webhooks/, but there's no evidence of comprehensive webhook payload tests. This is critical for payment systems where webhook handling is error-prone and processor-specific. New contributors can add fixture-based tests validating each processor's webhook signatures, payload parsing, and idempotency.

  • [ ] Create spec/controllers/pay/webhooks/stripe_controller_spec.rb with webhook payload fixtures for common events (charge.completed, customer.subscription.updated, etc.)
  • [ ] Repeat for Paddle Billing, Paddle Classic, Braintree, and Lemon Squeezy webhook controllers
  • [ ] Add test cases validating webhook signature verification for each processor
  • [ ] Add tests for webhook idempotency (duplicate webhook handling)
  • [ ] Reference app/controllers/pay/webhooks/* to ensure all webhook actions are covered

Add missing unit tests for Payment Method syncing across processors

The file structure shows Pay::*::PaymentMethod classes for each processor (Braintree, Lemon Squeezy, and likely Stripe/Paddle variants), but there's no indication of comprehensive tests for payment method lifecycle (create, update, delete, sync). This is critical for ensuring payment method state stays consistent across the gem and external processors.

  • [ ] Audit app/models/pay/*/payment_method.rb files to identify all public methods
  • [ ] Create spec/models/pay/payment_methods_spec.rb testing basic CRUD operations across processors
  • [ ] Add tests for payment method validation, processor-specific attributes, and sync scenarios
  • [ ] Add tests for edge cases like deleted payment methods, expired tokens, and processor API errors
  • [ ] Ensure Pay::FakeProcessor::PaymentMethod has matching test coverage

Implement CI workflow for multi-gem compatibility testing (Appraisals)

The repo has an Appraisals file (used for testing against multiple versions of Rails/dependencies), but there's no GitHub Actions workflow explicitly running appraisal tests. This is critical for ensuring Pay works across the stated 'Rails 6.0 and higher' support range. Currently, CI may only test against one Rails version.

  • [ ] Review .github/workflows/ci.yml to check if appraisal tests are currently running
  • [ ] If missing, create a new workflow or extend ci.yml with a matrix job for Rails 6.0, 7.0, 8.0
  • [ ] Run 'bundle exec appraisal install' and 'bundle exec appraisal rspec' in the workflow
  • [ ] Ensure the workflow tests against multiple payment processor gems (stripe, paddle-sdk, braintree, etc.)
  • [ ] Document expected Rails versions in README if not already present

🌿Good first issues

  • Add missing webhook event test coverage for Lemon Squeezy processor—the controller exists at app/controllers/pay/webhooks/lemon_squeezy_controller.rb but test file is likely sparse; write integration tests mirroring stripe_controller tests
  • Expand Pay::CurrencyHelper in app/helpers/pay/currency_helper.rb to handle more edge cases (e.g., zero-decimal currencies for Stripe); add tests in spec/
  • Document processor-specific limitations in docs/ (e.g., Braintree/Lemon Squeezy PayPal support vs Stripe Stripe-only)—create docs/processor_comparison.md with a feature matrix

Top contributors

Click to expand

📝Recent commits

Click to expand
  • 5013eaa — Add background job syncing instructions (#1226) (loqimean)
  • 33f75f9 — Update retry_failed_payments api access to payment intent (#1225) (mguidetti)
  • e07f91e — Merge branch 'main' of github.com:pay-rails/pay (excid3)
  • e82cc7b — Update test deps (excid3)
  • b97a814 — 'hosted_page' was renamed for ui_mode (excid3)
  • 774dc5a — Require MFA to publish (excid3)
  • c68f122 — Version bump (excid3)
  • ee51047 — Merge branch 'BrandonCzaja-issue_1219/add_additional_listeners' (excid3)
  • e9efbc7 — Lint (excid3)
  • 35726cd — Merge branch 'issue_1219/add_additional_listeners' of github.com:BrandonCzaja/BrandonCzaja-pay into BrandonCzaja-issue_1 (excid3)

🔒Security observations

  • High · Multiple Payment Processor Integrations - Credential Exposure Risk — app/controllers/pay/webhooks/, app/models/pay/*/. The codebase integrates with multiple payment processors (Stripe, Braintree, Paddle, Lemon Squeezy) via separate controller and model files. Without visibility into configuration files, there's risk of API keys/credentials being exposed in environment variables, config files, or hardcoded in the application. Fix: Ensure all payment processor credentials are stored in environment variables using Rails credentials system (Rails.application.credentials). Never commit .env files or credential files to version control. Implement credential rotation policies and use separate API keys for different environments (dev/staging/prod).
  • High · Webhook Handler Security — app/controllers/pay/webhooks/. Multiple webhook controllers exist for different payment processors (Stripe, Braintree, Paddle, Lemon Squeezy). Webhook handlers are common attack vectors if not properly validated. Without seeing the implementation, there's risk of: (1) Missing signature verification, (2) Replay attacks, (3) Privilege escalation via webhook manipulation. Fix: Implement strict webhook signature verification for all processors. Verify webhook authenticity using processor-provided signatures (e.g., Stripe's X-Stripe-Signature). Implement idempotency checks to prevent replay attacks. Log all webhook events and verify request origins. Use HTTPS only for webhook endpoints.
  • High · Missing CSRF Protection Configuration — app/controllers/pay/payments_controller.rb, app/controllers/pay/application_controller.rb. Payment-related controllers (pay/payments_controller.rb, webhook controllers) handle sensitive operations. If CSRF tokens are not properly configured, attackers could forge requests to charge customers or modify payment methods. Fix: Ensure protect_from_forgery is configured in ApplicationController. Exempt webhook endpoints from CSRF protection but implement alternative signature-based verification. Validate all state-changing payment operations with CSRF tokens.
  • Medium · Potential SQL Injection in Dynamic Routing — app/models/concerns/pay/routing.rb. The routing concern in app/models/concerns/pay/routing.rb suggests dynamic polymorphic associations between customers and payment records across different processor implementations. Dynamic query building without proper parameterization could lead to SQL injection. Fix: Use Rails ORM methods (ActiveRecord) exclusively for database queries. Avoid string interpolation in where clauses. Use parameterized queries. Implement query logging and monitoring for suspicious patterns.
  • Medium · Information Disclosure via Error Messages — app/controllers/pay/, app/models/pay/*/. Payment processing operations often throw exceptions with sensitive details (processor error messages, transaction IDs, customer data). These could be exposed to users via error pages or logs. Fix: Implement custom error handling that sanitizes error messages shown to users. Log full error details server-side only. Return generic error messages to clients. Never expose processor-specific error details or transaction identifiers to end users.
  • Medium · Insufficient Input Validation on Payment Parameters — app/controllers/pay/payments_controller.rb. Payment controllers accept user input (amounts, currencies, customer data) without visible validation. Invalid or malicious input could cause processing errors, integer overflows in financial calculations, or bypass business logic. Fix: Implement strict input validation for all payment parameters: validate amount ranges, currency codes against ISO 4217, customer identifiers against whitelist. Use strong parameter filtering. Implement rate limiting on payment endpoints to prevent brute force attacks.
  • Medium · Missing or Inadequate Logging of Payment Operations — app/models/pay/, app/controllers/pay/. Payment operations involve sensitive financial data. Insufficient logging could hinder security investigations, audits, and fraud detection. Excessive logging could expose sensitive data. Fix: Implement comprehensive audit logging for all payment operations (create, update, refund). Log processor transaction IDs and operation status, but never log full credit card numbers, expiry dates, or CVV. Implement tamper-proof logging. Ensure logs are securely stored with access controls.
  • Medium · Weak Dependency Management Visibility — Gemfile,. The dependency file content was not provided. The gem manages multiple payment processor integrations which themselves have dependencies that could contain known vulnerabilities. Fix: undefined

LLM-derived; treat as a starting point, not a security audit.


Generated by RepoPilot. Verdict based on maintenance signals — see the live page for receipts. Re-run on a new commit to refresh.

Healthy signals · pay-rails/pay — RepoPilot