---
id: w2/m7/t002
title: Core `keyvalue` feature package — CRUD + connection-info over KeyValue CRs + `red-` ids
worker: worker2
status: done
estimate: 60m
depends_on: [w2/m7/t001]
---

## Objective

Build `lego/backend/internal/keyvalue/` mirroring `internal/postgres/` (`service.go` + `tiers.go`): a core service that lists/gets/creates/deletes `KeyValue` CRs (`lego/types/v1alpha1`) in the watched namespace and derives connection info from the credentials Secret — internal `redis://` URI always, external `rediss://` only when `spec.public` and `status.externalHost` are set, plus the `cliCommand`.

**Id decision (revised during implementation):** use the KeyValue **name as the id**, exactly like its managed-Postgres sibling — NOT a minted `red-` id. `docs/ADR020-identifiers.md` § Known deviations already records managed databases as name-keyed CRs with no separate opaque key; KeyValue has identical properties, so extending that deviation keeps the two datastore surfaces uniform. Minting a `red-` kind while Postgres uses name-as-id would make sibling surfaces inconsistent — a worse outcome than either uniform choice. The `internal/id` registry is left unchanged (no `red-` kind), and the deviations note in `docs/ADR020-identifiers.md` is extended to name key-value stores alongside databases (done in t007's ledger/doc pass).

## Context

- Mechanism contract (w1/m14, live in prod): CR spec `plan`/`version`/`storageGB`/`public`; status `phase`/`host`/`port`/`secretName`/`externalHost`; Secret keys `host`/`port`/`username`/`password`/`uri`. Plans come from `lego/types/tiers` (`Valkey` catalog: `free`/`starter`/`standard`).
- Follow `internal/postgres/service.go` for the Core-method + k8s-client shape and `tiers.go` for the plan mapping; docs/ADR006-bex-api.md one-core/thin-adapters rule.
- `id.New(kind)` only — never hand-concatenate (docs/ADR020-identifiers.md); adding the Kind updates the package registry, and its guard test enforces it.
- Render object shape per the t001 capture (id/name/plan/status enum/…); fields the CR lacks (`maxmemoryPolicy`, `persistenceMode`, `ipAllowList`) are omitted or fixed-default for now — record the divergence for t007.

## Steps

1. `internal/keyvalue/service.go`: List/Get/Create/Delete + ConnectionInfo (Secret read; never surface the password anywhere else). Name-as-id lookups, like postgres.
2. `internal/keyvalue/tiers.go`: plan id ↔ tiers.Valkey mapping, Render-shaped plan validation errors.
3. Unit tests against a fake client (deferred bulk to t009; smoke-level here).

## Files

- `lego/backend/internal/keyvalue/{service.go,tiers.go}` (new)

## Acceptance criteria

- [ ] Core verbs round-trip against a fake client; Create makes a name-keyed KeyValue CR (id == name, postgres sibling parity).
- [ ] ConnectionInfo returns the 3-field contract (internal `redis://`, external `rediss://` when public, `cliCommand`); password only inside the strings.
- [ ] `go build ./... && make lint-backend` green.

## Out of scope

- REST/GraphQL/MCP adapters (t003–t006); suspend/resume (t004).
