Forever Trial (complimentary access) — Design
Date: 2026-05-28 Status: Approved
Problem
The admin wants to let friends use the full app indefinitely without paying, and
without faking Stripe data. Today the paywall (ApplicationController#require_subscription)
gates access purely on current_user.payment_processor.subscribed?, redirecting
everyone else to /subscriptions. There is no way to comp a user.
Goal
A one-click toggle in the admin users list (/admin/users?tab=list, actions
column) that grants/revokes a "Forever Trial" — complimentary access that bypasses
the paywall. Comped friends can still subscribe and pay later, at which point they
become normal paying subscribers.
Approach (chosen: complimentary-access flag)
Add a comp_access:boolean flag on User. The paywall passes when
comp_access? || subscribed?. No fake Pay/Stripe subscription is created, so there
is no naming conflict on upgrade, no Stripe-sync risk, and no bogus trial-banner
countdown.
Rejected alternatives:
- Fake "forever" Pay subscription (local
trialing, far-futuretrial_ends_at): on upgrade you get two"default"subscriptions (ambiguousPay#subscription), the fake sub isn't in Stripe (sync/cancel paths can error), and the trial banner counts down ~36,500 days. - Real Stripe 100%-off / infinite-trial subscription: needs live Stripe calls, network dependency, and puts friends into Stripe as customers — overkill.
Components
-
Migration —
add_column :users, :comp_access, :boolean, default: false, null: false. YieldsUser#comp_access?. The column name reflects what it is (complimentary access); the UI label is "Forever Trial". -
Paywall —
ApplicationController#require_subscription:return if current_user.comp_access? || current_user.payment_processor.subscribed?. Thecomp_access?short-circuit also avoids touchingpayment_processorfor comped users. -
Admin toggle — member route
POST /admin/users/:id/toggle_comp_access→Admin::UsersController#toggle_comp_access. Flips the boolean, writes anaudit(:"user.comp_access", target:, granted:)record, and renders a turbo-stream replacing the user row + a flash. Rejects admin targets (non-admin only), mirroring the existing impersonate/confirm actions. -
Button — in
app/views/admin/users/_user_row.html.erbactions column, beside impersonate: a gift-iconbutton_toto the toggle route, withturbo_confirmandtitlereflecting the current state ("Grant Forever Trial" / "Revoke Forever Trial"). Rendered only for non-admins. Indigo to distinguish from the other icons. -
Badge —
SubscriptionHelper#subscription_status_forgains a:forever_trialstate. Precedence: a real Pay subscription (on_trial?/subscribed?) wins; elsecomp_access?→ "Forever Trial" (indigo badge); else "Not Subscribed". A comped friend shows Forever Trial; once they pay they show Subscribed. -
Auto-clear on payment —
Billing::CompAccessRevoker, subscribed tostripe.subscription.createdviaPay::Webhooks.delegatorin an initializer (mirroringstripe_connect.rb/Affiliate::WebhookHandler). Resolves the Stripe customer →Pay::Customer→Userand setscomp_access = false, so paying establishes a single source of truth.
Testing
- Paywall (integration): with the paywall enabled, a
comp_accessuser is NOT redirected; a plain non-subscribed user IS. - Admin controller: toggle grants then revokes, writes an audit row, refreshes the row; admin target is rejected.
- Helper: badge shows "Forever Trial" for a comp user, "Subscribed" when also subscribed.
- Webhook handler:
Billing::CompAccessRevokerclearscomp_accesson asubscription.createdevent for the matching customer.
Edge cases / notes
- Admin targets: button hidden + server-side guard.
- Per the "auto-clear on payment" decision: if a comped friend pays and later cancels, the grant is already gone, so they fall back to the paywall. Acceptable and consistent with that choice.
- Analytics
subscribed_countis left unchanged — comp users are not paying revenue.