# Composition Profiles

Authoritative source: [`docs/adr/013-composition-profiles-as-the-only-extension-point.md`](../adr/013-composition-profiles-as-the-only-extension-point.md).

## What a profile is

A composition profile is a frozen, schema-validated TypeScript object loaded
once at boot. It declares **branding, feature flags, and integrations** —
nothing else. The product core reads the profile via `getProfile()`; it never
branches on a profile name.

## Layout

```
config/profiles/
├── profile.ts        ← Zod schemas + types (single source of truth)
├── default.ts        ← The portable, customer-free baseline
├── bmw.ts            ← BMW-specific values (branding, flags). NO real BMW infra in Phase 1.
├── _template.ts      ← Starter for new profiles. Boots into a hard error so it can't ship by accident.
└── index.ts          ← Loader: reads APP_PROFILE, validates, freezes
```

## Adding or extending a profile

1. **New tenant** → copy `_template.ts` to `<tenant>.ts`, edit values, register
   in `index.ts`.
2. **New flag/integration** → add it to the schema in `profile.ts` first
   (Zod), then set values in each profile.
3. **Run `npm run check:profiles`** — every profile must validate cleanly.
   The portability CI matrix will fail if any profile drifts.

## Selecting at runtime

```bash
APP_PROFILE=bmw npm run dev      # BMW branding + flags
APP_PROFILE=default npm run dev  # vanilla
# Default if unset: 'default'
```

## What does NOT belong in a profile

- Business logic (lives in `lib/<domain>/`).
- Customer-specific data shapes (lives in `lib/customers/<name>/`).
- Secrets (use env vars; profile holds the *shape*, env holds the *value*).
- Anything tested in only one profile — if it doesn't run portably, it
  belongs in an adapter, not the profile.

## Phase 1 boundary

`bmw.ts` currently contains branding only. All `integrations.*` are set to
`null` / defaults with `TODO(phase-9)` markers — there is no real BMW
infrastructure wired up yet. See risk **R-2026-04-19-07** in the risk
register.
