Skip to content

FRD-031: API Key Manager

FieldValue
OwnerDavid Holmes
StatusDraft
Last Updated2026-05-26
RelatedADR-027 (Default Tech Stack)
Target Releasev2.0.0
TypeWidget
SizeM
PriorityP1 — SaaS Widget

Document Summary

Build an API Key Manager widget that provides list, create, reveal-once, rotate, and revoke UX for API keys. The widget displays scoped permission badges, last-used metadata, and handles loading, empty, and destructive-confirmation states. It builds on the existing secret-reveal-field.tsx and secret-visibility-toggle.tsx primitives.


Introduction

Overview

SaaS applications universally need API key management: listing existing keys, creating new ones with scoped permissions, revealing a key exactly once after creation, rotating compromised keys, and revoking keys with confirmation. The design system provides SecretRevealField and SecretVisibilityToggle for secret display, but no composed widget for the full key lifecycle. Teams repeatedly build this pattern from scratch.

Goals

  • Provide a self-contained ApiKeyManager widget handling the full key lifecycle (list, create, reveal-once, rotate, revoke).
  • Display scoped permission badges per key.
  • Show last-used timestamp and creation date metadata.
  • Handle loading, empty, and error states with appropriate visual treatments.
  • Provide destructive-action confirmation dialogs for rotate and revoke.
  • Ship with comprehensive Storybook stories and unit tests.

Non-Goals

  • Actual API integration (key generation, storage); the widget is headless and calls consumer-provided callbacks.
  • OAuth token management (different UX pattern).
  • Rate limiting or usage analytics display.

Scope

In Scope

ItemDescription
Key list viewDataGrid-based table showing key name, masked prefix, permissions, last used, created date, and actions.
Create key flowModal or slide-out panel with name input, permission scope checkboxes, and a generate action.
Reveal-once displayAfter creation, display the full key using SecretRevealField with auto-remask and copy-to-clipboard.
Rotate keyConfirmation dialog explaining that the old key will be invalidated; returns new key via callback.
Revoke keyDestructive confirmation dialog with key name echo; calls revoke callback.
Permission badgesBadge components showing scoped permissions (e.g., “read”, “write”, “admin”).
Metadata displayLast-used relative timestamp, creation date, optional expiry date.
State handlingLoading skeleton, empty state (“No API keys yet”), error state with retry.
Stories and testsStorybook stories for all states; unit tests for interaction flows.

Out of Scope

ItemReason
Key generation logicBackend concern; widget calls onCreate callback and displays the returned key.
Usage analyticsSeparate widget; API Key Manager focuses on CRUD.
OAuth/SSO token managementDifferent interaction model and security considerations.

Users and Pain Points

UserPain Point
SaaS developersBuild API key management UI from scratch for every project; inconsistent patterns across teams.
DevOps/platform engineersNeed to manage service account keys; existing tools are CLI-only.
End users managing API keysCannot easily see which permissions a key has or when it was last used.
Security teamsNo standard revoke confirmation flow; accidental revocations or missed rotations.

Definitions

TermDefinition
Reveal-onceA key is shown in full exactly once after creation; subsequent views show only a masked prefix.
Key rotationGenerating a new key to replace an existing one; the old key is invalidated immediately.
Scoped permissionsA set of capabilities assigned to a key (e.g., “read:data”, “write:data”, “admin”).
Masked prefixThe first and last few characters of a key shown with the middle masked (e.g., sk_live_abc...xyz).

Current State

  • SecretRevealField (src/components/ui/secret-reveal-field.tsx): Read-only secret display with reveal/copy actions, auto-remask timer. Accepts value, revealed, onRevealedChange, autoRemaskMs, onCopy.
  • SecretVisibilityToggle (src/components/ui/secret-visibility-toggle.tsx): Eye icon toggle for show/hide.
  • DataGrid (src/components/ui/data-grid.tsx): Full-featured table with sort, filter, paginate, row selection.
  • No API key management widget exists.
  • Badge component exists for permission display.
  • StatusCard exists for empty/error states.
  • AlertDialog exists for destructive confirmations.

Proposed Solution

Build src/components/widgets/api-key-manager.tsx as a composed widget:

Key List

Uses DataGrid with columns: Name, Key (masked prefix), Permissions (badge group), Last Used, Created, Actions (rotate/revoke dropdown). The list supports search filtering by key name.

Create Flow

A “Create API Key” button opens a SlideOutPanel or Dialog containing:

  1. Name input (required).
  2. Permission scope checkboxes (consumer provides available scopes).
  3. Optional expiry date picker.
  4. “Generate Key” button calls onCreate(config) callback.

Reveal-Once

After onCreate resolves with the full key string:

  1. The panel transitions to a reveal-once view using SecretRevealField.
  2. A prominent “Copy” button and warning text: “This key will not be shown again.”
  3. “Done” button closes the panel and the key list refreshes.

Rotate

Clicking “Rotate” on a key row opens AlertDialog with destructive variant:

  • Explains the old key will be invalidated immediately.
  • Calls onRotate(keyId) callback on confirm.
  • Shows the new key in reveal-once mode.

Revoke

Clicking “Revoke” opens AlertDialog with destructive variant:

  • Requires typing the key name to confirm (pattern from GitHub/Vercel).
  • Calls onRevoke(keyId) callback on confirm.

State Handling

  • Loading: DataGrid skeleton rows.
  • Empty: StatusCard with “No API keys” message and “Create your first key” CTA.
  • Error: StatusCard with error message and retry button.

Requirements

Requirement Priorities

  • Must Have: Key list, create flow, reveal-once, revoke with confirmation.
  • Should Have: Rotate flow, permission badges, last-used metadata.
  • Could Have: Key expiry date, bulk revoke, key name validation.

Functional Requirements

IDRequirementPriority
FR-01Widget displays a DataGrid of API keys with name, masked prefix, permissions, last used, created date, and actions columns.Must
FR-02”Create API Key” button opens a form with name input and permission scope selection.Must
FR-03After key creation, the full key is displayed once using SecretRevealField with copy-to-clipboard.Must
FR-04A warning is shown that the key will not be displayed again after the reveal-once panel is closed.Must
FR-05”Revoke” action opens a destructive AlertDialog; user must type the key name to confirm.Must
FR-06”Rotate” action opens a destructive AlertDialog explaining the old key will be invalidated.Should
FR-07After rotation, the new key is shown in reveal-once mode.Should
FR-08Permission scopes render as colored Badge components in the key list.Should
FR-09Last-used timestamp displays as relative time (e.g., “2 hours ago”) with absolute tooltip.Should
FR-10Loading state renders DataGrid skeleton rows.Must
FR-11Empty state renders a StatusCard with CTA to create the first key.Must
FR-12Error state renders a StatusCard with error message and retry button.Must
FR-13Widget calls consumer-provided callbacks (onCreate, onRotate, onRevoke) and does not manage API state internally.Must

Non-Functional Requirements

IDRequirementTarget
NFR-01Widget renders key list with 50 keys in < 50ms.Measured via React Profiler.
NFR-02Bundle size< 10 KB gzipped (excluding shared DataGrid and Dialog deps).
NFR-03Reveal-once key is never persisted in component state after panel close.Security requirement; key is cleared from state on unmount.
NFR-04Dark modeFull token-based dark mode support.
NFR-05ResponsiveUsable on tablet (768px+); degrades gracefully on mobile.

API/Interface Requirements

interface ApiKey {
id: string;
name: string;
prefix: string; // masked display, e.g. "sk_live_abc...xyz"
permissions: string[]; // e.g. ["read:data", "write:data"]
lastUsed?: string; // ISO datetime or relative string
createdAt: string; // ISO datetime
expiresAt?: string; // ISO datetime
}
interface ApiKeyManagerProps {
keys: ApiKey[];
availablePermissions: string[];
loading?: boolean;
error?: string;
onRetry?: () => void;
onCreate: (config: { name: string; permissions: string[]; expiresAt?: string }) => Promise<{ key: string }>;
onRotate?: (keyId: string) => Promise<{ key: string }>;
onRevoke: (keyId: string) => Promise<void>;
emptyTitle?: string;
emptyDescription?: string;
className?: string;
}

Accessibility Requirements

IDRequirement
A11Y-01Create and revoke dialogs trap focus and return focus to trigger on close.
A11Y-02Revoke confirmation input has aria-label describing what to type.
A11Y-03Permission badges use aria-label describing the permission scope.
A11Y-04Reveal-once SecretRevealField announces “Key copied to clipboard” via aria-live on copy.
A11Y-05DataGrid keyboard navigation works for all action buttons in the actions column.
A11Y-06All components pass axe-core automated checks with zero violations.

