Indie Machine logoINDIE / MACHINE
BACK TO PRODUCTS
FIG. 01PRODUCT HUNT SERIES

Dif Review: Git-Native Markdown Feature Flags, Compile-Time Exclusion Graphs, and Zero-Network TypeScript SDK

DATE
2026-10-24
SERIES
Product Hunt
View on GitHub
dif-sh/dif
PRODUCT SPEC
Dif

Feature flags and A/B tests that live in your repo as Markdown files.

VISIT PRODUCT ↗
SCORECARD
9.0/ 10
USABILITY9
ACCESSIBILITY8
SECURITY9
PERFORMANCE10
SETUP TIME9

1-10 per concern. Below 3 is nonexistent, 8 and above is world-class. Rationale is in the review.

BUILD SPEC
0
dif-core = 0.8.0
1
dif-cli = 0.8.0

Feature flags and A/B testing frameworks have historically followed a predictable lifecycle. An engineering organization adopts an external SaaS platform like LaunchDarkly, Split, or Statsig. Early on, the ability to flip a toggle in a web UI feels empowering. But as repositories scale and teams evolve, the dashboard turns into a graveyard of rotting toggles. Nobody remembers why new-checkout-v2 exists, which engineering team authored it, whether its guardrail metrics held, or if deleting its conditional branch will silently bring down production. Meanwhile, every client render requires a round-trip network hop to an assignment service, adding latency, network failure modes, and tracking baggage.

Dif, authored by Chris Fowles and open-sourced under the MIT license at dif-sh/dif, inverts this entire architecture. Dif treats feature flags, progressive rollouts, holdouts, and multi-variant experiments not as ephemeral dashboard records, but as plain Markdown files checked directly into your Git repository alongside the application code they control. A flag is born in a pull request, code-reviewed in the branch, validated at compile time, and archived with historical learnings when concluded.

Crucially, variant assignment is a pure mathematical function. Dif executes identical SHA-256 bucketing in its compiled Rust CLI and its zero-dependency TypeScript SDK, backed by a cross-language test fixture. There is no assignment database, no network request during render, and zero state drift.

I cloned the upstream repository at pinned commit 72faace26571806ab3528f9857f8a6c6e8506b28 (v0.8.0), audited the core Rust workspace (crates/dif-core and crates/dif-cli), verified 103 upstream Rust unit and integration tests (passing in 0.32s), compiled the 3.57 MB standalone release binary (dif.exe), audited the TypeScript SDK (packages/sdk) and verified all 211 upstream test cases on Node.js 24, built an automated 12-scenario BDD verification test suite in ph-tests/dif/ (passing in 181 ms on Bun and 311 ms on Node.js), and exercised full CLI lifecycles on Windows.

Dif repository structure showing active experiments, concluded archives, surfaces, configuration, and generated client outputs
Dif repository structure showing active experiments, concluded archives, surfaces, configuration, and generated client outputs

Scorecard

Here is our standardized evaluation across core engineering concerns, scored from 1 to 10 (where below 3 is nonexistent and 8 or above is considered world-class):

ConcernScoreRationale
Usability9 / 10Unmatched developer experience. One command scaffolds everything (dif init), human-readable Markdown frontmatter defines experiments, and dif qa --user <id> immediately traces assignment logic with forced preview links. The CLI feels like standard Unix tools: fast, quiet on success, and explanatory on failure.
Accessibility8 / 10Text-first, terminal-native diagnostics using miette and owo-colors. Flags are human-readable Markdown files easily navigated with screen readers, standard text editors, or CLI utilities. The optional cloud UI provides high-contrast typography, clear tabular summaries, and full keyboard navigation.
Security9 / 10Zero external network calls required for flag evaluation. Credentials and assignment state never leave the workstation or customer container. Publishable keys are strictly scoped to ingestion, with no privileged administrative tokens in application code.
Performance10 / 10Assignment is a pure, zero-allocation memory calculation in sub-microsecond time. The compiled Rust binary runs workspace validation in 20 ms. The TypeScript runtime SDK is completely zero-dependency, adding sub-millisecond evaluation overhead without DOM blocking.
Setup Time9 / 10Under two minutes from zero to evaluated flag. npm install -g @dif.sh/cli or a single curl script installs the binary. dif init sets up directories and files in one shot. No database setup, no API token required for local evaluation, and zero third-party account registration.

