# reference v0.1

dif/config.yaml.

One YAML file. Checked in. Project name, audience attribute schema, bucketing identity, exposure convention, and build settings.

dif init writes the canonical version below. Edit it by hand; re-running dif init --force overwrites it.

Canonical example

# dif.sh project config. Checked in. Edit by hand or re-run `dif init`.

project: acme-shop
default_surface: home

# Audience attribute schema. The predicate language is closed over this set —
# anything not declared here is a validation error. Each attribute also needs a
# resolver at dif/audiences/<name>.ts.
audience_attributes:
  - name: locale
    type: string
  - name: device_type
    type: enum
    values: [mobile, tablet, desktop]
  - name: returning_visitor
    type: boolean
  - name: lifetime_value
    type: number

# How users are bucketed.
bucketing:
  id: user_id
  fallback: anon_cookie

# Exposure convention. Runtime sinks are configured in dif.init().
exposure:
  sink: webhook
  fire_at: render   # never at assignment.

build:
  out: dif/generated
  fail_on: [conflict, orphan_ref, missing_owner]
  commit_generated: false

Fields

project

Human-readable project identifier. Surfaces in CLI output and the generated context file's metadata.

typerequireddefault
stringyesdirectory name

default_surface

The default surface recorded by the scaffold. Must exist under dif/surfaces/.

// notedif new currently still requires --surface. It does not yet fall back to default_surface.
typerequireddefault
stringyeshome

audience_attributes

The closed set of attribute names experiments may target. Anything used in an experiment's audience block that isn't listed here fails dif validate with E006; a declared attribute with no resolver at dif/audiences/<name>.ts fails with E008. Run dif scaffold-audiences to add the two starter resolvers.

Each entry has name, type, and (for enum types) values:

typenotes
stringFree-form. Locale codes, copy variants, etc.
booleantrue / false.
numberInteger or float. Equality only. No >, <, or ranges.
enumMust be one of values. The list closes the type.

bucketing

Identity for bucketing. Two fields:

fieldnotes
idPrimary user id field. Typically user_id from your auth system.
fallbackWhat to bucket on when the primary is null. Typically anon_cookie.

Both values are advisory; the SDK reads its userId() resolver from dif.init() at runtime. The config exists so other tooling (analytics jobs, the Cloud sync) can agree on the bucketing identity.

exposure

Where exposure events go and when they fire.

fieldnotes
sinkA project-level sink declaration for tooling. The runtime SDK still uses whatever you pass to dif.init({ sink }).
fire_atMust be render. See the note below.

build

Compile-time settings.

fielddefaultnotes
outdif/generatedWhere dif build writes client.ts and audiences.ts. The dif/generated/ directory is gitignored by default.
fail_on[conflict, orphan_ref, missing_owner]Reserved build policy. Today, validation errors always fail the build and warnings never do; this field is here so the policy can be wired up without a schema change.
commit_generatedfalseRecords whether the generated files are meant to be committed. Advisory; it doesn't change CLI behavior today.

A note on fire_at

render is the only correct value. The parser accepts assignment only so dif validate can refuse it with a clear message instead of silently producing wrong data.

Why: bucketing is deterministic. Every call to dif(...) for the same user under the same experiment returns the same variant. What you're counting is "did this user see the variant," not "did the SDK compute the variant." Firing at assignment over-counts audience misses you happen to evaluate, route changes that abort the render, and server placeholders that hydrate to something else. Firing at render counts the actual exposure.

// cloudThere's no cloud: block in the config. dif.sh Cloud is wired up at runtime through dif.init({ project, publishableKey, apiUrl }). See dif.init(). Everything in this file works the same whether or not you use Cloud.