braze-sync 0.10.0

GitOps CLI for managing Braze configuration as code
Documentation
# Configuration

`braze-sync` is driven by a single YAML file, `braze-sync.config.yaml`, in
the root of your workspace. `braze-sync init` scaffolds a commented
template; this page documents every field.

The config schema is **frozen at v1.0** under the `version: 1` tag. A
future v2 schema would bump that number — v1.x binaries will only accept
`version: 1`.

## Full example

```yaml
version: 1

default_environment: dev

environments:
  dev:
    api_endpoint: https://rest.fra-02.braze.eu
    api_key_env: BRAZE_DEV_API_KEY
  prod:
    api_endpoint: https://rest.fra-02.braze.eu
    api_key_env: BRAZE_PROD_API_KEY

resources:
  catalog_schema:
    enabled: true
    path: catalogs/
  content_block:
    enabled: true
    path: content_blocks/
  email_template:
    enabled: true
    path: email_templates/
  custom_attribute:
    enabled: true
    path: custom_attributes/registry.yaml

naming:
  catalog_name_pattern: "^[a-z][a-z0-9_]*$"
  content_block_name_pattern: "^[a-zA-Z0-9_]+$"
  custom_attribute_name_pattern: "^[a-z][a-z0-9_]*$"
```

## Top-level fields

### `version` (required)

Config schema version. Must be exactly `1` for every v1.x release of
`braze-sync`. Any other value is a config error (exit code `3`).

### `default_environment` (required)

Name of the environment used when `--env` is not passed on the command
line. Must match a key under `environments`.

### `environments` (required, at least one entry)

Map of environment name → settings. Pick the active one with
`--env <name>`; otherwise `default_environment` applies.

| Field | Type | Required | Notes |
|:---|:---|:---|:---|
| `api_endpoint` | URL | yes | Braze REST endpoint for your instance (see [Braze API endpoints]https://www.braze.com/docs/api/basics/#endpoints). The scaffold defaults to the EU `fra-02` cluster — change it if your instance lives elsewhere. |
| `api_key_env` | string | yes | Name of the **environment variable** holding the API key. The key itself must never appear in this file. |

API keys are loaded into `secrecy::SecretString` at startup and never
appear in `Debug` output, tracing, or panic messages. A `.env` file in
the current working directory is loaded via `dotenvy` (no recursive
parent-directory search).

### Rate limiting

braze-sync does not carry a client-side rate limiter. The HTTP client
reacts to 429 responses by honoring `Retry-After` when present and
falling back to exponential backoff with jitter otherwise. Total retry
sleep is bounded by an internal budget; beyond that a
`RateLimitExhausted` error surfaces to the caller.

### `resources` (optional)

Toggles and paths for each v1.0 resource kind. Every sub-block is
optional — omitted entries fall back to the defaults shown below. To
skip a resource entirely in a workspace, set `enabled: false`.

Every resource sub-block also accepts an optional
`exclude_patterns: [<regex>, …]` list; see
[§ exclude_patterns](#exclude_patterns) below.

#### `catalog_schema`

| Field | Type | Default |
|:---|:---|:---|
| `enabled` | bool | `true` |
| `path` | path | `catalogs/` |
| `exclude_patterns` | list of regex strings | `[]` |

Directory holding one `<catalog>/schema.yaml` per catalog.

#### `content_block`

| Field | Type | Default |
|:---|:---|:---|
| `enabled` | bool | `true` |
| `path` | path | `content_blocks/` |
| `exclude_patterns` | list of regex strings | `[]` |

Directory of `<name>.liquid` files.

#### `email_template`

| Field | Type | Default |
|:---|:---|:---|
| `enabled` | bool | `true` |
| `path` | path | `email_templates/` |
| `exclude_patterns` | list of regex strings | `[]` |

Directory holding one `<template>/` subdirectory per email template, each
containing `template.yaml`, `body.html`, and `body.txt`.

#### `custom_attribute`

| Field | Type | Default |
|:---|:---|:---|
| `enabled` | bool | `true` |
| `path` | path | `custom_attributes/registry.yaml` |
| `exclude_patterns` | list of regex strings | `[]` |

A single-file registry — see [registry-mode.md](registry-mode.md).

#### `tag`

| Field | Type | Default |
|:---|:---|:---|
| `enabled` | bool | `false` |
| `path` | path | `tags/registry.yaml` |
| `exclude_patterns` | list of regex strings | `[]` |

A single-file registry of workspace tags. **Opt-in:** omitting
`resources.tag` from your config leaves tag tracking off so existing
projects upgrade without surprise validation failures. Set
`resources.tag.enabled: true` and create `tags/registry.yaml` (e.g. via
`braze-sync export --resource tag`) to turn the feature on. Braze does
**not** expose a public REST API for tags (no list, create, update, or
delete), so the registry is derived from local resource frontmatter
rather than a remote pull, and `apply` cannot create tags — it can only
fail-fast (with an actionable message) when a referenced tag is missing.
See [registry-mode.md](registry-mode.md) for the registry contract.

### `exclude_patterns`

`exclude_patterns` is a list of regular expressions — when a resource's
`name` matches any pattern, it is treated as **managed out of band**
and is skipped by every command:

- `export` does not write it to disk
- `diff` / `apply` ignore it on both sides (so a locally-excluded
  resource will not surface as "missing from Braze" or "orphaned")
- `validate` skips its structural and naming-pattern checks

This is the recommended way to cohabit with Braze reserved attributes
(`_unset`), developer debugging leftovers (`hoge`, `hack`, `test_*`),
or legacy camelCase duplicates that a dashboard operator introduced
and cannot be renamed without disrupting downstream pipelines.

Syntax is the [`regex-lite`](https://docs.rs/regex-lite) dialect (same
subset as `naming.*_name_pattern`). Bad regexes hard-error at config
load time — not at first use — so a typo in a pattern does not
silently become "matches nothing".

> **Anchoring:** patterns are substring-matched, so `hoge` will also
> match `foo_hoge_bar`. Anchor with `^…$` when you want exact-name
> equality (see the example below).

If `--name <n>` is passed on the command line and `<n>` matches an
exclude pattern, the CLI prints a warning and skips the kind for that
invocation. Excludes always win over `--name`.

```yaml
custom_attribute:
  enabled: true
  path: custom_attributes/registry.yaml
  exclude_patterns:
    - "^_"              # Braze reserved attributes like _unset
    - "^(hoge|hack)$"   # developer leftovers
    - "^test_"          # anything prefixed test_
```

### `naming` (optional)

Optional name validators enforced by `braze-sync validate`. Each entry
is a regex evaluated by the [`regex-lite`](https://docs.rs/regex-lite)
crate — a subset of the full `regex` crate with no Unicode classes
(`\p{…}`), limited `\d`/`\w` behavior, and no look-around. Consult the
`regex-lite` syntax reference when writing patterns. Omitted patterns
mean "no check".

| Field | Applies to |
|:---|:---|
| `catalog_name_pattern` | Catalog names |
| `content_block_name_pattern` | Content block names |
| `custom_attribute_name_pattern` | Custom attribute names |
| `tag_name_pattern` | Tag names (workspace tags in `tags/registry.yaml`) |

Naming checks exit code `3` on mismatch, the same as any other
`validate` failure.

## Strictness

The config file itself uses `#[serde(deny_unknown_fields)]` at every
level, so typos and stray keys fail fast with a pointer to the offending
line. Resource files (`schema.yaml`, `template.yaml`, the registry, etc.)
are intentionally **permissive** — unknown fields there are ignored,
preserving forward-compatibility across v1.x.