crxpaydocs

Publishing

How a draft paywall gets to a customer's popup. Runtime fetch, KV cache, and rollback.

A paywall is draft when you create it and published the moment you hit the publish button in the editor. The SDK only ever sees published paywalls.

What "publish" actually does

┌──────────────────────────────────────────────────────────────┐
│ POST /v1/dashboard/paywalls/:id/publish                      │
└─────────────────────────┬────────────────────────────────────┘


┌──────────────────────────────────────────────────────────────┐
│ status='published'   version = N+1   publishedAt = now()     │
│ KV.activePaywall(extId)  →  DELETE                           │
└──────────────────────────────────────────────────────────────┘

D1 stores the row. KV invalidates immediately so the next SDK fetch hits fresh data + writes it back.

What the SDK does

GET /v1/sdk/paywall/active
Authorization: Bearer crxpay_pub_...
X-CrxPay-Mode: live | test

Cache hit (KV): returns in < 50 ms. Cache miss: D1 single-index lookup (paywalls_ext_status_idx) → write back to KV with 5-minute TTL → return.

The response is wrapped:

{ "id": "pw_abc", "version": 12, "config": { ... } }

version is monotonic — increments on every publish. Useful for "show a 'paywall just updated' toast" to active customers.

Where to render it in your extension

Inside the popup, after the SDK is configured + the provider mounted:

import { useEffect, useState } from 'react';
import {
  CrxPayProvider, Paywall, PricingTable, EntitlementGate,
} from '@crxpay/react-paywall';
import '@crxpay/react-paywall/styles.css';

interface ActivePaywall { id: string; version: number; config: PaywallConfig }

function App() {
  const [paywall, setPaywall] = useState<PaywallConfig | null>(null);

  useEffect(() => {
    fetch('https://api.crxpay.io/v1/sdk/paywall/active', {
      headers: { Authorization: `Bearer ${API_KEY}` },
    }).then(async (res) => {
      if (res.ok) {
        const data = await res.json() as ActivePaywall;
        setPaywall(data.config);
      }
    });
  }, []);

  return (
    <CrxPayProvider apiKey={API_KEY}>
      <EntitlementGate
        entitlement="pro"
        fallback={paywall ? <Paywall config={paywall}/> : <PricingTable/>}
      >
        <YourPaidUi />
      </EntitlementGate>
    </CrxPayProvider>
  );
}

Falling back to <PricingTable> when /active returns 404 means your popup never breaks during development — before you've published your first paywall, customers see the auto-generated default.

Latency

PathTime
Click publish in editort = 0
D1 row updated · KV purgedt + ~80 ms
Customer opens popup, SDK fetchesup to 5s before the next open hits D1 (existing KV cache misses immediately on purge so first fetch after publish hits D1)
<Paywall> renders+ frame

Worst case "publish → existing user sees the new paywall on next popup open": ~1 second if their popup is closed at publish time.

Cache + invalidation

The KV key is active_paywall:{extensionId}. TTL is 5 minutes — short enough that even if a publish-time invalidation fails, customers see the update within 5 minutes of next fetch.

The dashboard invalidates on:

  • Publish — flips status, removes the cached entry
  • Unpublish — same purge; subsequent fetches return 404 until a new paywall is published or the previous version is re-published
  • Delete — same purge; if the deleted row was the active one, the endpoint serves the next-most-recently-published row

Multiple drafts → one publish

You can have any number of draft paywalls per extension. Only one is "active" at a time — the most recently published. Publishing a different draft demotes the previously-active one (it stays in the dashboard as a published row, just no longer the latest).

Rolling back

In the editor's index page (Paywalls → Editor), click an older published paywall → re-publish. That bumps its version + publishedAt, making it the new "active" row. The customer's popup picks it up on next fetch — no extension rebuild required.

(Inline version history is a v2 feature.)

Test mode

The SDK respects X-CrxPay-Mode: test. The paywall fetch + render flow is identical in both modes; only the offerings + checkout endpoints split.

What's not supported in v1

  • Geo-targeting / segmentation (Phase 9)
  • A/B testing variants (Phase 9)
  • Push to a subset of customers (Phase 9)
  • Schedule a publish for later (Phase 10)

Next

Was this page helpful?

Your feedback shapes what we document next.