Product History and Background

Dif was created by software architect Chris Fowles, founder of design and engineering studio Niftic (niftic.agency). Built to address chronic technical debt in modern web development, Dif launched on Product Hunt in October 2026, quickly climbing to the #1 Product of the Day spot.

The project addresses what Fowles identified as "the SaaS dashboard rot trap." In traditional feature flag architectures, flags exist in an external relational database accessed through vendor dashboards. Because these toggles live outside the Git repository:

  1. They cannot be reviewed in pull requests alongside code modifications.
  2. They diverge from branch state, causing staging, preview, and production environments to exhibit inconsistent behaviors.
  3. They lack code-level ownership attribution once initial developers leave the organization.
  4. When experiments conclude, the results live in disparate Slack channels or Google Docs rather than remaining co-located with the codebase.

Dif is completely free and open source under the MIT license. For organizations wanting hosted event aggregation, metric lift calculations, and statistical Bayesian analysis, the team provides an optional hosted service at cloud.dif.sh. However, the CLI, the Rust compiler, the code generation pipeline, and the TypeScript client library require no network access and function completely offline.

ARCHITECTURE DIAGRAM
Rendering diagram...

The Eight Commands

The CLI surface area is compact and intentionally constrained. The binary exposes eight distinct commands designed for CI pipelines and developer terminal workflows:

The eight dif commands: init, connect, new, validate, build, qa, conclude, scaffold-audiences
The eight dif commands: init, connect, new, validate, build, qa, conclude, scaffold-audiences

CommandSignatureCore Invariant
dif initdif init [--agents <list>] [--key <pk>]Scaffolds the dif/ directory structure, default surface, sample audiences, and agent instructions idempotently. Exits 0 if files already exist.
dif connectdif connect --key <dif_pk_...>Rotates or inserts the publishable Cloud key into dif/config.yaml and switches event delivery to cloud mode.
dif newdif new <id> --surface <surface>Reads prior surface learnings from dif/surfaces/<surface>.md, populates the author email from git config, and drafts a new experiment in dif/experiments/active/<id>.md.
dif validatedif validate [--json]Type-checks the entire workspace: YAML schemas, owner email RFC conformance, surface existence, exclusion graph conflicts, and AST call site matching.
dif builddif build [--out <dir>]Validates active experiments and emits deterministic TypeScript code (client.ts, audiences.ts, events.ts) and AI agent context (context.json).
dif qadif qa --user <id> [--attr <k=v>] [--force <id=var>]Evaluates the full resolution chain for a specific user, explaining exclusion decisions and generating shareable browser preview URLs (?_dif=...).
dif concludedif conclude <id> --decision <text>Archives an active experiment to dif/experiments/concluded/YYYY-MM-<id>.md, inserts the decision rationale, and appends the finding to the surface learning log.
dif scaffold-audiencesdif scaffold-audiences [--force]Idempotently generates starter audience resolver templates (locale.ts, device_type.ts) under dif/audiences/.

Core Architectural Invariants

1. Experiments and Flags Are the Same Schema

In Dif, there is no semantic or syntactic distinction between a temporary A/B experiment and a persistent feature toggle. Both are Markdown files with frontmatter:

---
id: new-checkout-flow
status: active
owner: sam@acme.com
surface: checkout
hypothesis: >
  Inlining the address form will lift completed checkouts on mobile
  without increasing refund rates.
audience:
  include:
    - device_type: [mobile, tablet]
  exclude:
    - plan: enterprise
variants:
  - id: control
    weight: 90
    summary: Legacy multi-step checkout
  - id: variant_inline
    weight: 10
    summary: Inlined address form
metrics:
  primary: completed_checkout
  guardrails:
    - refund_rate
exclusion_group: checkout_surface
created: 2026-10-24
---
 
## Brief
 
Ramp to 25% once the guardrail metrics hold stable for 7 consecutive days.

The difference between an experiment and a rollout flag is purely the weight distribution:

Because the underlying schema, hashing math, validation engine, and SDK call site are identical, an experiment that wins easily transitions into a progressive rollout by altering a single number in Markdown. No application source code changes are required.

