# braze-sync
GitOps CLI for managing Braze configuration as code.
`braze-sync` lets you keep Braze workspace state in a Git repository and
synchronize it to Braze with the same workflow you'd use for
`terraform plan` / `kubectl diff` — including dry-run previews, drift
detection in CI, and an `--allow-destructive` gate that has to be
crossed explicitly before anything is dropped.
## Status: v0.1.0 (Catalog Schema)
v0.1.0 ships **Catalog Schema** end-to-end:
| `braze-sync export` | Pulls current Braze state into local files |
| `braze-sync diff` | Shows drift between local files and Braze |
| `braze-sync apply` | Applies local intent to Braze (dry-run by default) |
| `braze-sync validate` | Local-only structural and naming checks (no API call) |
Four other resource kinds (Content Block, Email Template, Catalog
Items, Custom Attribute) are visible in `--resource` and emit a
"not yet implemented (Phase B)" warning. They fill in across
v0.2.0 → v0.5.0.
## Install
**Pre-built binaries** (recommended):
Download from [GitHub Releases](https://github.com/uny/braze-sync/releases/latest)
for Linux (x86_64, aarch64), macOS (x86_64, Apple Silicon), and Windows (x86_64).
**Homebrew** (macOS / Linux):
```bash
brew install uny/tap/braze-sync
```
**cargo install** (requires Rust toolchain):
```bash
cargo install braze-sync
```
**Build from source:**
```bash
cargo install --path .
```
## Quick start
1. Set your Braze API key in an environment variable:
```bash
export BRAZE_DEV_API_KEY="your-key-here"
```
2. Create `braze-sync.config.yaml`:
```yaml
version: 1
default_environment: dev
environments:
dev:
api_endpoint: https://rest.fra-02.braze.eu
api_key_env: BRAZE_DEV_API_KEY
```
3. Pull the current state from Braze:
```bash
braze-sync export
```
This writes `catalogs/<name>/schema.yaml` for every Catalog Schema in
your workspace.
4. Edit a schema (e.g. add a field) and check the drift:
```bash
braze-sync diff
```
5. Apply the change — dry-run first, then for real:
```bash
braze-sync apply braze-sync apply --confirm ```
6. In CI, fail builds on drift or local validation issues:
```bash
braze-sync validate braze-sync diff --fail-on-drift ```
`validate` is local-only and **does not need an API key**, so it
runs cleanly on fork PRs that don't have access to repository
secrets.
## Safety by default
`braze-sync apply` is **dry-run by default**. You must pass `--confirm`
to write to Braze. Destructive operations (field deletes) require an
additional `--allow-destructive` flag — `apply` exits with code **6**
if you try to drop a field without it.
```bash
braze-sync apply --confirm # add fields ok, drop fields → exit 6
braze-sync apply --confirm --allow-destructive # field drops permitted
```
API keys never live in the config file. The config only references the
*name* of the environment variable (`api_key_env`), and the key is
held in `secrecy::SecretString` from the moment it leaves the OS so
that `tracing` / `Debug` / panic messages cannot leak it.
## v0.1.0 limitations
These will be lifted across the v0.x → v1.0 milestones:
- **Catalog Schema only.** The other four resource kinds land in
v0.2 → v0.5. They appear in `--resource` so the CLI surface stays
stable, but selecting one in v0.1.0 just emits a "not yet
implemented (Phase B)" warning.
- **No catalog create / delete.** v0.1.0 manages fields on existing
catalogs. To create a brand-new catalog, create it in the Braze
dashboard first, then run `braze-sync export`.
- **No field type changes.** Changing a field's type from `string` to
`number` (or similar) is not auto-applied because the operation is
data-losing on the field. Drop the field manually in Braze, then
run `braze-sync apply` to re-add it with the new type.
- **`/catalogs` pagination.** v0.1.0 sends a single GET to `/catalogs`
and returns the first page. Workspaces with very many catalogs
(>50) may see truncated results until pagination support lands in
Phase C scale validation.
- **`--no-color` only affects tracing output.** v0.1.0 does not emit
ANSI colors in table or diff output, so the flag currently only
suppresses ANSI escapes from the tracing subscriber on stderr.
## Exit codes
These are **frozen at v1.0**: scripts and CI configs can rely on them
across all v1.x releases.
| `0` | Success |
| `1` | General error |
| `2` | Drift detected (`diff --fail-on-drift`) |
| `3` | Config / argument error (or `validate` issues) |
| `4` | Authentication failed (invalid API key) |
| `5` | Rate limit retries exhausted |
| `6` | Destructive change blocked (pass `--allow-destructive`) |
## Output formats
The global `--format` flag picks between human-readable and
machine-readable output for `diff` and `apply`:
```bash
braze-sync diff --format table # default — emoji + indented text
braze-sync diff --format json # frozen v1 schema with `version: 1`
```
The JSON shape is **frozen at v1.0** with an explicit `version: 1`
field on the root. Future schema bumps will increment `version`, so
CI consumers can branch on it.
## License
[MIT](LICENSE)