Skip to main content

Documentation

Frontend SDKs

JS/TS SDK, React bindings, and the embedded admin dashboard.

Frontend

Three companion frontend pieces ship alongside the server module.

@stackhouse/js (JS/TS SDK)

Location: stackhouse/js-sdks/stackhouse-js

import { createClient } from '@stackhouse/js';
const stackhouse = createClient('http://localhost:3000');
await stackhouse.signIn(email, password);

// Customer
const info = await stackhouse.billing.getCustomerInfo(appId, 'user-42');
if (await stackhouse.billing.hasEntitlement(appId, 'user-42', 'pro')) { /* unlock */ }
await stackhouse.billing.submitAppleReceipt(appId, 'user-42', receiptDataB64);

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

await stackhouse.billing.trackImpression(appId, 'user-42');
await stackhouse.billing.trackConversion(appId, 'user-42');

// Admin (requires service-admin JWT)
await stackhouse.billing.admin.upsertProduct({
  app_id: appId, store: 'app_store', store_product_id: 'pro.monthly',
});

// Growth suite admin
await stackhouse.billing.admin.upsertAudience({
  app_id: appId,
  identifier: 'us-users',
  rules: [{ field: 'country', op: 'eq', value: 'US' }],
});

await stackhouse.billing.admin.upsertExperiment({
  app_id: appId,
  identifier: 'price-test',
  metric: 'purchase',
  variants: [
    { identifier: 'control', offering_id: 1, is_control: true, traffic_weight: 50 },
    { identifier: 'treatment', offering_id: 2, is_control: false, traffic_weight: 50 },
  ],
});

const paywall = await stackhouse.billing.admin.upsertPaywall({
  offering_id: 1,
  template: 'default',
  draft_config: { sections: [{ type: 'text', title: 'Go Pro', body: 'Unlock everything.' }] },
});

@stackhouse/react (React bindings)

Location: stackhouse/js-sdks/stackhouse-react

import { BillingProvider, EntitlementGate, Paywall } from '@stackhouse/react';

<BillingProvider appId={42} appUserId="user-42">
  <EntitlementGate identifier="pro" fallback={<Paywall onSelectPackage={buy} />}>
    <ProOnlyFeature />
  </EntitlementGate>
</BillingProvider>

useResolvedOffering / useExperimentConversion are now exported

Both hooks are implemented in stackhouse/js-sdks/stackhouse-react/src/billing.tsx and re-exported from @stackhouse/react since the export gap was closed. import { useResolvedOffering, useExperimentConversion } from '@stackhouse/react' now works. The experiment-aware Paywall example below is live usage.

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

Exports from @stackhouse/react today: BillingProvider, useOfferings, useEntitlements, useCustomerInfo, useHasEntitlement, useResolvedOffering, useExperimentConversion, <EntitlementGate>, <Paywall>.

Admin dashboard

The billing admin UI is embedded in the main Stackhouse Explore app at /admin/billing. It is built from stackhouse/ui and included in the Rust binary via rust-embed when you build stackhouse/ui and then rebuild the Rust server.

cd stackhouse/ui
npm run build

cd ..
cargo build --release

The dashboard manages apps, secrets, products, entitlements, offerings, audiences, experiments, paywalls, promo grants, and outbound webhook endpoints.

Tests

Unit tests (no DB required):

cargo test --lib billing

Covers the entitlement resolver, audience matching, experiment bucketing, Stripe signature round-trip, Apple JWS decoding, and webhook payload signing/filtering.