Gating Content in Ghost: How Membership Tiers, Handlebars Helpers, and Stripe Actually Fit Together
Ghost gates content server-side with Handlebars helpers while Stripe owns the payments, synced by webhooks. Here's how the pieces fit, how to verify them, and where the limits are.
10 Aug 2026, 05:57 UTC

You want some posts free, some posts for paying subscribers, and you don't want to build an auth system to do it. Ghost's membership feature handles this out of the box — but the mechanics are worth understanding before you flip the switch, because the boundary between "Ghost does it" and "you're responsible for it" is easy to misjudge.
The short version: Ghost manages member state and renders gated content server-side using Handlebars helpers, while Stripe owns the money. Webhooks keep the two in sync. Your job is mostly configuration, plus a few security decisions that Ghost can't make for you.
Tiers live in Ghost, money lives in Stripe
In Ghost Admin, under Settings → Membership (or Tiers, depending on your version), you create tiers with monthly or annual pricing. Ghost connects to your Stripe account and generates the corresponding products and prices there. When a reader clicks "Subscribe," they're sent through a Stripe Checkout flow Ghost constructs for you.
The important architectural point: Ghost never touches card data. Stripe processes the payment, then fires webhook events back to Ghost — things like customer.subscription.created and customer.subscription.updated. Ghost's webhook endpoint translates those into membership state changes: active, canceled, tier upgraded, comped. This sync is near real time, which is why a subscriber can pay and immediately see gated content.
You can verify the plumbing any time: open your Stripe dashboard, find the webhook endpoint pointing at your Ghost site, and check the delivery log. Successful deliveries show 200 responses. Failed deliveries are the first place to look when someone says "I paid but I still can't see the post."
The {{#member}} helper does the actual gating
At the theme layer, gating is a conditional render. Ghost exposes member context to Handlebars templates, so you can write:
{{#post}}
{{#has visibility="paid"}}
{{#if @member.paid}}
{{content}}
{{else}}
<p>This post is for paying subscribers.</p>
<a href="#/portal/signup">Subscribe</a>
{{/if}}
{{/has}}
{{/post}}In practice, most themes use the built-in {{#has}} visibility check combined with post access settings in the editor, and Ghost's Portal handles the signup/login overlay. The {{#if @member}} family of helpers is what you reach for when you want finer control — for example, showing a different byline block to members, or hiding a "subscribe" banner from people who already subscribed.
Because rendering happens server-side, gated content is not present in the HTML sent to anonymous visitors. That's a meaningful security property: there is no client-side JavaScript to bypass. You can confirm it yourself by opening an incognito window, viewing a members-only post, and checking page source — the full body should be absent, replaced by your teaser or excerpt if you configured one.
A worked example: free teaser plus paid body
A common pattern is a public excerpt that ends with a paywall. In the Ghost editor, set the post's access to "Paid-members only" and write a public excerpt in the post settings. Ghost will render the excerpt to everyone and the full body only to members with an active paid subscription. No theme changes required — this is the default behavior in the official themes like Source.
To check it works end to end:
- Create a test member in Admin → Members with a comped (free) paid subscription, rather than spending real money in Stripe.
- Log in as that member through Portal on your site's frontend.
- Open the gated post — full content should render.
- Open the same post in a private/incognito window — you should see only the excerpt and the subscribe prompt.
- Cross-check the member's status in Ghost Admin against what Stripe shows, if you used a real checkout.
Trade-offs and things that bite
Caching delays. Ghost caches rendered content, so a membership state change isn't always reflected instantly in what a page shows. If a member reports stale access right after upgrading, a cache purge or waiting for cache expiry usually resolves it. Don't assume the webhook failed until you've checked Stripe's delivery log.
Key hygiene is on you. Stripe publishable keys are safe in frontend contexts by design; secret keys and webhook signing secrets are not. Ghost stores these server-side, but if you build any custom integration on top — a custom checkout flow, a sync script — never embed secret keys in theme files. Theme files are downloadable by anyone who knows where to look.
Promotions are basic. Ghost supports complimentary subscriptions and simple offers, but free trials, complex coupons, and proration logic are limited compared to what Stripe itself supports. If your business model leans on aggressive discounting or trial funnels, expect to build custom flows against the Stripe API and reconcile state back into Ghost, or accept the simpler built-in model.
Version assumptions. The behavior described here reflects Ghost 5.x self-hosted and Ghost(Pro). Admin navigation labels shift between minor versions, so if a menu name doesn't match, look for the nearest equivalent under Settings.
Where to start
Connect Stripe in a test mode first, create one paid tier, and gate a single post. Run the five-step verification above with a comped test member. If the incognito check shows the excerpt and the logged-in check shows the full post, your gating is sound — and everything after that is content strategy, not engineering.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.