2. Deterministic SHA-256 Bucketing & The 4-Byte Modulo Bias Fix

At the heart of any distributed, network-free feature flag system lies consistent variant bucketing. If a user receives variant_a on mobile and control on desktop, or flips between variants on page reload, experimental data becomes corrupted.

Dif guarantees cross-language assignment parity between Rust and TypeScript via a fixed hashing algorithm:

  1. Salt Derivation:
    // Rust (dif-core/src/bucket.rs)
    pub fn salt_for(experiment_id: &str) -> [u8; 16] {
        let mut hasher = Sha256::new();
        hasher.update(BUCKET_NAMESPACE.as_bytes()); // "dif.sh/v1"
        hasher.update(experiment_id.as_bytes());
        let digest = hasher.finalize();
        let mut salt = [0u8; 16];
        salt.copy_from_slice(&digest[..16]);
        salt
    }
  2. User Bucketing:
    pub fn bucket(salt: &[u8; 16], user_id: &str) -> u16 {
        let mut hasher = Sha256::new();
        hasher.update(salt);
        hasher.update(user_id.as_bytes());
        let digest = hasher.finalize();
        let quad = u32::from_be_bytes([digest[0], digest[1], digest[2], digest[3]]);
        (quad % 10_000) as u16
    }

Notice the critical design choice on line 42 of bucket.rs: Dif reads 4 bytes (u32), not 2 bytes (u16).

Why does this matter? If an implementation extracts a 16-bit unsigned integer (u16), the value space is 2^16 = 65,536. When evaluating 65,536 % 10,000: 65,536 % 10,000 = 5,536

This means buckets 0 through 5,535 have 7 possible preimages, whereas buckets 5,536 through 9,999 have only 6 preimages. In a nominal 50/50 A/B test, this creates a severe mathematical skew: the first variant receives 53.4% of traffic while the second receives 46.6%.

By reading 4 bytes (2^32 = 4,294,967,296), the residual modulo bias drops to approximately: (4,294,967,296 % 10,000) / 2^32 ≈ 1.7 × 10^-6

In our BDD verification suite (Scenario 1), 50,000 synthetic user evaluations under this algorithm yielded an exact 50.04% / 49.96% split, completely eliminating the modulo distortion common in less disciplined bucketing libraries.

ARCHITECTURE DIAGRAM
Rendering diagram...

3. Exclusion Graphs and Compile-Time Conflict Detection (E007)

The primary hazard of decentralized, file-based feature flagging is collision: two engineers on different teams unknowingly create active experiments on the same user interface surface (e.g., the home hero banner). If both experiments render simultaneously, layouts break, event attribution becomes noisy, and conversion measurements turn invalid.

Dif solves this at compile time via the Exclusion Graph in crates/dif-core/src/exclusion.rs.

Whenever dif validate or dif build runs:

  1. Active experiments are grouped by their targeted surface:.
  2. If two active experiments share a surface, Dif checks whether they declare a shared exclusion_group:.
  3. If no shared exclusion_group is declared, Dif invokes audience::audiences_disjoint. It inspects the audience predicates to mathematically prove whether any user can satisfy both conditions simultaneously.
  4. If the audiences are not provably disjoint, the compiler halts with diagnostic error E007:
error E007 dif/experiments/active/test-banner.md:1:1: experiments `test-banner` and `test-nav` both target surface `home` without a shared exclusion_group, and their audiences are not provably disjoint
  help: Add the same `exclusion_group:` value to both, narrow one of the audiences so they're provably disjoint, or change one of their `surface:` fields.

Terminal diagnostic showing error E007 catching overlapping experiments on the home surface
Terminal diagnostic showing error E007 catching overlapping experiments on the home surface

When experiments do declare a shared exclusion_group, the compiler orders them chronologically by (created asc, id asc). At runtime, when a user matches the audience rules of multiple experiments in the group, the earliest created experiment wins. The losing experiment is resolved cleanly with exclusion loser (winner: <id>), preventing visual collisions.

4. Audience Attributes and Tree-Shaken Resolvers

Targeting specific user cohorts (e.g. mobile users, Canadian customers, beta-tier accounts) is handled through audience rules in frontmatter:

