# ADR-047: Recce Client Visibility Is Filtered Per-Brand, Not Per-Counter

## Status

Accepted, 2026-06-22.

## Status History

```yaml
status_history:
  - date: 2026-06-22
    status: Proposed
    changed_by: hkl
    reason: Formalising per-brand visibility filter in recces_for_client_user RPC
    changed_via: adr-kit (360lm)
  - date: 2026-06-22
    status: Accepted
    changed_by: hkl
    reason: Filter live; multi-brand counters verified to not leak cross-brand Recce data
    changed_via: adr-kit (360lm)
```

## Context

Recce submissions record observations for a specific brand at a specific counter. A counter (retail outlet) may carry multiple brands. A client user (e.g. a brand manager) logs into the Recce client portal and should see Recce data only for their brand — even if the counter in question also stocks competitor brands managed by other client users. The simplest filter would be "show all Recces for counters the client manages" — but this would expose other brands' Recce data at those same counters to any brand manager who has visibility of that counter.

## Decision

`recces_for_client_user(client_user_id)` filters by matching the **Recce submission's brand** against the **client user's `brand_codes[]` array**, not by counter membership.

SQL: `WHERE r.brand = ANY(SELECT brand_codes FROM client_users WHERE id = client_user_id)`

A Recce for "Brand A at Counter X" is visible only to Client Users who have `'BRAND_A'` in their `brand_codes[]`. A Client User with visibility of Counter X but only `brand_codes = ['BRAND_B']` does NOT see the Brand A Recce at the same counter.

**Decision Maker:** hkl

## Alternatives Considered

- **Filter by counter membership (show all Recces at counters the client manages).** Rejected: a multi-brand counter would expose Brand B's Recce data to Brand A's client manager; this is a cross-brand data leak — explicitly the wrong behavior for a competitive market.
- **Filter by counter AND brand (counter must be in client's list AND brand must match).** Rejected: same result as per-brand filter but with an unnecessary extra join; if a client user has counter-level visibility, their brand filter is sufficient; counter membership is implicit in the brand relationship.
- **Separate client portal per brand (no shared portal).** Rejected: operational overhead of maintaining N client portals; the shared portal with per-brand filtering achieves the same isolation with one codebase.

## Consequences

**Positive:**
- Multi-brand counters cannot leak cross-brand competitive data.
- Adding a new brand to a counter does not automatically expose that brand's data to existing client users of that counter.
- `brand_codes[]` is the single access control dimension — easy to audit and modify.

**Negative / Trade-offs:**
- A client user who manages "Brand A" at counter X and counter Y will not see Brand B data at those counters even if Brand B is in the same company — this is intentional but may require explanation to clients who expect to see all their counters.
- `brand_codes[]` must be maintained per client user — a client user added to a brand group must have `brand_codes` updated or they see nothing.

**Risks and mitigations:**
- Misconfigured `brand_codes[]` (empty array) leaves client user seeing no data: admin portal shows brand code assignment per client user; empty brand_codes is visible in admin dashboard.

## Related Decisions

- ADR-018 (client auth) — client users are external; this filter is their primary access control mechanism.
- ADR-052 (client auth tables RPC-only) — the client_user lookup in this filter goes through a SECURITY DEFINER RPC.

## References

- `memory/dbt_recce.md` — "recces_for_client_user: per-Recce brand filter NOT per-counter, prevents multi-brand counter leaking"
- `memory/dbt_client.md` — client_users.brand_codes[] column
- `recce/index.html` — recces_for_client_user RPC call
