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.
| type | required | default |
|---|---|---|
| string | yes | directory name |
default_surface
The default surface recorded by the scaffold. Must exist under dif/surfaces/.
dif new currently still requires --surface. It
does not yet fall back to default_surface.| type | required | default |
|---|---|---|
| string | yes | home |
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:
| type | notes |
|---|---|
string | Free-form. Locale codes, copy variants, etc. |
boolean | true / false. |
number | Integer or float. Equality only. No >, <, or ranges. |
enum | Must be one of values. The list closes the type. |
bucketing
Identity for bucketing. Two fields:
| field | notes |
|---|---|
id | Primary user id field. Typically user_id from your auth system. |
fallback | What 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.
| field | notes |
|---|---|
sink | A project-level sink declaration for tooling. The runtime SDK still uses whatever you pass to dif.init({ sink }). |
fire_at | Must be render. See the note below. |
build
Compile-time settings.
| field | default | notes |
|---|---|---|
out | dif/generated | Where 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_generated | false | Records 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.
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.