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
| Path | Time |
|---|---|
| Click publish in editor | t = 0 |
| D1 row updated · KV purged | t + ~80 ms |
| Customer opens popup, SDK fetches | up 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
- → Browse the eight templates
- → Tour the visual editor
- → Or skip the paywall config entirely and use drop-in components
Was this page helpful?
Your feedback shapes what we document next.