---
id: w1/m9/t002
title: Key→tenant binding — OAuth2 client → tenant_oauth_clients + OpenFGA workspace membership
worker: worker1
status: done
estimate: 30m
depends_on: [w1/m9/t001]
---

## Objective

An API key minted by a tenant member belongs to that tenant: `POST /v1/api-keys` records the new Hydra client in `tenant_oauth_clients` and writes the OpenFGA tuple making `user:<client_id>` a member of `workspace:tea-<tenant-id>` (replacing today's `workspace:default` membership).

## Context

- Key minting: `lego/backend/internal/apikeys/service.go` (Hydra admin, clients stamped `bex.co/api-key`).
- Mapping table `tenant_oauth_clients` exists in the m2 schema (mapping keys only — auth stays bought).
- OpenFGA writer exists from m2/t006 (`workspace:tea-<id>` mint path in the store API); reuse, don't duplicate.

## Steps

1. Resolve the caller's tenant (t001's context value) in the api-keys service; refuse minting with no tenant (session-less machine caller) with a clear error.
2. On successful Hydra client creation: insert the `tenant_oauth_clients` row + write the FGA membership tuple; on FGA/store failure, delete the Hydra client (no orphaned credentials).
3. Revoke path removes the mapping row and the tuple.

## Files

- `lego/backend/internal/apikeys/service.go`
- `lego/backend/internal/store/store.go`
- `lego/backend/internal/authz/` (tuple write helper if not already exposed)

## Acceptance criteria

- [ ] Minting a key as tenant-A member yields a `tenant_oauth_clients` row and a positive FGA `check(user:<client_id>, member, workspace:tea-A)`.
- [ ] Revoking the key removes both; a failed FGA write leaves no live Hydra client.

## Out of scope

- Scoping the verbs to the workspace (t003); key roles beyond member.
