# Tenant API Keys UI (`/settings/api-keys`)

**Date:** 2026-05-31  
**Status:** Draft  
**Spec:** 60  
**Tier:** Business+  
**Depends on:** `white-label-api`, `settings-module`, `foundation-auth-rbac`  
**Referenced by:** `white-label-api`, `settings-module`

---

## Overview

UI for managing tenant API keys — creation, listing, and revocation. Spec 27 (`white-label-api`) owns the `tenant_api_keys` data model and the CRUD API endpoints. This spec owns the settings page UI.

Route: `/settings/api-keys`  
Permission: `settings:write`. OWNER only — API keys grant full-scope access on the owner's behalf.  
Tier gate: Business+ (spec 27). Freelancer sees an upgrade prompt (spec 33 upsell modal).

---

## Page: `/settings/api-keys`

```
┌────────────────────────────────────────────────────────────┐
│  Settings / API Keys                [+ New API Key]        │
│                                                            │
│  API keys let external integrations access Zync on        │
│  your behalf. Keys are shown once — store them securely.  │
│                                                            │
│  ┌──────────────────────────────────────────────────────┐  │
│  │  Name          Prefix    Scopes        Created   Act │  │
│  │  ──────────────────────────────────────────────────  │  │
│  │  Zapier sync   zyk_liv…  invoices:r…   2026-05-01 ●  │  │
│  │  n8n export    zyk_liv…  time:read     2026-04-12 ●  │  │
│  │  Old key       zyk_old…  (all)         2026-01-01 ✕  │  │
│  └──────────────────────────────────────────────────────┘  │
│                                                            │
│  "●" = Active   "✕" = Revoked (shown last 30 days)        │
└────────────────────────────────────────────────────────────┘
```

Columns: Name, Prefix (first 8 chars of key, e.g. `zyk_liv…`), Scopes (truncated list), Created date, Last used date, Status, Revoke button.

Revoked keys shown for 30 days (greyed, no revoke button), then hidden.

---

## Create API Key Modal

Triggered by "+ New API Key" button.

```
┌──────────────────────────────────────────────────────────┐
│  New API Key                                    [×]       │
│                                                          │
│  Name *                                                  │
│  [Zapier Integration_________________________]           │
│                                                          │
│  Scopes                                                  │
│  ☑ invoices:read    ☐ invoices:write                    │
│  ☑ time:read        ☐ time:write                        │
│  ☑ expenses:read    ☐ expenses:write                    │
│  ☐ customers:read   ☐ customers:write                   │
│  ☐ projects:read    ☐ projects:write                    │
│  ☐ marketing:read   ☐ marketing:write                   │
│                                                          │
│  Select all read  |  Select none                         │
│                                                          │
│  [Cancel]                [Generate Key]                  │
└──────────────────────────────────────────────────────────┘
```

Name: required, 1–64 chars.  
Scopes: at least one required.  
"Select all read" shortcut: checks all `:read` scopes (common case for read-only integrations).

---

## Key Reveal Modal (shown once after creation)

```
┌──────────────────────────────────────────────────────────┐
│  Your new API key                               [×]       │
│                                                          │
│  ┌──────────────────────────────────────────────────┐   │
│  │  zyk_live_a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6   │   │
│  └──────────────────────────────────────────────────┘   │
│                             [Copy to clipboard]          │
│                                                          │
│  ⚠ This key will not be shown again.                    │
│  Store it somewhere safe (e.g. your password manager    │
│  or secret vault).                                       │
│                                                          │
│  [I've saved my key — Close]                             │
└──────────────────────────────────────────────────────────┘
```

"[×]" and "Close" only enabled after "Copy to clipboard" has been clicked OR user ticks "I've saved my key" checkbox. Prevents accidental dismissal.

Modal cannot be reopened. If key is lost, revoke and create new one.

---

## Revoke Confirmation

Clicking "Revoke" on a key row:

```
┌────────────────────────────────────────────────┐
│  Revoke API key?                               │
│                                               │
│  "Zapier Integration" (zyk_liv…)              │
│                                               │
│  Any integrations using this key will stop   │
│  working immediately and cannot be undone.   │
│                                               │
│  [Cancel]        [Revoke Key]                 │
└────────────────────────────────────────────────┘
```

On confirm: `DELETE /api/api-keys/:id` (spec 27). Key status → revoked; row greyed in list.

---

## Architecture Decisions

| Decision | Choice | Reason |
|----------|--------|--------|
| Key shown only once | Not stored in DB | Spec 27: only SHA-256 hash stored; plaintext never retrievable after creation — matches industry standard (GitHub PAT, Stripe API keys) |
| OWNER-only permission | Not `settings:write` open to ADMIN | API keys carry full scoped access; ADMIN granting themselves a key bypasses RBAC role boundaries |
| Revoked keys shown 30 days | Not hidden immediately | Audit trail; operator can see "old key" and confirm rotation before integrations break |
| "Select all read" shortcut | Not "select all" | Read-only keys are the 90% case for external integrations; write scope requires deliberate opt-in |
