This document is normative for navigation and URL state in Studio.
Navigation state MUST be URL-driven and managed through useNavigation + Nuqs. Do not introduce alternate routing/state systems for Studio views.
This architecture governs:
- active Studio view (
table,schema,console,sql,stream,queries) - active schema/table/stream
- active stream follow mode
- active stream request-observability sheet lookup
- active stream aggregation-panel visibility
- active stream aggregation range while the aggregation panel is open
- pagination URL state
- sorting URL state
- column pinning URL state
- applied filter URL state
- hash adapter behavior
- URL params are the source of truth for navigation state.
- All reads/writes MUST go through
useNavigationorcreateUrl. - Hash synchronization MUST go through
NuqsHashAdapter. - Components MUST NOT write
window.location.hashdirectly. - Components MUST NOT parse URL params manually.
Only keys declared in ui/hooks/nuqs.ts are allowed:
viewschematablestreamstreamFollowstreamObserveaggregationsstreamAggregationRangefiltersortpinpageIndexpageSizesearchsearchScope
Notes:
searchis shared search term state for the active data view. In table view it drives row search, and in stream view it drives stream-event search when the selected stream advertises search capability.searchScopeis legacy URL state and is not used for table-name navigation filtering.pinstores left-pinned data columns for the grid as a comma-separated list (for examplepin=id,bigint_col).pinorder is authoritative and MUST be updated when users drag-reorder pinned columns.pageIndexremains URL-backed for table navigation.pageSizeremains a supported hash key for compatibility, but table rendering now takes its authoritative rows-per-page preference fromstudioUiCollection.tablePageSizeinArchitecture/ui-state.md.streamFollowstores the active stream follow mode (paused,live, ortail).streamObservestores the active request-observability lookup for supported Streams profiles. Values serialize asreq:<requestId>,trace:<traceId>, orspan:<spanId>.aggregationsis an open-only flag for the active stream aggregation strip; when present it MUST be serialized as a bare key with no explicit value.streamAggregationRangestores the active stream aggregation range, but MUST only be serialized whileaggregationsis present.
Adding a new URL key requires updating StateKey in nuqs.ts first.
useNavigationInternal derives defaults from adapter + introspection:
schema: adapter default schema, else first introspected schema, elsepublictable: first table in resolved schemafilter: serializeddefaultFilterpageIndex:"0"pageSize:"25"search:""searchScope:"table"(legacy default)view:"table"queries: no standalone default; only meaningful when the current adapter provides query insightsstream: no default; only meaningful whenview=streamstreamFollow: no global default inuseNavigation; the active stream view MUST resolve an absent value totailand materialize that into the hashstreamObserve: no global default inuseNavigation; the active stream view MUST treat an absent or malformed value as a closed request-observability sheetaggregations: no global default inuseNavigation; the active stream view MUST treat an absent flag as closed and MUST NOT materialize that closed state into the hashstreamAggregationRange: no standalone default; the active stream view MUST clear it wheneveraggregationsis absent, and MUST materialize its default range only after the aggregation panel is opened
When Studio is running without a database connection but with Streams enabled:
- the resolved default
viewMUST become"stream"instead of"table" - stale database-oriented views such as
table,schema,console,sql, andqueriesMUST resolve back to the stream view instead of trying to render database-only UI against a disabled database session
When URL params are stale from a previous DB, invalid schema/table values MUST be resolved to valid current defaults.
When URL params contain view=queries but the current adapter does not provide query insights, useNavigation MUST resolve back to the default view and the sidebar MUST hide the Queries link.
Shared table page size and infinite-scroll mode are not derived from URL defaults; they are restored through Studio UI state and then mirrored into query behavior by usePagination.
NuqsHashAdapter is required for Studio.
- It stores the raw hash in TanStack DB UI state key
nuqs-hash. - It updates browser history using Nuqs adapter options (
pushvsreplace). - It listens to
hashchangeand debounces updates. - It exposes snapshots as
URLSearchParamsfor Nuqs.
Do not replace hash synchronization logic with custom listeners in feature code.
Use two patterns only:
- Link rendering:
href={createUrl({ ...ParamValues })} - Imperative updates:
setViewParam,setSchemaParam,setTableParam,setStreamParam, etc.
On schema switch, code MUST also resolve and set a valid table for that schema (current behavior in Navigation.SchemaSelector).
Database view links in the Studio sidebar (schema, queries, console, and
sql) MUST preserve the active schema URL param so switching views does not
silently fall back to the adapter default schema.
NavigationContextProvider wraps one useNavigationInternal instance to reduce re-renders and centralize behavior.
Feature components MUST consume useNavigation() and MUST NOT create independent URL management hooks.
- Manual
URLSearchParamsparsing in feature components. - Manual
history.pushState/replaceStateoutsideNuqsHashAdapter. - Local component state that duplicates URL navigation state.
- View selection logic based on anything other than
viewParam.
Navigation changes MUST include tests for:
- hash <-> query state sync via
NuqsHashAdapter - stale schema/table fallback behavior
- URL write serialization for changed controls (sort/filter/pagination/view)
Baseline reference test: