Paywall config schema
Every field on a PaywallConfig. The visual editor / templates / SDK fetch all speak this shape.
The paywall config is a JSON object. Templates produce one. The visual editor edits one. The renderer consumes one. The SDK fetches one. Every field is documented here.
The TypeScript source of truth lives in
packages/types/src/paywall.ts.
Import the types directly:
import type {
PaywallConfig, PaywallBlock, PaywallTheme, PaywallPolicy,
PaywallSurface, PaywallTemplateId,
} from '@crxpay/types';
Top level
interface PaywallConfig {
schemaVersion: 1;
name: string;
templateId?: PaywallTemplateId;
surface: PaywallSurface;
theme: PaywallTheme;
blocks: PaywallBlock[];
policy: PaywallPolicy;
behaviour?: { modalTitle?, modalSubtitle?, successUrl?, cancelUrl? };
}
| Field | Notes |
|---|---|
schemaVersion | Always 1 for now. The renderer rejects configs whose schemaVersion it doesn't know. |
name | Display name in the dashboard ("Holiday Sale 2026"). |
templateId | Originating built-in template, if any (e.g. pixel-shop). Null when hand-rolled. |
surface | 'extension-popup' / 'extension-overlay' / 'web' — affects max-width and padding. |
theme | Visual identity. See Theme. |
blocks | Ordered array of block definitions. See Blocks. |
policy | CWS compliance + refund / terms / privacy. See Policy. |
behaviour | Optional checkout / modal overrides. |
Theme
PaywallTheme is a superset of BrandTheme (used by the standalone
components). Extra fields apply only when blocks opt into them.
interface PaywallTheme {
// Core colors (BrandTheme tokens)
accent: string; accentForeground: string;
text: string; textMuted: string; textFaint: string;
surface: string; surfaceMuted: string;
border: string; borderActive?: string;
success?: string; danger?: string;
// Optional richer surfaces (game themes, etc)
pageBackground?: string; // CSS background string (gradient or solid)
pageBackgroundImage?: string; // public URL of an image
decoration?: 'none' | 'scanlines' | 'grain' | 'starfield' | 'dot-grid';
// Typography
fontBody?: string;
fontHeading?: string;
fontDisplay?: string; // pixel/arcade style; only when block.useDisplayFont
// Geometry
radius?: string; // default border-radius
backdrop?: string; // modal scrim color (with alpha)
}
Set pageBackground to a gradient and decoration: 'scanlines' for the
Pixel Shop look. Set just accent for the 90% case.
Blocks
PaywallBlock is a tagged union. Every block has { id, type, hidden? }
plus type-specific fields.
Hero
interface HeroBlock {
id: string; type: 'hero'; hidden?: boolean;
eyebrow?: string;
headline: string;
subheadline?: string;
badge?: { label: string; tone?: 'accent' | 'success' | 'danger' };
imageUrl?: string; // small icon/sprite at the top
useDisplayFont?: boolean; // applies theme.fontDisplay to the headline
}
Plans
interface PlansBlock {
id: string; type: 'plans'; hidden?: boolean;
layout: 'cards' | 'tier-ladder' | 'compact-list';
offeringIdentifier?: string; // null = use the org's default offering
ctaLabel?: string; // overrides "Choose plan"
}
The renderer fetches the offering at runtime via the SDK. cards reuses
the standalone <PricingTable>; tier-ladder and compact-list are
alternate layouts shipped in v1.
Features
interface FeaturesBlock {
id: string; type: 'features'; hidden?: boolean;
title?: string;
columns?: 1 | 2 | 3;
items: Array<{
icon?: string; // emoji, asset URL, or icon name
title: string;
description?: string;
}>;
}
icon strings starting with http(s):// or / render as <img>; everything
else renders as text (emoji, char, lucide name).
Testimonials
interface TestimonialsBlock {
id: string; type: 'testimonials'; hidden?: boolean;
title?: string;
layout: 'quote' | 'grid' | 'carousel';
quotes: Array<{
body: string;
author: string;
role?: string;
avatarUrl?: string; // falls back to first-letter avatar
}>;
}
FAQ
interface FaqBlock {
id: string; type: 'faq'; hidden?: boolean;
title?: string;
items: Array<{ q: string; a: string }>;
}
First item starts open; the rest are collapsed.
Comparison
interface ComparisonBlock {
id: string; type: 'comparison'; hidden?: boolean;
title?: string;
columns: string[]; // header labels (e.g. ['Free', 'Pro'])
rows: Array<{
feature: string;
values: Array<boolean | string>; // booleans render ✓ / —, strings pass through
}>;
}
Trust
interface TrustBlock {
id: string; type: 'trust'; hidden?: boolean;
title?: string;
badges: Array<{
kind: 'ssl' | 'pci' | 'refund' | 'no-data' | 'cancel-anytime' | 'custom';
label: string;
}>;
}
kind selects a pre-baked SVG icon. custom falls back to a check.
CTA
interface CtaBlock {
id: string; type: 'cta'; hidden?: boolean;
label: string;
action?: 'open-checkout' | 'open-paywall'; // default: 'open-checkout'
priceId?: string; // pin to a specific price; otherwise opens picker
}
CosmeticGrid
interface CosmeticGridBlock {
id: string; type: 'cosmetic-grid'; hidden?: boolean;
title?: string;
items: Array<{
name: string;
imageUrl?: string;
rarity?: 'common' | 'rare' | 'epic' | 'legendary';
}>;
}
Game-shop visual treatment — items render with a locked overlay, rarity badges, and a name tag.
Policy
interface PaywallPolicy {
refund: 'standard' | 'no-refunds' | { custom: string };
termsUrl?: string;
privacyUrl?: string;
enforceCwsCompliance?: boolean;
}
When enforceCwsCompliance: true, the renderer auto-injects:
- The refund line (from
refundfield) - Terms + Privacy links (from URLs)
- "You can cancel anytime in your account."
…into the paywall footer, regardless of which template you're using.
Surface
type PaywallSurface = 'extension-popup' | 'extension-overlay' | 'web';
Affects max-width and padding:
| Surface | Max-width | Use case |
|---|---|---|
extension-popup | 360 px | Inside the popup window opened from the extension icon |
extension-overlay | 480 px | Modal-style overlay drawn over the host page |
web | 760 px | Customer Center, hosted checkout, marketing |
Validation
The dashboard validates the top-level shape with Zod
(apps/api/src/routes/dashboard/paywalls.ts). Block-level shape is checked
at render time by the per-block components — bad blocks land in an
error-boundary, the rest of the paywall renders.
Migrations
Bumping schemaVersion requires migrating stored configs. Migration
helpers will live in @crxpay/types so the dashboard can rewrite stored
configs in-place when v2 lands.
Next
- → Publishing — runtime fetch + KV cache + version bumps
Was this page helpful?
Your feedback shapes what we document next.