Content and Documentation Requirements

IDRequirement
DOC-01Storybook docs page with usage guidelines, prop table, and interactive examples.
DOC-02Recipes showing integration with TanStack Query for key fetching and mutation.
DOC-03Security guidance: never log full keys, clear from state after reveal, use HTTPS-only.

Dependencies

DependencyTypeRisk
src/components/ui/secret-reveal-field.tsxInternalLow — core reveal-once primitive.
src/components/ui/secret-visibility-toggle.tsxInternalLow — visibility toggle icon.
src/components/ui/data-grid.tsxInternalLow — table display.
src/components/ui/badge.tsxInternalLow — permission badges.
src/components/ui/status-card.tsxInternalLow — empty/error states.
src/components/ui/alert-dialog.tsxInternalLow — destructive confirmations.

Risks and Tradeoffs

RiskLikelihoodImpactMitigation
Reveal-once key accidentally persists in React state or DevToolsLowCriticalClear key from state on panel close; use a ref instead of state for the raw key.
Revoke name-match confirmation is too strict (case sensitivity, whitespace)MediumLowTrim and lowercase comparison; show the exact expected string.
Widget API is too opinionated for diverse backend shapesMediumMediumKeep ApiKey interface minimal; consumers transform their data to fit.

Open Questions

#QuestionOwnerStatus
OQ-01Should the create flow use a Dialog or SlideOutPanel?David HolmesOpen
OQ-02Should the widget support bulk revoke (multi-select + revoke all)?David HolmesOpen
OQ-03Should key expiry be displayed as a badge with color coding (green/yellow/red)?David HolmesOpen

Acceptance Criteria

#Criterion
AC-01Widget renders a list of API keys with name, masked prefix, permissions, last used, and actions.
AC-02Create flow collects name and permissions, calls onCreate, and displays the full key exactly once.
AC-03Reveal-once view includes copy button and warning text; key is cleared from state on close.
AC-04Revoke flow requires typing the key name and calls onRevoke on confirmation.
AC-05Loading, empty, and error states render appropriate visual treatments.
AC-06All components pass axe-core checks with zero violations.
AC-07Storybook stories exist for all states and interaction flows.
AC-08Unit tests cover create, reveal-once, revoke confirmation, and state transitions.
AC-09pnpm typecheck and pnpm vitest run --project unit pass with zero errors.

LLM Handoff Instructions

When implementing this FRD:

  1. Create src/components/widgets/api-key-manager.tsx. Follow the pattern from people-table.tsx — define the data interface, column factory function, and composed widget.
  2. DataGrid columns: Name (text), Key (masked prefix with monospace font), Permissions (badge group), Last Used (relative time), Created (date), Actions (dropdown with Rotate/Revoke).
  3. Create flow: Use SlideOutPanel with a form containing TextField for name and a checkbox group for permissions. On submit, call onCreate and transition to reveal-once view.
  4. Reveal-once: Render SecretRevealField with autoRemaskMs={undefined} (no auto-remask — the key stays visible until the user closes the panel). Add a copy button and warning text. On panel close, clear the key from a ref.
  5. Revoke flow: Use AlertDialog with variant="destructive". Add a TextField where the user must type the key name. Disable the confirm button until the input matches.
  6. Stories in src/components/widgets/api-key-manager.stories.tsx. Include: Default, Loading, Empty, Error, CreateFlow, RevealOnce, RevokeConfirmation.
  7. Tests in src/components/widgets/api-key-manager.test.tsx. Test create callback, reveal-once display, revoke name matching, state transitions.

Key files to reference:

  • src/components/ui/secret-reveal-field.tsx — reveal/copy pattern.
  • src/components/widgets/people-table.tsx — widget composition pattern.
  • src/components/ui/data-grid.tsx — table API.
  • src/components/ui/badge.tsx — permission display.
  • src/components/ui/status-card.tsx — empty/error states.

Decision Log

DateDecisionRationale
2026-05-26Widget is headless (callbacks, not API calls).Consumers have diverse backends; the widget should be backend-agnostic.
2026-05-26Reveal-once uses a ref, not state, for the raw key.Prevents the key from appearing in React DevTools or persisting across renders.
2026-05-26Revoke requires typing the key name, not just clicking “Confirm”.Follows the GitHub/Vercel destructive-action confirmation pattern for safety.

Document History

VersionDateAuthorChanges
0.12026-05-26David HolmesInitial draft.