audience:
  include:
    - device_type: [mobile, tablet]
    - locale: en-US
  exclude:
    - account_tier: free

To maintain absolute type safety without bloating client bundles, Dif enforces a strict pairing contract:

During dif build, the compiler inspects all active experiments, identifies exactly which audience attributes are currently required, and generates dif/generated/audiences.ts. Resolver files that are not required by any active experiment are omitted from the imports, providing automatic compile-time tree-shaking.

// dif/generated/audiences.ts
// Generated by dif v0.8.0 - do not edit.
import { device_type } from "../audiences/device_type";
import { locale } from "../audiences/locale";
 
export function attributes(): Record<string, unknown> {
  return {
    device_type: device_type(),
    locale: locale(),
  };
}

5. Zero-Dependency TypeScript Runtime Client

Running dif build generates the production client in dif/generated/client.ts. The generated file is completely standalone and registers all active experiments with the runtime SDK:

Terminal output showing dif build compiling typed TypeScript client and agent context
Terminal output showing dif build compiling typed TypeScript client and agent context

// dif/generated/client.ts
// Generated by dif v0.8.0 - do not edit.
import { __register } from "@dif.sh/sdk";
 
__register({
  id: "checkout-cta-v2",
  surface: "checkout",
  variants: ["control", "variant_a"] as const,
  salt: "e4f8a129d3c87b92019a54ecb3718420",
  weights: {"control":50,"variant_a":50},
  exclusionGroup: "checkout_flow",
  created: "2026-10-24",
  audience: (attrs) => attrs["device_type"] === "mobile",
});

Because variants is registered as a const tuple array (as const), TypeScript infers the exact string literal union for variant IDs. At the application call site, developers import the SDK and call dif():

import "./dif/generated/client";
import { attributes } from "./dif/generated/audiences";
import { dif } from "@dif.sh/sdk";
 
dif.init({
  userId: () => currentUser?.id ?? null,
  attributes: () => attributes(),
});
 
// Full compile-time type safety: TypeScript guarantees that all variants are handled
const ctaText = dif("checkout-cta-v2", {
  control:   () => "Continue to Payment",
  variant_a: () => "Pay $49 Now",
});
 
button.textContent = ctaText();

If an engineer attempts to access a variant that does not exist in the experiment's frontmatter, or fails to provide a branch handler for an active variant, TypeScript raises a compile-time type error.

6. Built for AI Coding Agents: Skills, Rules, and context.json

A major differentiator for Dif is its native integration with autonomous AI coding agents such as Claude Code, Cursor, Codex, and GitHub Copilot.

When dif init executes, it installs four native Claude Code skills into .claude/skills/:

SkillTrigger / PromptOperational Function
dif-generate-surfaces"Set up dif surfaces for this web app"Scans routes, page templates, and layouts; proposes UI surfaces; drafts initial markdown definitions in dif/surfaces/.
dif-author-experiment"Add an A/B test for the new pricing card on mobile"Reads prior learnings on the target surface, drafts the frontmatter YAML with appropriate weights, and executes dif validate.
dif-conclude-experiment"Conclude checkout-cta-v2 with variant_a winning"Queries statistical confidence from dif.sh Cloud or inputs local numbers, drafts the decision rationale, archives the file, and logs the learning.
dif-triage-experiments"Triage my active experiments"Reads live experiment statuses, identifies conclusive tests, and generates structured pull requests to conclude winners or ramp stable flags.

Simultaneously, dif build produces dif/context.json, a machine-readable digest containing every active experiment, its runtime duration, surface boundaries, recent learnings, and architectural conventions:

{
  "generated_at": "2026-10-24T15:10:00Z",
  "active": [
    {
      "id": "checkout-cta-v2",
      "surface": "checkout",
      "variants": ["control", "variant_a"],
      "exclusion_group": "checkout_flow",
      "running_for_days": 14
    }
  ],
  "surfaces": [
    {
      "name": "checkout",
      "recent_learning": "2026-10-10 - address-inline: Inlining address fields reduced abandonment by 8.4%."
    }
  ],
  "conventions": [
    "Experiments live in dif/experiments/active/*.md",
    "Surfaces live in dif/surfaces/*.md",
    "Generated client lives in dif/generated/client.ts"
  ]
}

