Pennantportfolio demo

Feature flags · real-time SDK · production-shaped

A feature flag service that ships the whole product surface a product team expects — not just an API behind a README.

Pennant is a portfolio demonstration. A Laravel API with a targeting rule engine, a Filament admin with audit trails, a Node SSE broadcaster, and four SDKs that share a cross-implementation drift corpus — so the rule engine on your server and the rule engine in the browser can never disagree about who's in the rollout.

Four SDKs, one contract

TypeScript core, React adapter, PHP / Laravel, Python / Django / FastAPI. Every implementation runs the same drift corpus in its native test runner. Sub-200ms updates via SSE; deterministic SHA-256 bucketing locally so percentage rollouts don't flicker across page refreshes.

TypeScript
import { Pennant } from '@philiprehberger/pennant';

const flags = new Pennant({
  apiBase: 'https://api.pennant.philiprehberger.com',
  apiKey: process.env.NEXT_PUBLIC_PENNANT_KEY!,
  environment: 'prod',
  context: { userId: 'alice', plan: 'enterprise' },
});

if (flags.bool('new-checkout-flow', false)) {
  // ...
}
React
import { PennantProvider, useBool } from '@philiprehberger/pennant-react';

function Checkout() {
  // Re-renders only when this specific flag changes.
  const enabled = useBool('new-checkout-flow', false);
  return enabled ? <NewCheckout /> : <LegacyCheckout />;
}

export default function App() {
  return (
    <PennantProvider options={{ apiBase, apiKey, environment, context }}>
      <Checkout />
    </PennantProvider>
  );
}
PHP
use Pennant\Pennant;

$flags = new Pennant([
    'api_base' => 'https://api.pennant.philiprehberger.com',
    'api_key' => getenv('PENNANT_KEY'),
    'environment' => 'prod',
    'context' => ['userId' => 'alice', 'plan' => 'enterprise'],
]);

if ($flags->bool('new-checkout-flow', false)) {
    // ...
}
Python
from pennant import Pennant

flags = Pennant(
    api_base="https://api.pennant.philiprehberger.com",
    api_key=os.environ["PENNANT_KEY"],
    environment="prod",
    context={"userId": "alice", "plan": "enterprise"},
)

if flags.bool("new-checkout-flow", False):
    ...

What's actually in the box

Rule engine that doesn't drift

Ten operators, all/any/none combinators, segment references with static cycle detection, dotted attribute paths. The same evaluator runs on the Laravel server and inside every SDK. A 34-case drift corpus + 5 bucketing cases at tests/corpus/rules.json round-trips through each implementation in CI. If the server says yes and the browser says no, that's a bug, and the corpus catches it.

Real-time without the ops sprawl

A small Node SSE broadcaster subscribes to Redis pub/sub; the Laravel API publishes a tiny envelope on every flag configuration write. SDKs hold a long-lived EventSource and apply diffs to their in-memory snapshot. Last-Event-ID replay covers brief disconnects; polling fallback covers everything else.

Offline-resilient client SDKs

The TypeScript SDK persists last-known-good snapshots to localStorage (browser) or memory (Node) and emits ready immediately on cold start even with no network. The cache survives malformed JSON and storage quota errors without ever crashing your app.

Two key kinds, one safe browser story

pn_srv_ keys see the full rule logic. pn_clt_ keys receive pre-evaluated values for the context attached to the request — the rules never leave the server. The same SDK handles both transparently.

Multi-environment + audit log + kill switch

Per-workspace environments (dev / staging / prod / custom), promote-between-environments with a diff, audit log on every mutation captured with the actor and reason, and a one-click kill-switch for production with a mandatory reason field.

Built like infrastructure, framed honestly

This is a portfolio demonstration, not a production service — see the about page for the framing. Every feature here is the kind of thing a team has to build for itself if it doesn't outsource the work. The point of the demo is to make that decision easier.