---
id: w1/m16/t001
title: Environment groups — Core verbs + store/OpenBao model (CRUD + link/unlink) + projection
worker: worker1
status: done
estimate: 45m
depends_on: []
---

## Objective

Add environment groups as a first-class Core concept in bex-api: a named, shared set of env vars (and later secret files) that can be linked to many services. A group is a control-plane store object (its name/identity/link set) plus an OpenBao path for its values; the Core exposes create/list/get/update/delete plus link/unlink(serviceId). On any change to a group's values or its link set, re-project every currently linked service's `<name>-env` Secret so linked services pick the group up. Reuse the existing `secrets` feature's OpenBao client and projection path — no new store.

## Context

- Extends the OpenBao-backed `secrets` feature: `lego/backend/internal/secrets` (`service.go` embeds the shared kernel `internal/core/base.go`).
- The env-vars projection to project against: on write, bex-api stores the map in OpenBao (source of truth) at `tenants/<tenant>/services/<service>/env`, materializes the per-app `<name>-env` Secret, points `App.spec.envFromSecret` at it, and rolls pods via `spec.restartedAt` (docs/ADR013-secrets.md "Product usage"; docs/ADR006-bex-api.md "Env vars").
- OpenBao path convention is `tenants/<tenant>/...` (single `default` tenant until w1/m2 grows real tenants) — put group values under a sibling path, e.g. `tenants/<tenant>/env-groups/<group>/env`.
- Projection precedence must be defined: how a group's vars combine with a service's own env vars when both set the same key (decide + document; a service-local value overriding the group is the natural default).
- Fail-closed: with `BEX_OPENBAO_URL` unset the group verbs 503, mirroring the env-vars verbs.
- Authz: reuse the same OpenFGA checker — sensitive-read scope (`can_view_sensitive`) to read values, manage scope (`can_create`) to write/link.

## Steps

1. Model the group object in the control-plane store (identity/name + its linked serviceId set) and its OpenBao values path; decide where the link set persists (store vs OpenBao metadata).
2. Add Core verbs on the secrets feature `Service`: `CreateEnvGroup`/`ListEnvGroups`/`GetEnvGroup`/`UpdateEnvGroup`/`DeleteEnvGroup` and `LinkEnvGroup(groupId, serviceId)`/`UnlinkEnvGroup(groupId, serviceId)`.
3. Implement re-projection: on any values change, link, or unlink, recompute + materialize the `<name>-env` Secret for every affected linked service and roll its pods (reuse the existing projection helper); define var precedence vs service-local env vars.
4. Wire fail-closed (503 when OpenBao unset) and the OpenFGA scope checks onto the new verbs.

## Files

- `lego/backend/internal/secrets/service.go` — new Core verbs.
- `lego/backend/internal/secrets/` — group store/OpenBao model + projection helper (new file(s) alongside the service).
- control-plane store (`BEX_CP_DB_URI` path) — group + link persistence if not held in OpenBao metadata.

## Acceptance criteria

- [ ] `CreateEnvGroup`/`List`/`Get`/`Update`/`Delete` + `Link`/`Unlink` exist as Core verbs and round-trip a group with vars.
- [ ] Linking a group to a service re-projects that service's `<name>-env` Secret to include the group's vars; unlinking removes them.
- [ ] Editing a linked group's vars re-projects every linked service (all pick up the change).
- [ ] Var precedence between group and service-local env vars is deterministic and documented.
- [ ] With `BEX_OPENBAO_URL` unset the group verbs 503; scope checks return 403 for a tuple-less key.

## Out of scope

- REST/GraphQL/MCP adapters and dashboard UI (t002).
- Secret files, in groups or per-service (t003).
- Multi-tenant scoping beyond the single `default` tenant.
