Developers tab (workspace settings)
The Developers section of workspace settings (/w/:workspace/settings/developers)
is the workspace's API integration surface, organized in five tabs —
Overview · API keys · Webhooks · Events · Curl examples — covering
API keys, webhooks (subscriptions + live delivery log), the event
catalog, and copy-paste examples. It is reachable from the settings nav
and gated by role.
Roles & access
| Role | Tab in nav | Direct URL (…/settings/developers) |
API (…/api-keys) |
|---|---|---|---|
| OWNER | visible | content | allowed |
| ADMIN | visible | content | allowed |
| MEMBER | hidden | "Owner or Admin required" notice | 403 FORBIDDEN |
| GUEST | hidden | "Owner or Admin required" notice | 403 FORBIDDEN |
| API key (any scope) | n/a | n/a | 403 requires_user_credentials |
The server is authoritative (AGENTS rule 19): hiding the tab and rendering the
notice are UX only. An API key — even an ADMIN-scope key — can never manage API
keys; the key-management endpoints are requireUser-gated.
API keys
Create dialog (ApiKeyCreateDialog)
Fields (validated client-side, re-validated server-side — the server is authoritative):
| Field | Rule |
|---|---|
| Name | required, ≤ 100 chars, unique among ACTIVE keys in the workspace (duplicate → 409 CONFLICT, details[0].field === "name" — surfaced as the field error) |
| Scopes | multi-checkbox, ≥ 1. Implication: ADMIN → WRITE → READ (each checkbox states its grant). The request stores the minimal selected set (minimalScopes). |
| Expiry | optional datetime-local, window now + 1 h … now + 1 y. |
| Rate limit | optional int, 1–10 000 req/min, placeholder 600 (default) (the key default; docs/api/authentication.md §11). |
One-time reveal (ApiKeyRevealDialog)
- The raw key (
fk_live_…) appears in exactly one DOM location: this dialog (data-testid="apikey-raw"). It is never persisted (no sessionStorage/localStorage), never in the list, never in logs, never in audit. - Copy button (clipboard API +
execCommandfallback) +aria-liveannouncement; an explicit warning — "This key will not be shown again. Store it securely." - Dismissal requires the "I have stored it" confirmation; the raw key is then dropped from UI state permanently (reopening settings never shows it again — the list only carries the prefix).
List (ApiKeyTable)
Columns: Key (name + masked prefix fk_live_ab12…••• + copy-prefix
action), Scopes (badges, aria-describedby grants, optional N/min),
Last used (relative; "never" — driven by the guard's debounced
lastUsedAt update, ≤ 1 write/min), Expiry (never / date — red when
≤ 7 days, grey + "expired" when passed), Status (Active / Revoked /
Expired).
Row expansion: a curl example with the prefix masked and an
Authorization: Bearer fk_live_ab12…(your key) placeholder (never a raw key).
Revoke
Inline confirm — "Revoke this key? Requests will fail immediately with
401." → DELETE …/api-keys/:id. The row flips to Revoked (the mutation
invalidates the list). The raw key is dead immediately (the auth guard
checks revokedAt on every request — no cache). Revoke is idempotent and
audits api_key.revoked once (the transition, not each DELETE).
Audit
api_key.created (metadata: name + scopes — never the raw key) and
api_key.revoked on every transition. Readable via
GET /api/v1/workspaces/:id/audit?action=api_key.created (OWNER/ADMIN).
Webhooks tab (WebhooksTab)
List, create, rotate, pause/resume, and delete webhook subscriptions.
- List —
GET …/webhooks→{ data: [ … ] }(the webhook shape; it never carries the secret, onlysecretPrefix), newest first. - Create — dialog with
url(required, validated), event chips (sourced from the shared catalog + the*wildcard, never a hand-typed list), an optional own secret field, andactive. The response is{ …shape, secret, warning }— the rawwhsec_…signing secret is revealed once in the secret dialog (same contract as the API-key reveal: not persisted, "I have stored it" gate, dropped from UI state after dismissal).data-testid="webhook-secret-raw". - Rotate —
PATCH …/webhooks/:id { rotateSecret: true }; the new secret is revealed once, and the UI warns that in-flight/old-signature deliveries will stop verifying until the receiver is updated. - Pause / resume —
PATCH …/webhooks/:id { active }; Delete —DELETE …/webhooks/:id(soft delete; the row + log are kept). - Deliveries — a per-webhook "Deliveries" button opens the delivery inspector below.
Delivery inspector (DeliveryInspector)
The live delivery log for one webhook, refreshed on a 10 s poll so retries are visible as they happen:
- Table —
GET …/webhooks/:id/deliveries: status (PENDING/DELIVERED/FAILED/REJECTED/DEAD_LETTER), event,attempts(the counter climbs in place — one row per logical delivery), status code / error,nextRetryAt, latency. - Drawer —
GET …/deliveries/:deliveryId: the full envelopepayloadas JSON, plus the exactX-Factory-Signaturehint (thet=…,v1=…header and thev1 = HMAC-SHA256(secret, "<t>.<rawBody>")formula) so the receiver can verify the sample. - Actions — Test (
POST …/test, synchronous, returns{ deliveryId, status, statusCode, attempts, latencyMs }), per-row Retry (POST …/deliveries/:deliveryId/retry; 409 if already delivered), and Resubmit dead (POST …/resubmit { all_dead: true }).
Delivery semantics — signing scheme, 10 s timeout, the
min(30s·2^(n−1), 15min) ±20 % backoff, the 8-attempt → DEAD_LETTER +
admin-notification path — live in docs/api/webhooks.md §4–§6
(TASK-0130).
Events tab (EventExplorer)
Lists every webhook event with its description, rendered payload schema, and
a copyable example. Driven entirely by WEBHOOK_EVENT_CATALOG in
@factory/shared (imported via the browser-safe ./events/catalog subpath —
the shared barrel pulls node:crypto and must not reach the web bundle),
the same single source that drives dispatcher matching, API subscription
validation, and the completeness test — never a hand-kept list.
Curl tab (CurlExamples)
Per-resource snippets (pages, databases, records, comments, files, webhooks)
curated from docs/api/*.md, using the real runtime base URL and a
<YOUR_API_KEY> placeholder — never a real credential. The Overview
tab leads with the base URL, a curl quickstart (API-key auth), an OAuth2
token example, and links to the OpenAPI UI (/api/v1/docs).
Incoming webhooks (trigger URL)
The reverse direction (external system → automation) is configured on the
Automations surface: an automation with a WEBHOOK trigger gets a
POST /webhooks/incoming/:webhookId URL and a one-time signing secret
(POST …/automations/:id/webhook/regenerate rotates it). Delivery/signature
semantics: docs/api/webhooks.md §8.
Files
| File | Role |
|---|---|
apps/web/src/app/(app)/w/[workspace]/settings/developers/page.tsx |
the hub shell + tab switch (Overview / keys / webhooks / events / curl) |
apps/web/src/components/settings/api-keys/ApiKeyCreateDialog.tsx |
key create form (scope implication, 409 field error) |
apps/web/src/components/settings/api-keys/ApiKeyRevealDialog.tsx |
raw-key-once reveal (warning, copy, confirm) |
apps/web/src/components/settings/api-keys/ApiKeyTable.tsx |
key list (status, expiry windows, revoke confirm, curl) |
apps/web/src/components/settings/api-keys/ScopesBadge.tsx |
scope chips (a11y-described) |
apps/web/src/lib/api-keys.ts |
pure logic: minimalScopes, SCOPE_IMPLIES, curlExample, expiryState, relativeTime (unit-tested) |
apps/web/src/components/settings/webhooks/WebhooksTab.tsx |
webhooks tab (list / create+secret-once / rotate / pause / delete) |
apps/web/src/components/settings/webhooks/DeliveryInspector.tsx |
delivery log + drawer + test / retry / resubmit (10 s poll) |
apps/web/src/components/settings/webhooks/EventExplorer.tsx |
event explorer (catalog-driven schema + examples) |
apps/web/src/components/settings/webhooks/CurlExamples.tsx |
per-resource curl snippets (runtime base URL, key placeholder) |
apps/web/src/components/settings/webhooks/OverviewTab.tsx |
base URL + quickstart + OAuth example + docs links |
apps/web/src/components/settings/settings-shell.tsx |
nav entry (Owner/Admin only) |