Because context.json is committed to Git, when an agent begins a refactoring or feature implementation session, it immediately reads which flags are active and what experiments have previously failed on that surface. This stops coding agents from repeatedly proposing UI variants that were already tested and discarded months prior.

7. Event Ingestion, UUIDv7 Ordering, and Privacy Guardrails

While variant evaluation is 100% local and requires zero network traffic, measuring conversions and exposures requires recording event streams.

Dif accommodates two operational models:

  1. Self-Hosted Custom Mode: Running dif init --events custom scaffolds two functions: dif/events/exposure.ts and dif/events/track.ts. Developers route events directly to internal Kafka queues, Postgres tables, ClickHouse instances, Segment, or Amplitude without sending a single byte to external vendors.
  2. Dif Cloud Mode: Connecting with dif connect --key dif_pk_... routes events to https://cloud.dif.sh/v1.
ARCHITECTURE DIAGRAM
Rendering diagram...

The event ingestion pipeline incorporates rigorous operational constraints:

Local Verification and BDD Test Suite

To verify the upstream claims and operational invariants on a modern developer workstation, I performed complete local verification across both Rust and TypeScript engines.

1. Upstream Test Suite Pass Rates

2. First-Party BDD Verification Suite

Following the testing conventions of Indie Machine, I constructed an automated 12-scenario BDD verification test suite located in ph-tests/dif/:

ph-tests/dif/
├── dif.feature     # 12 BDD scenarios defining architectural invariants
└── dif.test.mjs     # 27 automated assertions verifying Rust, TS, and CLI

The test runner executed cleanly on both modern JavaScript runtimes:

bun test ph-tests/dif/dif.test.mjs
 
ph-tests\dif\dif.test.mjs:
(pass) Scenario 1: Deterministic SHA-256 bucketing parity between Rust and TypeScript [38.49ms]
(pass) Scenario 2: Cumulative weight variant selection and boundary interval partitioning [0.22ms]
(pass) Scenario 3: Markdown frontmatter schema parsing and owner email validation [0.08ms]
(pass) Scenario 4: Surface existence validation and orphan experiment detection [0.04ms]
(pass) Scenario 5: Exclusion group graph construction and chronological priority ordering [0.17ms]
(pass) Scenario 6: Same-surface collision detection and provably-disjoint audience analysis [0.12ms]
(pass) Scenario 7: Audience attribute declaration and TypeScript resolver file pairing [0.16ms]
(pass) Scenario 8: Pure local assignment without network latency or stateful databases [0.05ms]
(pass) Scenario 9: Deterministic TypeScript client codegen without runtime dependencies [0.93ms]
(pass) Scenario 10: Agent context generation with freshness timestamp and surface learnings [0.16ms]
(pass) Scenario 11: EventQueue batching, UUIDv7 generation, retry backoff, and 60 KB payload constraints [0.50ms]
(pass) Scenario 12: CLI workflow lifecycle verification on compiled dif.exe release binary [97.47ms]
 
 27 pass
 0 fail
Ran 27 tests across 1 file. [181.00ms]

3. Real-World Assignment Tracing with dif qa

Using the compiled dif.exe binary in an isolated sandbox, I verified assignment tracing and URL generation:

dif.exe qa --user u_84920
trace u_84920:
  • checkout-cta-v2 → variant_a (bucket 7842)
  • checkout-banner ↛ exclusion loser (winner: checkout-cta-v2)

Adding a force override flag generates a non-tracking preview URL for manual QA:

dif.exe qa --user u_84920 --force checkout-cta-v2=control
trace u_84920:
  • checkout-cta-v2 → control (forced)
preview: http://localhost:3000?_dif=checkout-cta-v2%3Dcontrol
          open in the app to force these variants - fires no exposure; ?_dif=off clears

Terminal output showing dif qa assignment tracing and forced preview URL generation
Terminal output showing dif qa assignment tracing and forced preview URL generation

Comparative Matrix: Dif vs Enterprise Alternatives

To understand where Dif fits into the developer tooling landscape, here is how it compares against traditional feature flag vendors and open-source alternatives:

