---
id: w6/m1/t003
title: "Core verbs: CreateWorkspace + RenameWorkspace (tenant row + FGA admin tuple; 5-Hobby cap)"
worker: worker6
status: done
estimate: 35m
depends_on: [w6/m1/t002]
---

## Objective

User-initiated workspace creation and rename as Core verbs. CreateWorkspace(name, plan) writes the `tenants` row (`tea-` id), a `tenant_members` row for the caller, and the OpenFGA admin tuple — atomically, reusing w1/m9's mint path (one write path, not two). It refuses the caller's 6th Hobby workspace. RenameWorkspace updates the display name, admin-only.

## Context

- w1/m9 t001's tenant-mint code is the template: same tables, same FGA tuple shape (`workspace:tea-<id>`, model in `deploy/gitops/authz/model.fga`).
- Core is the single business-logic layer (`lego/backend/internal/core/`); REST/GraphQL/MCP are thin adapters (docs/ADR006-bex-api.md).
- Render semantics: creation asks name + plan (RESEARCH-workspaces.md finding 2); 5-Hobby cap is per user (finding 3); rename semantics per t001's captured artifact.
- Authorization: create needs only an authenticated identity; rename requires workspace admin (`core.Authorize`).

## Steps

1. Extract/reuse m9's mint into a shared create path; add name+plan parameters and the Hobby-count check (count caller's `hobby` workspaces via `tenant_members` join).
2. Add `RenameWorkspace` with admin check.
3. Wire both through `core.Authorize` and the audit hook if w4/m10 has landed.

## Files

- `lego/backend/internal/core/` (verbs)
- `lego/backend/internal/store/` (queries: hobby count, rename)
- `lego/backend/internal/authz/` (relation checks, if needed)

## Acceptance criteria

- [ ] CreateWorkspace writes row + membership + FGA tuple atomically (failure leaves no partial state)
- [ ] 6th Hobby workspace for the same identity returns a typed limit error; a `pro` workspace is unaffected by the cap
- [ ] Rename by a non-admin member is refused (403)

## Out of scope

- Delete (t004); GraphQL exposure (t006); payment/billing.
