Case Study

Paywall Blueprint

The first publicly available worked example of monetizing a Sitecore Marketplace App — a foundation tranche, a real Stripe Checkout integration, and two adapter seams ready to swap.

Case Study7 min read

What it is

Sitecore's Cloud Portal Marketplace ships no built-in commerce primitives. Every team that wants to charge for a Marketplace App starts from zero on every decision — which provider, how to map Cloud Portal tenants to subscription objects, how to enforce seats inside an iframe sandbox, what to show editors when access is denied, how to handle reinstalls, how to wire webhooks back to entitlements. The result is an ecosystem that stays effectively free-only because the per-app cost of inventing a paywall in isolation is too high.

Paywall Blueprint is the first publicly available worked example of monetizing a Sitecore Marketplace App. It is a public OSS reference app on the SitecoreAI Full Screen extension point that demonstrates a freemium pattern — free piece + gated premium piece on one page — plus a complete denial-state UX library, all driven by a <PaywallGate> React wrapper and an entitlement store.

The reference app itself has no real feature behind the paywall. The paywall flow IS the feature. Fork the repo, replace the placeholder concerns through two clean adapter seams, ship a monetized Marketplace App without reinventing the gating pattern.

4
PRD product roadmap
(000 · 001 · 002 · 003)
2
PRDs shipped
(foundation + Stripe)
18
ADRs captured
across both PRDs
5
operator gates
green at PRD-000 ship

The roadmap, named up front

The product is deliberately architected as four PRDs so adopters can copy at any layer of completeness. Each PRD ends in a ship; each ship stands on its own.

PRD-000 · Foundation
shipped 2026-05-15

PaywallGate component, EntitlementStore adapter (Supabase v1), freemium UX shell, all four denial-state UX components (design-reference), env-flag toggle between enforced and demo modes, OSS launch surface — public GitHub repo + README + LICENSE + SECURITY + CONTRIBUTING. Tenant-only entitlement evaluation per ADR-0011. Placeholder PaymentProvider so the abstraction boundary exists before the real provider lands.

PRD-001 · Real Stripe
shipped 2026-05-17

Scaffold migration 4a → 4b (Next.js API routes added). StripeProvider — first concrete PaymentProvider implementation. Four new API routes (/api/checkout, /api/portal, /api/entitlement, /api/webhooks/stripe). /paywall-return page with postMessage + polling fallback. Webhook idempotency, signature verification, Stripe Customer orphan recovery. €0.99 lifetime one-time payment wired against a live tenant.

PRD-002 · Bento redesign
in flight

Bento-grid redesign of /full-page. Five free cards present real data from APIs already verified (welcome / sites count / plan / user profile / tenant info). Six premium cards behind a blur + Subscribe banner reveal in a stagger-in cascade after payment. Premium cards are pure marketing — Sitecore-flavored fake data, no API fetches, "fake it till you make it" by operator direction.

PRD-003 · Customer Portal
queued

Self-service customer portal via Stripe Customer Portal (one API call; Stripe hosts the entire UI for card / plan / cancel / invoices). Closes the loop on the freemium → paid → managed lifecycle. Per-user seat enforcement is queued behind this as a separate scope decision.

The two adapter seams

The whole product hinges on two abstraction boundaries that adopters can swap on the day they fork the repo. Each one has a placeholder implementation in PRD-000 and a real one in PRD-001 (PaymentProvider) or PRD-002 (EntitlementStore polish).

Architecture · 01

Two adapters, one fork-friendly seam each

ADR-0002 (EntitlementStore) · ADR-0003 (PaymentProvider)

Adopters who hate Stripe swap PaymentProvider for Polar.sh or Lemon Squeezy in a single file. Adopters who hate Supabase swap EntitlementStore for Upstash or Firestore in a single file. The gate component, the freemium shell, the denial-state UX, and the iframe-return logic stay untouched on both swaps.

  • PaymentProvider — the billing seam — Stripe direct in v1 (Stripe Billing + Customer + Checkout + webhooks). One TypeScript file. The interface returns Checkout URLs and emits entitlement-change events from webhooks. Swap to Polar.sh / Lemon Squeezy by writing a new file that satisfies the same shape.
  • EntitlementStore — the persistence seam — Supabase Postgres in v1, with row-level security set permissively (ADR-0009) so adopters don’t paint themselves into a multi-tenant corner. One TypeScript file. Swap to Upstash / Firestore / DynamoDB by writing a new file that satisfies the same shape.
  • The entitlement key is the tenantmarketplaceAppTenantId proved durable across user sessions, tenant re-installs, and seat changes during the Tranche 1 probe. ADR-0011 named it as the only PRD-000 evaluation input. host.user is a separate SDK query, queued for seat-aware evaluation in a later PRD.
  • The gate is one component<PaywallGate> wraps a subtree. It reads the entitlement, picks one of four denial states (no-subscription / loading / blocked / error), and renders the children when allowed. Adopters apply it once and forget it.
  • Env-flag toggleNEXT_PUBLIC_PAYWALL_ENFORCED flips the gate between enforced and demo modes. The blueprint ships in demo mode so a fresh clone runs without Stripe credentials.

The PRD-001 captures

Wiring Stripe Checkout into a Marketplace iframe end-to-end returned a specific stack of integration captures. None of them are in the Stripe docs in the form they ship as inside Cloud Portal. Each one became an ADR.

Capture 01

CSP frame-ancestors must allow-list the host.

The sitecore:setup-marketplace-full-stack scaffold ships a default frame-ancestors that omits app.sitecorecloud.io. Cloud Portal embeds from that host. Add it before first real-tenant smoke or the iframe never loads.

Capture 02

stripe listen needs https:// + --skip-verify against next dev --experimental-https.

The CLI defaults to http:// and silently fails when the dev server runs HTTPS. Pass https://localhost:3000/... explicitly and --skip-verify (mkcert is self-signed). Copy a fresh whsec_* from each listen session into .env.local and restart dev.

Capture 03

automatic_tax requires customer_update.address.

automatic_tax: { enabled: true } against an existing Customer without an address throws customer_tax_location_invalid. Fix: customer_update: { address: 'auto', name: 'auto' } on the Checkout Session params.

Capture 04

Idempotency keys cache params — version the key.

Stripe caches the params shape against the first idempotency-key submission for 24h. Same key + changed params = rejected. Build the key as ${id}:${PARAMS_VERSION} and bump the version constant when the params shape changes. Self-documenting changelog.

Capture 05

Subscription.current_period_end was removed from the SDK type — but still ships at runtime.

stripe@22.x migrated current_period_end to per-item but webhook payloads still ship it at the top level. Cast through unknown as Record<string, unknown> to read it without losing type discipline elsewhere.

Capture 06

Post-payment refresh needs a visibilitychange listener.

External-tab payment flows carry no payload back to the iframe; webhook + DB is the truth. Polling alone has a finite window. A visibilitychange listener catches returns after polling timeout — and returns from previous sessions — so the iframe always reconciles. Build pattern, not a workaround.

What the methodology shifted

PRD-000's Tranche 1 ran the same kind of two-day real-tenant probe that Redirect Manager made unavoidable. The Paywall Blueprint probe asked a different question — not "is this possible" but "is this key the right key" — and got a green verdict on marketplaceAppTenantId as the entitlement key, plus the side-finding that host.user is a separate SDK query rather than a property on tenant context.

That second finding was the move that pushed seat-aware evaluation cleanly into a future PRD (ADR-0011). Without the probe, PRD-000 would have shipped with a confused seat shape, or stretched its scope to include seats, or both. With it, Phase 0 ran as a deliberate minimum-viable foundation — a shippable reference that an adopter could install, read, and copy from on day one, while the differentiated SDK surfaces (Stripe billing, seat enforcement, customer portal) layered on top in subsequent PRDs against a verified key.

The methodology pattern from the Real-Tenant Probe applied:

  • Two-day cap — probe ended in a day on this product.
  • One assumption, named — tenant-only evaluation, with the marketplaceAppTenantId as the durable key.
  • Three legitimate outcomes — green for tenant-only PRD-000; yellow on seat enforcement (workaround = queue it); red would have killed the product before PRD-000 ever opened.
  • The capture is the artefact — the SDK identity table is now a reference memory that PRD-002 and PRD-003 will inherit.

Where this leaves the loop

Paywall Blueprint is the sixth Marketplace product shipped through the full agentic pipeline (after PageShot, QuickCopy, Component Atlas, Last-Edit Trail, and Redirect Manager). It is the first of them designed as a public reference rather than a focused product — "Blueprint" is in the name on purpose.

The four-PRD shape is the methodology contribution that scales beyond this product. Foundation → real provider → polish → portal is the same shape any monetized Marketplace App can copy. The provider and the entitlement store are seams, not assumptions. The probe before each PRD is the gate. The dogfood loop hardens the skills as each PRD ships. The pipeline runs the inside half; the operator runs the outside half against a real tenant.

If you're building a paid Sitecore Marketplace App, start by reading the public repository. Adopt the seams. Probe your own load-bearing assumption in two days. Ship the foundation tranche first; layer the provider second. The hard work is in the captures — and the captures are already done.

Related case studies