# Supabase Studio Next.js pages router + TanStack Start (mid-migration, see below), React 19. Dev server: `pnpm dev:studio` → http://localhost:8082. ## Skills — load before working **Always load the `studio-best-practices` skill before writing or modifying any Studio code.** Then stack the area-specific skills: | Task | Additional skills | | ------------------------------------------------------ | ------------------------------------------------------------------------------------------------- | | Query/mutation hooks, query keys (`data/**`) | `studio-queries` | | UI: pages, forms, tables, charts, sheets, empty states | `studio-ui-patterns` | | Displaying API errors | `studio-error-handling` | | Tests (deciding, writing, reviewing) | `studio-testing`, then `studio-mock-api-tests` (component/MSW) or `studio-e2e-tests` (Playwright) | | PostHog event tracking | `telemetry-standards` | | SQL against user databases | `safe-sql-execution` | | Logs Explorer SQL, `data/logs` | `clickhouse-logs-queries` | | Component API design, boolean-prop refactors | `vercel-composition-patterns` | | User-facing copy | `copywriting` | ## TanStack Start migration Studio is migrating from the Next.js pages router (`pages/**`) to TanStack Start (`routes/**`). Both runtimes ship side-by-side; the `STUDIO_FRAMEWORK` env var selects which one `pnpm dev`/`build` runs (default: `next`, resolved in `scripts/dispatch.js`). Full route map and strategy: `TANSTACK_MIGRATION.md`. - **Never delete a page file.** Most `routes/**` files are thin wrappers re-exporting the default export of their `pages/**` counterpart, so the Next file is load-bearing for both runtimes until the final cleanup pass. - Pure page-body edits propagate to the route automatically. Mirror a change by hand into the corresponding `routes/**` file only when it touches what the route duplicates: `getLayout`/layout wrapping, page titles or other `staticData` (incl. `skip*Layout` flags), `withAuth`, or redirect paths. - A new page under `pages/**` needs a matching route under `routes/**` plus a checklist entry in `TANSTACK_MIGRATION.md`. - New code uses native TanStack APIs — no `next/router` or `next/link`. The `compat/next/` shims exist only for legacy re-exported pages. - `routeTree.gen.ts` is generated by the Vite plugin — never hand-edit. ## Orientation - **Data layer** — all platform API calls go through `data/fetchers.ts` (`openapi-fetch`, typed by the generated `api-types` package) with `handleError`; never raw `fetch`. One folder per resource in `data/`, most with a `keys.ts` query-key factory. - **State** — valtio for global state (`state/`), nuqs for URL state, react-hook-form + zod for forms. - **Platform vs self-hosted** — `IS_PLATFORM` gates platform-only behavior; `withAuth` is a no-op when self-hosted. - **Telemetry** — `useTrack()` from `lib/telemetry/track`; event types live in `packages/common/telemetry-constants.ts`. - **Tests** — default to including relevant tests with any change: a couple of unit tests for extracted logic, component tests for UI behavior, E2E only when the scope demands it (`studio-testing` has the decision tree). Not every PR needs them, but "no tests" should be a considered choice, not the default. Tooling: vitest + MSW; component tests use `customRender` + `addAPIMock` from `tests/lib/`; unhandled network requests fail tests. Don't `vi.mock('@/data/...')`. - **Shortcuts** — use the registry in `state/shortcuts/` and `components/ui/Shortcut*.tsx`; keep `G then …` chords for navigation; no one-off keyboard listeners. - **Reuse first** — before writing a new hook or helper, search for an existing one (`hooks/`, `lib/`, `packages/common`, `packages/ui-patterns`). If you do need a new one, make it as reusable as possible: general naming, no page-specific coupling, placed where other callers can find it. - Co-locate sub-components with their parent; avoid barrel re-export files. ## Defaults that differ here - **ESLint warnings are ratcheted in CI**: the per-rule occurrence count must not increase, so a new `any`, unresolved `exhaustive-deps` warning, or default export fails the build even though it's "only a warning". Check locally with `pnpm --filter studio run lint:ratchet`. - **Clipboard**: `copyToClipboard` from `'ui'`, and never `await` anything before calling it (Safari requires the write inside the user gesture; lint-enforced) — pass a Promise as the argument instead. - **`useParams()` comes from `'common'`**, not `next/navigation` — it camelCases keys and returns `string | undefined`. - **Permissions**: `useAsyncCheckPermissions` from `hooks/misc/useCheckPermissions` (returns `can: true` when self-hosted). - **Gating**: `useIsFeatureEnabled` for product features, `useFlag` from `'common'` for feature flags — two different systems. - **Dates**: `dayjs` (plugins pre-loaded at both entries, `pages/_app.tsx` and `routes/__root.tsx`), not `date-fns`. **Toasts**: `toast` from `'sonner'`. - **Import split**: `'ui'` = primitives, `'ui-patterns'` = composed patterns (`ConfirmationModal`, …), `@ui/*` = alias into `packages/ui/src`. Icons come from `lucide-react`. - **New tables** use `@tanstack/react-table`; `react-data-grid` is banned for new code. - **Ad-hoc SQL** against the user's database goes through `executeSql` / `useExecuteSqlMutation` (`data/sql/execute-sql-mutation`). - **Confirmations**: `ConfirmationModal` / `TextConfirmModal` from `ui-patterns`, never `window.confirm`. Disabled buttons needing an explanation use `ButtonTooltip`; inline warnings use `Admonition`.