crxpaydocs

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? };
}
FieldNotes
schemaVersionAlways 1 for now. The renderer rejects configs whose schemaVersion it doesn't know.
nameDisplay name in the dashboard ("Holiday Sale 2026").
templateIdOriginating built-in template, if any (e.g. pixel-shop). Null when hand-rolled.
surface'extension-popup' / 'extension-overlay' / 'web' — affects max-width and padding.
themeVisual identity. See Theme.
blocksOrdered array of block definitions. See Blocks.
policyCWS compliance + refund / terms / privacy. See Policy.
behaviourOptional 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 refund field)
  • 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:

SurfaceMax-widthUse case
extension-popup360 pxInside the popup window opened from the extension icon
extension-overlay480 pxModal-style overlay drawn over the host page
web760 pxCustomer 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.