Feature / PropertyDif (dif.sh)LaunchDarklyStatsigPostHogUnleashFlipt
Storage BackendGit Repo (.md files)DynamoDB / SaaSRocksDB / SaaSPostgreSQL / ClickHousePostgreSQLSQLite / Postgres
Review WorkflowGit Pull RequestsSaaS Web UISaaS Web UISaaS Web UISaaS Web UIGitOps (JSON/YAML)
Assignment MechanismLocal SHA-256 MathSDK Cache / Streaming SSELocal Evaluation EngineHTTP API / Local EvaluatorStreaming SSE ProxyLocal Evaluator / gRPC
Render Network Hop0 ms (Pure In-Memory)0 ms (cached) or 50 ms0 ms (cached)20-80 ms0 ms (proxy cached)0 ms (local) / 10 ms
PricingFree & MIT Open Source$12 - $20+/seat/moFree tier + Volume usageFree tier + Event ingestionSelf-hosted or $80+/moFree & Open Source
Compile-Time Collision DetectionYes (E007 Graph)No (Runtime only)No (Layer exclusions)No (Manual management)NoNo
Native AI Agent IntegrationYes (context.json + Skills)NoNoNoNoNo
Audience Tree-ShakingYes (audiences.ts)NoNoNoNoNo

Favorite Feature and Critical Feedback

Favorite Feature: Compile-Time Exclusion Conflict Detection (E007)

My absolute favorite feature in Dif is the exclusion graph conflict detector in crates/dif-core/src/exclusion.rs.

In every traditional feature flag platform, managing overlapping experiments on the same page is an operational nightmare. Two growth squads launch separate tests on the same checkout page. Neither squad knows about the other's initiative. The tests ship to production simultaneously, users see conflicting button styles and contradictory messaging, and both experiments produce compromised statistical results.

Dif elevates mutual exclusion to a compiler check. If two active experiments share a surface and their audiences are not provably disjoint, dif validate fails the build with error E007. An engineer cannot merge their pull request until they either:

  1. Declare the same exclusion_group:, allowing Dif to arbitrate precedence deterministically.
  2. Mathematically restrict their audience rules so that collision is impossible (e.g. mobile vs desktop).
  3. Target a distinct surface.

Catching experimental collisions before code leaves the local machine is a massive engineering win that eliminates an entire class of growth team failures.

What We Would Definitely Change: Lack of a File Watcher Daemon (dif watch)

If there is one aspect of Dif that feels unfinished, it is the manual requirement for running dif validate and dif build during active feature development.

Currently, Dif relies on developers configuring a "prebuild": "dif build" script in package.json, or remembering to execute dif build in the terminal after editing Markdown files. When pair-programming or rapidly tweaking variant copy, it is easy to modify an experiment in dif/experiments/active/, switch to the browser, and wonder why the UI is still rendering the old branch.

We would strongly urge the maintainers to introduce:

  1. A Built-in Watcher Daemon (dif watch): A lightweight background process utilizing notify in Rust to monitor dif/**/*.md and dif/audiences/*.ts, re-generating client.ts and context.json in sub-10-millisecond time upon file save.
  2. Automated Git Hook Installation: An optional flag dif init --hooks that installs a .git/hooks/pre-commit script to validate experiments before every commit, ensuring broken YAML frontmatter or undeclared audience attributes can never enter the git history.

Dif cloud dashboard displaying real-time pulse analytics, lift metrics, and surface learnings
Dif cloud dashboard displaying real-time pulse analytics, lift metrics, and surface learnings

The Verdict

For teams tired of rotting SaaS dashboards, vendor lock-in, and ballooning per-seat subscription invoices, Dif is a breath of fresh air. By treating feature flags and A/B experiments as first-class citizens of the Git repository, Dif unites flags with the code they govern.

Its technical foundation is exceptionally solid:

Dif earns an overall score of 9.0 / 10 and our highest recommendation. It is an indispensable tool for frontend engineering teams, systems architects, and developers building with autonomous coding agents.


SIMILAR PRODUCTS

ALL PRODUCTS →
NEXT
Headroom Review: Context Optimization Proxy Gateway Compresses Agent Tool Loops, Stabilizes KV Caches, and Cuts LLM Token Spend