---
id: w2/m7/t003
title: REST adapter — Render `/v1/key-value` endpoints + connection-info
worker: worker2
status: done
estimate: 45m
depends_on: [w2/m7/t002]
---

## Objective

Thin REST adapter over the t002 core, byte-shaped to the t001 capture: `GET/POST /v1/key-value`, `GET/PATCH/DELETE /v1/key-value/{idOrName}`, `GET /v1/key-value/{idOrName}/connection-info` — Render field names, status enums, list envelope, and error shapes (401/403/404/409), wired into the existing router + authz like `internal/postgres/rest.go`.

## Context

- `lego/backend/internal/postgres/rest.go` is the template (route registration, OpenFGA checks, Render list envelope `[{keyValue:…,cursor:…}]`-style per capture).
- Suspend/resume routes land in t004 (they need the mechanism verb).
- PATCH scope per capture — likely plan/name; reject fields the mechanism can't honor with a Render-shaped 400 rather than silently ignoring.

## Steps

1. `internal/keyvalue/rest.go`: route table + handlers delegating to the core; register in `internal/api/server.go`.
2. Match the captured JSON exactly (field order aside): object, list envelope, connection-info keys.
3. Authz: same relation checks the postgres surface uses, scoped to the workspace tenant.

## Files

- `lego/backend/internal/keyvalue/rest.go` (new)
- `lego/backend/internal/api/server.go`

## Acceptance criteria

- [ ] All six non-lifecycle endpoints serve Render-shaped payloads against a live CR (envtest or mock cluster).
- [ ] connection-info is the only place the password appears; 404s use Render's error shape.
- [ ] Unauthorized/unauthenticated requests fail exactly like the postgres surface.

## Out of scope

- suspend/resume (t004); GraphQL/MCP (t005/t006).
