Firm profile: rename your firm from a single field
A firm admin can now change the firm's display name themselves.
Settings → Firm configuration → Firm profile
shows the current name in one input; edit, save, done — the new
name appears across the app (starting with the settings sidebar header)
immediately, no reload. No support ticket, and no forcing the firm to
re-enter an address it never gave us. The save rides the
partial-update PATCH /app/firm contract
that functions Story-1861 landed: a name-only request leaves every other
firm column untouched (RFD-FIRM-002).
What changed
A new FirmProfileRoute renders one
react-hook-form + zod field
(z.string().trim().min(1).max(255)). Save is
disabled until the name actually changes
(trim-aware dirty-check against the form baseline), so a no-op PATCH
is never sent and a whitespace-only edit never enables Save.
The settings layout has no admin guard, so a read-only member
reaches the page. The RSC passes only the display name +
canWrite to the client — never the full
FirmProfile (plan / seats / CSM / address). Because
me.firm_name already IS the display name, no extra
over-fetching gateway read is made at all.
The firm name renders in the sidebar header, which the settings
layout (not the page) builds from
loadAppMe().firm_name. The server action calls
revalidatePath(`/${locale}/settings`, "layout") so the
header reflects the new name without a reload (RFD §5.5 F5).
Success → localized toast.success. Validation
→ localized field error + focus-to-field. 401/403/5xx →
localized fallback toast — the raw backend
error.message is never rendered. The
typed value is retained on failure.
Contract status (verified against merged functions main)
-
Partial PATCH — WIRED. functions
Story-1861 made
FirmProfileUpdateRequest's six address/contact fields optional. The OpenAPI mirror was synced from functionsmainHEAD andpnpm codegenregeneratedsrc/gen; the zod request schema now accepts a name-only payload ({ displayName }). -
Trimmed response (Q4) — ENFORCED. The
PATCH response is now
FirmProfileUpdateResponse(editable fields only; no plan/seat/CSM echo). The firm-profile gateway'supdate()boundary check was switched from the old fullfirmProfileResponseSchematofirmProfileUpdateResponseSchema— the stale assert would otherwise (correctly) failassertContractat the trust boundary now that plan/seats are gone (HR-21). -
First-run honesty. A firm with no saved
address renames fine: the partial PATCH carries only
displayName, so the empty address is never validated (this was the whole reason for the new contract; staging proof pending on deploy).
Where it lives
openapi/app-api.json+src/gen/**— mirror synced from functionsmainHEAD +pnpm codegen:FirmProfileUpdateRequestall-optional, newFirmProfileUpdateResponsetype + zod.src/lib/firm-profile/gateway.ts—update()asserts the trimmedfirmProfileUpdateResponseSchema(Q4 / ADR-099).src/app/[locale]/(workspace)/settings/firm/profile/page.tsx—FirmProfileRoute:requireOkAppMegate, passes name +canWrite+ a bound save action;agentSurface(settings.firm-profile).src/app/[locale]/(workspace)/settings/firm/profile/actions.ts—updateFirmNameAction: trust-boundary zod, gateway write, layout revalidation, localized typed result.src/components/settings/firm-profile/firm-profile-page.tsx(+*.stories.tsx) — the one-field form / read-only text view; a11y label/aria-invalid/aria-describedby/focus.src/components/settings/settings-sidebar.tsx—firm-profilenav item (type + icon +SETTINGS_GROUPS) at the top of thefirmgroup.src/lib/surface-registry/route-registry.ts+page-card/catalog.ts— regenerated registry; navigate-only catalog entry (mirrors sibling settings atlas surfaces).messages/en.json+messages/hi.json— the orphanFirmProfilenamespace (from superseded RFD-FIRM-001) repurposed for this page (no parallel namespace);SettingsNav.firmProfile+AgentSurfaces.settingsFirmProfile; both locales.
Observability
No new instrumentation (pramaan-docs ADR-104 / ADR-065 / ADR-124). The
save traverses src/lib/gateway.ts, which already
mints/forwards traceparent +
x-pramaan-correlation-id — the trace propagates for
free. PATCH /app/firm is an existing instrumented domain
operation; this Story only changes the request shape. No OTel SDK,
spans, or ad-hoc headers added.
Verify
pnpm lint pnpm typecheck pnpm test pnpm build pnpm check:routes
- Storybook
FirmProfilePage→ Editable (Save disabled until the name changes) / Read-only (name as text, no input) / Server field error (localized + focus) / Save failure (localized fallback toast). - Confidentiality the client payload carries only
firmName+canWrite— no plan/seat/CSM/address crosses the boundary. - i18n the repurposed
FirmProfilenamespace + nav + agent-surface keys present in bothenandhi; orphan namespace resolved, no parallel one. - Tokens no color literals; surfaces/text from semantic tokens only.
- Staging live rename (incl. a no-saved-address firm) + read-only member payload check are deploy-time verifications (functions Story-1861 must be live in the target env).
Files changed
55 file(s), +3348 / -217 lines
| Path | Role | + | - |
|---|---|---|---|
| messages/en.json | Config | +14 | -30 |
| messages/hi.json | Config | +14 | -30 |
| openapi/app-api.json | Config | +636 | -37 |
| openapi/permission-map.json | Config | +28 | -0 |
| src/app/[locale]/(workspace)/settings/firm/profile/actions.ts | App route | +58 | -0 |
| src/app/[locale]/(workspace)/settings/firm/profile/page.tsx | App route | +47 | -0 |
| src/components/settings/firm-profile/firm-profile-page.stories.tsx | Component | +68 | -0 |
| src/components/settings/firm-profile/firm-profile-page.tsx | Component | +172 | -0 |
| src/components/settings/settings-sidebar.tsx | Component | +8 | -0 |
| src/gen/client/appCreateCreditTopup.ts | Generated contract | +68 | -0 |
| src/gen/client/appGetChatCredits.ts | Generated contract | +65 | -0 |
| src/gen/client/appGetFirmCredits.ts | Generated contract | +58 | -0 |
| src/gen/client/index.ts | Generated contract | +3 | -0 |
| src/gen/hooks/index.ts | Generated contract | +20 | -0 |
| src/gen/hooks/useAppCreateCreditTopup.ts | Generated contract | +156 | -0 |
| src/gen/hooks/useAppGetChatCredits.ts | Generated contract | +134 | -0 |
| src/gen/hooks/useAppGetChatCreditsSuspense.ts | Generated contract | +137 | -0 |
| src/gen/hooks/useAppGetFirmCredits.ts | Generated contract | +122 | -0 |
| src/gen/hooks/useAppGetFirmCreditsSuspense.ts | Generated contract | +121 | -0 |
| src/gen/index.ts | Generated contract | +108 | -0 |
| src/gen/schemas/chatCreditUsageResponse.json | Generated contract | +19 | -0 |
| src/gen/schemas/creditTopupOrderRequest.json | Generated contract | +18 | -0 |
| src/gen/schemas/creditTopupOrderResponse.json | Generated contract | +8 | -0 |
| src/gen/schemas/firmCreditsResponse.json | Generated contract | +64 | -0 |
| src/gen/schemas/firmProfileUpdateRequest.json | Generated contract | +44 | -41 |
| src/gen/schemas/firmProfileUpdateResponse.json | Generated contract | +49 | -0 |
| src/gen/schemas/lawyerCreditUsageView.json | Generated contract | +15 | -0 |
| src/gen/types/AppCreateCreditTopup.ts | Generated contract | +75 | -0 |
| src/gen/types/AppGetChatCredits.ts | Generated contract | +76 | -0 |
| src/gen/types/AppGetFirmCredits.ts | Generated contract | +68 | -0 |
| src/gen/types/AppUpdateProfile.ts | Generated contract | +3 | -3 |
| src/gen/types/ChatCreditUsageResponse.ts | Generated contract | +21 | -0 |
| src/gen/types/CreditTopupOrderRequest.ts | Generated contract | +25 | -0 |
| src/gen/types/CreditTopupOrderResponse.ts | Generated contract | +16 | -0 |
| src/gen/types/FirmCreditsResponse.ts | Generated contract | +51 | -0 |
| src/gen/types/FirmProfileUpdateRequest.ts | Generated contract | +15 | -32 |
| src/gen/types/FirmProfileUpdateResponse.ts | Generated contract | +45 | -0 |
| src/gen/types/LawyerCreditUsageView.ts | Generated contract | +21 | -0 |
| src/gen/types/index.ts | Generated contract | +44 | -0 |
| src/gen/zod/appCreateCreditTopupSchema.ts | Generated contract | +101 | -0 |
| src/gen/zod/appGetChatCreditsSchema.ts | Generated contract | +98 | -0 |
| src/gen/zod/appGetFirmCreditsSchema.ts | Generated contract | +93 | -0 |
| src/gen/zod/appUpdateProfileSchema.ts | Generated contract | +4 | -4 |
| src/gen/zod/chatCreditUsageResponseSchema.ts | Generated contract | +20 | -0 |
| src/gen/zod/creditTopupOrderRequestSchema.ts | Generated contract | +21 | -0 |
| src/gen/zod/creditTopupOrderResponseSchema.ts | Generated contract | +19 | -0 |
| src/gen/zod/firmCreditsResponseSchema.ts | Generated contract | +35 | -0 |
| src/gen/zod/firmProfileUpdateRequestSchema.ts | Generated contract | +39 | -30 |
| src/gen/zod/firmProfileUpdateResponseSchema.ts | Generated contract | +46 | -0 |
| src/gen/zod/index.ts | Generated contract | +41 | -0 |
| src/gen/zod/lawyerCreditUsageViewSchema.ts | Generated contract | +20 | -0 |
| src/lib/firm-profile/gateway.ts | Library | +7 | -2 |
| src/lib/surface-registry/page-card/catalog.ts | Library | +1 | -0 |
| src/lib/surface-registry/route-registry.ts | Library | +12 | -2 |
| tests/unit/firm-profile-contract.test.ts | Test | +77 | -6 |