Splitstreamportfolio demo

Splitstream + Pennant — the joint stack

Pennant is a feature-flag / remote-config service with real-time SDK push. Splitstream is an A/B testing platform. The natural pair:

The @philiprehberger/growth umbrella wraps both clients in a single useExperiment():

import { Pennant } from "@philiprehberger/pennant";
import { Splitstream } from "@philiprehberger/splitstream";
import { Growth } from "@philiprehberger/growth";

const growth = new Growth({
  pennant: new Pennant({
    apiKey: process.env.PENNANT_KEY!,
    context: { userId: "alice" },
  }),
  splitstream: new Splitstream({
    apiKey: process.env.SPLITSTREAM_KEY!,
    context: { userId: "alice" },
  }),
});

const { variant, killSwitchActive, track } = await growth.useExperiment(
  "checkout-v2",
  { fallback: "control" }
);

if (killSwitchActive) {
  // Pennant flag was off — Splitstream was NOT consulted.
  // Exposure was NOT logged. Returns 'control' (or your controlKey).
} else {
  // Pennant flag was on — Splitstream.assign() ran, exposure logged.
  // variant ∈ {'control', 'treatment', null}.
}

track("purchase-completed", { revenue: 49 });

What the umbrella does (and doesn't)

Override the kill-switch convention

new Growth({
  pennant,
  splitstream,
  killSwitchKeyFor: (experimentKey) => `exp.${experimentKey}.gate`,
  killSwitchDefault: false, // fail closed when the flag isn't in the snapshot
});

React

import { GrowthProvider, useExperiment } from "@philiprehberger/growth/react";

function App() {
  return (
    <GrowthProvider growth={growth}>
      <CheckoutButton />
    </GrowthProvider>
  );
}

function CheckoutButton() {
  const { variant, killSwitchActive, isLoading, track } = useExperiment("checkout-v2", {
    fallback: "control",
  });
  if (isLoading) return <button disabled>Loading…</button>;
  return (
    <button
      className={variant === "treatment" ? "bg-green-500" : "bg-blue-500"}
      onClick={() => track("purchase-completed", { revenue: 49 })}
    >
      Buy now
    </button>
  );
}

Why ship them as separate products

Same buyer overlap, different artifacts. Pennant's headliner is the SDK + SSE story. Splitstream's headliner is the assignment API + the credible-interval readout. Sharing a repo would muddle both pitches. They cross-link from the docs sites, and the joint sample is the same five lines on both. The umbrella exists so the joint pitch can be one call.