URL contract
Scope: apps/web and every satellite that renders a settings dialog (the shared satellite user menu, plus the dedicated Calendar and Tasks hosts).- A bare
settingsTabis enough to open the dialog. Before 2026-10-23, the dialog opened only whensettingsDialog=openwas also present, so links such as/personal?settingsTab=securitysilently stayed closed. - Tab ids are lowercase
[a-z0-9_-], at most 64 characters. Malformed values are ignored. - An unknown or unavailable tab falls back to the app’s first enabled tab. The URL is not rewritten, so a tab that registers later (for example after a permission check resolves) still opens.
- Host-specific params travel with the dialog and are cleared with it:
settingsBoardId(Tasks/web board settings) andsettingsLinkedProvider(web identity-link callback). - When the signed-in web root (
/or/<locale>) redirects to the default workspace, settings params are carried over. Other root query params are still dropped, as before.
History
- Opening a closed dialog pushes a history entry, so Back closes it and Forward reopens it.
- Switching tabs and closing replace the current entry, so tab browsing does not fill the history stack.
- All updates are shallow (no server round-trip) and keep the scroll position.
- apps/web shows the dialog optimistically on an open intent, before the URL updates. That optimistic state is dropped as soon as the URL acknowledges the open, even if the lazy dialog bundle is still loading. Back always closes the dialog.
Identity-link callback
settingsLinkedProvider is set on the return URL whether the provider flow
succeeded, failed or was cancelled. apps/web shows the “linked” success message
only after a fresh identities read contains that provider. Otherwise it shows a
neutral “couldn’t confirm” message.
Tabs every app has
Scope: every satellite that renders the shared satellite user menu. Default: every such app shows one Account group with the same tab ids as apps/web, so?settingsTab=<tab> means the same thing in every app.
Apps with their own dialog (ai, calendar, cms, contacts, drive, finance, forms,
git, infrastructure, inventory, rewise, tasks, track) add the Account group with
createAccountSettingsNavGroup and render SatelliteAccountSettingsPanel, next
to their app tabs. Apps without one (chat, learn, lettin, mail, meet, mind,
parley, storefront, teach) get SatelliteDefaultSettingsDialog: the Account
group, appearance and the onboarding guide. The satellite user menu has a
Settings item (Cmd/Ctrl+,) that opens the same dialog.
App-specific tabs stay pluggable. An app adds its own nav groups and panels, and
SatelliteAccountSettingsPanel renders nothing for tabs it does not own.
Security-sensitive tabs stay in apps/web
Satellites do not reimplement password change, 2FA, passkeys, identity linking or session revocation, and they do not proxy those mutations. apps/web owns re-authentication, the WebAuthn relying party and session revocation. The satelliteaccounts, security and sessions tabs link to
<web origin>/personal?settingsDialog=open&settingsTab=<tab> in a new tab
(rel="noopener noreferrer"), which opens the web dialog on the same tab.
Satellites edit only display name, full name and avatar upload, through the
existing web profile endpoints (/api/v1/users/me/*) in
@tuturuuu/internal-api, using the satellite session and the /api proxy
contract.
- Email is a link-out. A satellite session’s admin client has no provider
session and cannot change the auth email. Satellites show the email read-only
and link to
?settingsTab=profilein apps/web, which owns verification. They never callPATCH /api/v1/users/me/email. - Avatar removal is off in satellites
(
SATELLITE_AVATAR_REMOVE_ENABLED) until the web avatar route fix is merged. Users remove avatars in apps/web.
Exceptions
- apps/web keeps its own dialog with the full tab set.
- nova keeps its own single-page profile dialog. It does not use the satellite user menu.
- hive, pay, apps, tools, external, playground and shortener have no user settings dialog.
- Personal timezone and notification preferences are only in apps/web.
- Calendar keeps workspace timezone under its own calendar tabs.
Implementation
packages/ui/src/hooks/settings-dialog-url.ts is the single source of truth: the
param names, tab-id sanitizing, parse/serialize, fallback resolution, href
building and history mode. useSettingsDialogUrlState
(packages/ui/src/hooks/use-settings-dialog-url-state.ts) is the only way a host
reads or writes the dialog URL state. Build links with buildSettingsDialogHref
instead of hand-writing query strings.
Regression coverage
packages/ui/src/hooks/settings-dialog-url.test.ts: parse/serialize, the baresettingsTabdeep link, sanitizing, unknown-tab fallback, history mode.packages/ui/src/hooks/use-settings-dialog-url-state.test.tsx: push on open, replace on tab switch, every param cleared on close.packages/ui/src/components/ui/custom/__tests__/settings-dialog-shell.test.tsx: URL-restored tab and unknown-tab fallback without a URL rewrite.packages/satellite/src/components/user-nav-client-deep-link.test.tsx: the shared satellite menu host.apps/calendar/src/components/settings/settings-dialog-host.test.tsxandapps/tasks/src/components/settings/settings-dialog-host.test.tsx: dedicated satellite hosts.apps/web/src/components/settings/settings-dialog-shortcuts.test.tsx: web dashboard and user-menu hosts.apps/web/src/components/settings/settings-dialog-host-lazy.test.tsx: Back while the lazy dialog import is pending keeps the dialog closed, including after a repeated open (Cmd/Ctrl+, twice on a task board).apps/web/src/components/settings/account/use-linked-provider-confirmation.test.tsx: link success only after confirmation.apps/web/src/components/settings/account/linked-identities-card.test.tsx: the real card shows no toast while identities are pending, neutral wording for an unlinked provider, and success only when the provider is present.packages/satellite/src/components/account-settings/account-settings.test.tsx: shared Account group tab ids and the web link-out URL.packages/satellite/src/components/user-nav-client-settings.test.tsx: the default dialog for apps without their own, and the menu Settings item.packages/ui/src/components/ui/custom/satellite-user-menu-items.test.tsx: the Settings menu item.packages/satellite/src/components/profile-settings/profile-sensitive-actions.test.tsx: the email link-out sends no email PATCH, and avatar removal is not offered.apps/calendar/src/components/settings/settings-dialog.test.tsx: an app-specific dialog includes the shared account tabs.apps/web/src/__tests__/proxy.test.ts: settings params survive the root redirect.