Answers · Billing

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.

Why the app side is already done

def entitled? = active? || trialing?

That is the whole of it, in app/models/subscription.rb. The status enum already has trialing, so a trial is not a special case anywhere in the gate, and app/models/entitlement.rb asks the same question whether or not a trial is involved.

What Stripe needs

Two settings, both on Stripe's side:

  • A trial period on the price or passed into the checkout session.
  • Payment method collection set so a card is not required to start.

Without a card, Stripe cannot charge when the trial ends. It sends customer.subscription.updated with the status moving out of trialing, and the subscription is canceled rather than billed. Your gate closes, which is the behavior you want.

The event that matters most

customer.subscription.updated is what carries trial transitions, and it is easy to leave disabled because it sounds generic. If it is not enabled on the endpoint, a trial that ended looks like an active trial forever, and you give the product away indefinitely.

bin/kamal app exec --interactive --reuse 'bin/rails console'
Subscription.where(status: "trialing").where("updated_at < ?", 30.days.ago).count

A number above zero on a 14 day trial is that bug.

Card or no card

No card converts more people into trials and fewer trials into customers, because the end of the trial is a decision rather than a default. With a card it is the reverse. Neither is wrong, but pick deliberately and then measure it, rather than inheriting whichever one you configured first.

Test the whole arc

Stripe test mode lets you advance a subscription's clock, which is the only honest way to test the end of a trial. Without it you are asserting that a status change you never saw will do what you expect. Run it once, watch the webhook arrive, and confirm the gate closes.

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 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.