Answers · Billing

How do I see which accounts are actually paying me?

Query the Subscription table in the Rails console: active and trialing rows are the entitled accounts. One Shot ships no admin UI on purpose. For anything about money rather than access, read Stripe's dashboard instead, because the local row tracks entitlement and Stripe tracks what was actually charged.

The query

bin/kamal app exec --interactive --reuse 'bin/rails console'
Subscription.where(status: %w[active trialing]).count
Subscription.where(status: "active").joins(:account).pluck("accounts.name")
Subscription.group(:status).count

That last one is the useful daily number: it shows active, trialing, past due and canceled side by side, and a growing past due bucket is a webhook problem rather than a customer problem.

Why there is no admin dashboard

Building one means building authentication for it, authorization for it, and a second set of views that read customer data without the account scoping the rest of the app enforces. That is a meaningful amount of surface to secure for something you will look at weekly. The console has none of those problems, because it is already behind SSH.

If you do build one later, it gets the same rules as everything else: account_id on every query, a Pundit policy, and a spec proving a foreign id 404s.

The local row is not your revenue

Two different questions with two different sources of truth:

  • Who has access? The Subscription row. Read it from the console.
  • How much money came in? Stripe. Read it in the dashboard.

They can legitimately disagree. An account activated by hand after a failed webhook has access and no charge. A refunded charge may leave access intact if the revoke branch is not wired. Reconciling them occasionally is worth doing; conflating them is not.

A number worth having on a schedule

If you want the counts without opening a console, put them in a job and have it post somewhere. config/recurring.yml is where the schedule goes, and Solid Queue runs it in the same container, so there is nothing new to deploy.

Related questions

Can I charge a one-time price instead of a subscription?

Yes. Create a one-time price in Stripe, use payment mode rather than subscription mode in the checkout session, and change what Entitlement.for returns so a completed purchase entitles the account permanently. The gate on each controller does not change, because RequireEntitlement never knew what it was checking.

How do I let customers cancel their own subscription?

Send them to the Stripe billing portal rather than building a cancellation UI. One Shot ships the route already: a form that posts to the billing portal controller and redirects to Stripe. When they cancel there, customer.subscription.deleted arrives and the local row is updated, so access ends without you writing any of it.

How do I offer a free trial without taking a card upfront?

Set a trial period on the Stripe price or the checkout session and allow it without a payment method. The app needs no change: Subscription#entitled? returns true for both active and trialing, so a trialing account has full access, and customer.subscription.updated moves it when the trial ends.