# 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/`.

> // note `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`](https://dif.sh/docs/cli/#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.](#fire-at) |

### 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 There's no `cloud:` block in the config. dif.sh Cloud is wired up at runtime through `dif.init({ project, publishableKey, apiUrl })`. See [dif.init()](https://dif.sh/docs/sdk/#init). Everything in this file works the same whether or not you use Cloud.
