---
id: w2/m8/t003
title: Control-plane store — git_connections table + connection verbs (authz-gated)
worker: worker2
status: done
estimate: 40m
depends_on: [w2/m8/t002]
---

## Objective

Persist the GitHub connection in the control-plane Postgres and expose service verbs on the core: record a connection (installation id + account login, keyed by workspace), read it, delete it. Writes are admin-only via OpenFGA; unset `BEX_CP_DB_URI` or missing GitHub env ⇒ 503.

## Context

- Store pattern: `lego/backend/internal/store` owns migrations + queries ([docs/ADR003-control-plane.md](../../../docs/ADR003-control-plane.md)).
- One connection per workspace for now (bex is effectively single-workspace; w6 owns multi-workspace).
- Every exported service verb must start with `Authorize` — `TestAuthzGuardsEveryVerb` reflection sweep enforces it.
- On record, validate the installation is real by minting a token for it (t002 client) before persisting.

## Steps

1. Migration: `git_connections(workspace_id, installation_id, account_login, created_at, …)` with a uniqueness constraint per workspace.
2. Store queries: upsert / get / delete.
3. Service verbs in `internal/github`: `Connect` (record installation after validating via token mint), `GetConnection`, `Disconnect` — OpenFGA-gated (admin scope for writes, member read).
4. Unit tests: verbs 503 when store or GitHub config absent; authz denial paths; record→get→delete round-trip against the store test harness.

## Files

- `lego/backend/internal/store/` (migration + queries)
- `lego/backend/internal/github/service.go` (new)
- tests alongside

## Acceptance criteria

- [ ] `TestAuthzGuardsEveryVerb` passes with the new verbs
- [ ] 503 semantics when `BEX_CP_DB_URI` or GitHub env unset
- [ ] Round-trip covered by tests

## Out of scope

- HTTP/GraphQL/MCP adapters (t004–t005)
