Skip to main content

Documentation

Experiments & Paywalls

A/B testing, audience targeting, and visual paywall configuration in Stackhouse-Billing.

Experiments & Paywalls

Stackhouse-Billing's growth suite adds four pieces on top of the core subscription model:

  • Audiences — rule-based customer targeting
  • Experiments — A/B tests whose variants point at existing offerings
  • Paywalls — per-offering visual configuration with draft/publish
  • Remote-config resolution — a customer endpoint that returns the right offering + paywall for that user

Data model

The new tables are created alongside the existing billing_* tables by the same idempotent migration:

  • billing_audiences
  • billing_experiments
  • billing_experiment_variants
  • billing_experiment_assignments
  • billing_experiment_events
  • billing_paywalls

Offerings also gained an optional audience_id column, used to restrict an offering to a specific audience.

Audience rules

An audience stores an array of { field, op, value } rules. The matching engine evaluates rules against:

  • customer attributes JSONB
  • request context (country, app_version)
  • derived flags (is_existing_subscriber)

Supported operators are eq, neq, gt, gte, lt, lte, in, and exists. All rules in an audience must match for the customer to be included.

Bucketing

When a customer hits the per-user resolution endpoint:

  1. Existing experiment assignments are read first; if found, that variant is sticky.
  2. For each running experiment, the customer is checked against its optional audience.
  3. A deterministic hash of (experiment_id, customer_id) maps to a weighted variant.
  4. The assignment is persisted, so later traffic-weight changes do not re-bucket already-assigned customers.

The hashing function is a truncated SHA-256 digest; the same inputs always produce the same variant.

Events and results

POST /v1/billing/customers/:app_user_id/experiments/impression and POST /v1/billing/customers/:app_user_id/experiments/conversion append rows to billing_experiment_events.

GET /v1/billing/admin/experiments/:id/results returns per-variant counts and a z-score for treatment variants versus control. These numbers are estimates based on distinct-customer counts and should not be treated as rigorous statistical inference without further analysis.

Endpoints

Customer

MethodEndpointNotes
GET/v1/billing/customers/:app_user_id/offerings/resolve?app_id=…Returns { offering, paywall, experiment }
POST/v1/billing/customers/:app_user_id/experiments/impressionRecord an impression for the assigned variant
POST/v1/billing/customers/:app_user_id/experiments/conversionRecord a conversion for the assigned variant

Admin

MethodEndpointNotes
POST / GET/v1/billing/admin/audiencesUpsert / list audiences
POST / GET/v1/billing/admin/experimentsUpsert / list experiments
POST/v1/billing/admin/experiments/:id/statusSet draft, running, paused, or completed
GET/v1/billing/admin/experiments/:id/resultsVariant statistics
POST/v1/billing/admin/offerings/:offering_id/audienceAttach an audience to an offering
POST / GET/v1/billing/admin/paywallsUpsert / get paywall config
POST/v1/billing/admin/paywalls/:offering_id/publishPromote draft to live config

SDK usage

import { createClient } from '@stackhouse/js';
const stackhouse = createClient('http://localhost:8080');

const resolved = await stackhouse.billing.getResolvedOffering(
  appId,
  'user-42',
  { country: 'US', app_version: '1.2.0' },
);

// Paywall shown; later, when a purchase completes:
await stackhouse.billing.trackConversion(appId, 'user-42');
import { Paywall, useExperimentConversion } from '@stackhouse/react';

function MyPaywall() {
  const track = useExperimentConversion(appId, 'user-42');
  return (
    <Paywall
      appId={appId}
      appUserId="user-42"
      context={{ country: 'US' }}
      onConversion={track}
      onSelectPackage={(pkg, offering) => startPurchase(pkg, offering)}
    />
  );
}

Admin UI

The billing admin interface is embedded at /admin/billing in the main Stackhouse Explore UI. New tabs cover Audiences, Experiments, and Paywalls.