diff --git a/.claude/scripts/install_pkgs.sh b/.claude/scripts/install_pkgs.sh
new file mode 100755
index 00000000000..b407f3ce31a
--- /dev/null
+++ b/.claude/scripts/install_pkgs.sh
@@ -0,0 +1,7 @@
+#!/bin/bash
+
+if [ "$CLAUDE_CODE_REMOTE" != "true" ]; then
+ exit 0
+fi
+
+pnpm install
diff --git a/.claude/settings.json b/.claude/settings.json
new file mode 100644
index 00000000000..502fd54dc4b
--- /dev/null
+++ b/.claude/settings.json
@@ -0,0 +1,15 @@
+{
+ "hooks": {
+ "SessionStart": [
+ {
+ "matcher": "startup",
+ "hooks": [
+ {
+ "type": "command",
+ "command": "\"$CLAUDE_PROJECT_DIR\"/.claude/scripts/install_pkgs.sh"
+ }
+ ]
+ }
+ ]
+ }
+}
diff --git a/.claude/skills/e2e-studio-tests/SKILL.md b/.claude/skills/e2e-studio-tests/SKILL.md
new file mode 100644
index 00000000000..6f9797f720d
--- /dev/null
+++ b/.claude/skills/e2e-studio-tests/SKILL.md
@@ -0,0 +1,233 @@
+---
+name: e2e-studio-tests
+description: Run e2e tests in the Studio app. Use when asked to run e2e tests, run studio tests, playwright tests, or test the feature.
+---
+
+# E2E Studio Tests
+
+Run Playwright end-to-end tests for the Studio application.
+
+## Running Tests
+
+Tests must be run from the `e2e/studio` directory:
+
+```bash
+cd e2e/studio && pnpm run e2e
+```
+
+### Run specific file
+
+```bash
+cd e2e/studio && pnpm run e2e -- features/cron-jobs.spec.ts
+```
+
+### Run with grep filter
+
+```bash
+cd e2e/studio && pnpm run e2e -- --grep "test name pattern"
+```
+
+### UI mode for debugging
+
+```bash
+cd e2e/studio && pnpm run e2e -- --ui
+```
+
+## Environment Setup
+
+- Tests auto-start Supabase local containers via web server config
+- Self-hosted mode (`IS_PLATFORM=false`) runs tests in parallel (3 workers)
+- No manual setup needed for self-hosted tests
+
+## Test File Structure
+
+- Tests are in `e2e/studio/features/*.spec.ts`
+- Use custom test utility: `import { test } from '../utils/test.js'`
+- Test fixtures provide `page`, `ref`, and other helpers
+
+## Common Patterns
+
+Wait for elements with generous timeouts:
+
+```typescript
+await expect(locator).toBeVisible({ timeout: 30000 })
+```
+
+Add messages to expects for debugging:
+
+```typescript
+await expect(locator).toBeVisible({ timeout: 30000 }, 'Element should be visible after page load')
+```
+
+Use serial mode for tests sharing database state:
+
+```typescript
+test.describe.configure({ mode: 'serial' })
+```
+
+## Writing Robust Selectors
+
+### Selector priority (best to worst)
+
+1. **`getByRole` with accessible name** - Most robust, tests accessibility
+ ```typescript
+ page.getByRole('button', { name: 'Save' })
+ page.getByRole('button', { name: 'Configure API privileges' })
+ ```
+
+2. **`getByTestId`** - Stable, explicit test hooks
+ ```typescript
+ page.getByTestId('table-editor-side-panel')
+ ```
+
+3. **`getByText` with exact match** - Good for unique text
+ ```typescript
+ page.getByText('Data API Access', { exact: true })
+ ```
+
+4. **`locator` with CSS** - Use sparingly, more fragile
+ ```typescript
+ page.locator('[data-state="open"]')
+ ```
+
+### Patterns to avoid
+
+- **XPath selectors** - Fragile to DOM changes
+ ```typescript
+ // BAD
+ locator('xpath=ancestor::div[contains(@class, "space-y")]')
+ ```
+
+- **Parent traversal with `locator('..')`** - Breaks when structure changes
+ ```typescript
+ // BAD
+ element.locator('..').getByRole('button')
+ ```
+
+- **Broad `filter({ hasText })` on generic elements** - May match multiple elements
+ ```typescript
+ // BAD - popover may have more than one combobox
+ // Could consider scoping down the container or filtering the combobox more specifically
+ popover.getByRole('combobox')
+ ```
+
+### Add accessible labels to components
+
+When a component lacks a good accessible name, add one in the source code:
+
+```tsx
+// In the React component
+
+```
+
+Then use it in tests:
+```typescript
+page.getByRole('button', { name: 'Configure API privileges' })
+```
+
+### Narrowing search scope
+
+Scope selectors to specific containers to avoid matching wrong elements:
+
+```typescript
+// Good - scoped to side panel
+const sidePanel = page.getByTestId('table-editor-side-panel')
+const toggle = sidePanel.getByRole('switch')
+
+// Good - find unique element, then scope from there
+const popover = page.locator('[data-radix-popper-content-wrapper]')
+const roleSection = popover.getByText('Anonymous (anon)', { exact: true })
+```
+
+## Avoiding `waitForTimeout`
+
+Never use `waitForTimeout` - always wait for something specific:
+
+```typescript
+// BAD
+await page.waitForTimeout(1000)
+
+// GOOD - wait for UI element
+await expect(page.getByText('Success')).toBeVisible()
+
+// GOOD - wait for API response
+const apiPromise = waitForApiResponse(page, 'pg-meta', ref, 'query?key=table-create')
+await saveButton.click()
+await apiPromise
+
+// GOOD - wait for toast indicating operation complete
+await expect(page.getByText('Table created successfully')).toBeVisible({ timeout: 15000 })
+```
+
+## Avoiding `force: true` on clicks
+
+Instead of forcing clicks on hidden elements, make them visible first:
+
+```typescript
+// BAD
+await menuButton.click({ force: true })
+
+// GOOD - hover to reveal, then click
+await tableRow.hover()
+await expect(menuButton).toBeVisible()
+await menuButton.click()
+```
+
+## Debugging
+
+### View trace
+
+```bash
+cd e2e/studio && pnpm exec playwright show-trace
+```
+
+### View HTML report
+
+```bash
+cd e2e/studio && pnpm exec playwright show-report
+```
+
+### Error context
+
+Error context files are saved in the `test-results/` directory.
+
+### Playwright MCP tools
+
+Use Playwright MCP tools to inspect UI when debugging locally.
+
+## CI vs Local Development
+
+The key difference is **cold start vs warm state**:
+
+### CI (cold start)
+
+Tests run from a blank database slate. Each test run resets the database and starts fresh containers. Extensions like pg_cron are NOT enabled by default.
+
+### Local dev with `pnpm dev:studio-local`
+
+When debugging with a running dev server, the database may already have state from previous runs (extensions enabled, test data present).
+
+## Handling Cold Start Bugs
+
+Tests that work locally but fail in CI often have assumptions about existing state.
+
+### Common issues
+
+1. Extension not enabled (must enable in test setup)
+2. Race conditions when parallel tests try to modify shared state (use `test.describe.configure({ mode: 'serial' })`)
+3. Locators matching wrong elements because the page structure differs when state isn't set up
+
+### Reproducing CI behavior locally
+
+The test framework automatically resets the database when running `pnpm run e2e`. This matches CI behavior.
+
+If using `pnpm dev:studio-local` for Playwright MCP debugging, remember the state differs from CI.
+
+## Debugging Workflow for CI Failures
+
+1. First, run the test locally with `pnpm run e2e -- features/.spec.ts` (cold start)
+2. Check error context in `test-results/` directory
+3. If you need to inspect UI state, start `pnpm dev:studio-local` and use Playwright MCP tools
+4. Remember: what you see in the dev server may have state that doesn't exist in CI
diff --git a/.cursor/rules/docs-graphql.mdc b/.cursor/rules/docs-graphql.mdc
deleted file mode 100644
index d8bcbc5d932..00000000000
--- a/.cursor/rules/docs-graphql.mdc
+++ /dev/null
@@ -1,267 +0,0 @@
----
-description: Docs GraphQL Architecture
-globs: apps/docs/resources/**/*.ts
-alwaysApply: false
----
-
-# Docs GraphQL Architecture
-
-## Overview
-
-The `/apps/docs/resources` folder contains the GraphQL endpoint architecture for the docs GraphQL endpoint at `/api/graphql`. It follows a modular pattern where each top-level query is organized into its own folder with consistent file structure.
-
-## Architecture Pattern
-
-Each GraphQL query follows this structure:
-
-```
-resources/
-├── queryObject/
-│ ├── queryObjectModel.ts # Data models and business logic
-│ ├── queryObjectSchema.ts # GraphQL type definitions
-│ ├── queryObjectResolver.ts # Query resolver and arguments
-│ ├── queryObjectTypes.ts # TypeScript interfaces (optional)
-│ └── queryObjectSync.ts # Functions for syncing repo content to the database (optional)
-├── utils/
-│ ├── connections.ts # GraphQL connection/pagination utilities
-│ └── fields.ts # GraphQL field selection utilities
-├── rootSchema.ts # Main GraphQL schema with all queries
-└── rootSync.ts # Root sync script for syncing to database
-```
-
-## Example queries
-
-1. **searchDocs** (`globalSearch/`) - Vector-based search across all docs content
-2. **error** (`error/`) - Error code lookup for Supabase services
-3. **schema** - GraphQL schema introspection
-
-## Key Files
-
-### `rootSchema.ts`
-- Main GraphQL schema definition
-- Imports all resolvers and combines them into the root query
-- Defines the `RootQueryType` with all top-level fields
-
-### `utils/connections.ts`
-- Provides `createCollectionType()` for paginated collections
-- `GraphQLCollectionBuilder` for building collection responses
-- Standard pagination arguments and edge/node patterns
-
-### `utils/fields.ts`
-- `graphQLFields()` utility to analyze requested fields in resolvers
-- Used for optimizing data fetching based on what fields are actually requested
-
-## Creating a New Top-Level Query
-
-To add a new GraphQL query, follow these steps:
-
-### 1. Create Query Folder Structure
-```bash
-mkdir resources/newQuery
-touch resources/newQuery/newQueryModel.ts
-touch resources/newQuery/newQuerySchema.ts
-touch resources/newQuery/newQueryResolver.ts
-```
-
-### 2. Define GraphQL Schema (`newQuerySchema.ts`)
-```typescript
-import { GraphQLObjectType, GraphQLString } from 'graphql'
-
-export const GRAPHQL_FIELD_NEW_QUERY = 'newQuery' as const
-
-export const GraphQLObjectTypeNewQuery = new GraphQLObjectType({
- name: 'NewQuery',
- description: 'Description of what this query returns',
- fields: {
- id: {
- type: GraphQLString,
- description: 'Unique identifier',
- },
- // Add other fields...
- },
-})
-```
-
-### 3. Create Data Model (`newQueryModel.ts`)
-
-> [!NOTE]
-> The data model should be agnostic to GraphQL. It may import argument types
-> from `~/__generated__/graphql`, but otherwise all functions and classes
-> should be unaware of whether they are called for GraphQL resolution.
-
-> [!TIP]
-> The types in `~/__generated__/graphql` for a new endpoint will not exist
-> until the code generation is run in the next step.
-
-```typescript
-import { type RootQueryTypeNewQueryArgs } from '~/__generated__/graphql'
-import { convertPostgrestToApiError, type ApiErrorGeneric } from '~/app/api/utils'
-import { Result } from '~/features/helpers.fn'
-import { supabase } from '~/lib/supabase'
-
-export class NewQueryModel {
- constructor(public readonly data: {
- id: string
- // other properties...
- }) {}
-
- static async loadData(
- args: RootQueryTypeNewQueryArgs,
- requestedFields: Array
- ): Promise> {
- // Implement data fetching logic
- const result = new Result(
- await supabase()
- .from('your_table')
- .select('*')
- // Add filters based on args
- )
- .map((data) => data.map((item) => new NewQueryModel(item)))
- .mapError(convertPostgrestToApiError)
-
- return result
- }
-}
-```
-
-### 4. Create Resolver (`newQueryResolver.ts`)
-```typescript
-import { GraphQLError, GraphQLNonNull, GraphQLString, type GraphQLResolveInfo } from 'graphql'
-import { type RootQueryTypeNewQueryArgs } from '~/__generated__/graphql'
-import { convertUnknownToApiError } from '~/app/api/utils'
-import { Result } from '~/features/helpers.fn'
-import { graphQLFields } from '../utils/fields'
-import { NewQueryModel } from './newQueryModel'
-import { GRAPHQL_FIELD_NEW_QUERY, GraphQLObjectTypeNewQuery } from './newQuerySchema'
-
-async function resolveNewQuery(
- _parent: unknown,
- args: RootQueryTypeNewQueryArgs,
- _context: unknown,
- info: GraphQLResolveInfo
-): Promise {
- return (
- await Result.tryCatchFlat(
- resolveNewQueryImpl,
- convertUnknownToApiError,
- args,
- info
- )
- ).match(
- (data) => data,
- (error) => {
- console.error(`Error resolving ${GRAPHQL_FIELD_NEW_QUERY}:`, error)
- return new GraphQLError(error.isPrivate() ? 'Internal Server Error' : error.message)
- }
- )
-}
-
-async function resolveNewQueryImpl(
- args: RootQueryTypeNewQueryArgs,
- info: GraphQLResolveInfo
-): Promise> {
- const fieldsInfo = graphQLFields(info)
- const requestedFields = Object.keys(fieldsInfo)
- return await NewQueryModel.loadData(args, requestedFields)
-}
-
-export const newQueryRoot = {
- [GRAPHQL_FIELD_NEW_QUERY]: {
- description: 'Description of what this query does',
- args: {
- id: {
- type: new GraphQLNonNull(GraphQLString),
- description: 'Required argument description',
- },
- // Add other arguments...
- },
- type: GraphQLObjectTypeNewQuery, // or createCollectionType() for lists
- resolve: resolveNewQuery,
- },
-}
-```
-
-### 5. Register in Root Schema
-In `rootSchema.ts`, add your resolver:
-
-```typescript
-// Import your resolver
-import { newQueryRoot } from './newQuery/newQueryResolver'
-
-// Add to the query fields
-export const rootGraphQLSchema = new GraphQLSchema({
- query: new GraphQLObjectType({
- name: 'RootQueryType',
- fields: {
- ...introspectRoot,
- ...searchRoot,
- ...errorRoot,
- ...newQueryRoot, // Add this line
- },
- }),
- types: [
- GraphQLObjectTypeGuide,
- GraphQLObjectTypeReferenceCLICommand,
- GraphQLObjectTypeReferenceSDKFunction,
- GraphQLObjectTypeTroubleshooting,
- ],
-})
-```
-
-### 6. Update TypeScript Types
-Run the GraphQL codegen to update TypeScript types:
-```bash
-pnpm run -F docs codegen:graphql
-```
-
-## Best Practices
-
-1. **Error Handling**: Error handling always uses the Result class, defined in apps/docs/features/helpers.fn.ts
-2. **Field Optimization**: Use `graphQLFields()` to only fetch requested data
-3. **Collections**: Use `createCollectionType()` for paginated lists
-4. **Naming**: Use `GRAPHQL_FIELD_*` constants for field names
-5. **Documentation**: Add GraphQL descriptions to all fields and types
-6. **Database**: Use `supabase()` client for database operations with `convertPostgrestToApiError`
-
-## Testing
-
-Tests are located in apps/docs/app/api/graphql/tests. Each top-level query
-should have its own test file, located at .test.ts.
-
-### Test data
-
-Test data uses a local database, seeded with the file at supabase/seed.sql. Add
-any data required for running your new query.
-
-### Integration tests
-
-Integration tests import the POST function defined in
-apps/docs/api/graphql/route.ts, then make a request to this function.
-
-For example:
-
-```ts
-import { POST } from '../route'
-
-it('test name', async () => {
- const query = `
- query {
- ...
- }
- `
- const request = new Request('http://localhost/api/graphql', {
- method: 'POST',
- body: JSON.stringify({ query }),
- })
-
- const result = await POST(request)
-})
-```
-
-Include at least the following tests:
-
-1. A test that requests all fields (including nested fields) on the new query
- object, and asserts that there are no errors, and the requested fields are
- properly returned.
-2. A test that triggers and error, and asserts that a GraphQL error is properly
- returned.
diff --git a/.cursor/rules/docs-test-requirements.mdc b/.cursor/rules/docs-test-requirements.mdc
deleted file mode 100644
index f76ca2ba7cf..00000000000
--- a/.cursor/rules/docs-test-requirements.mdc
+++ /dev/null
@@ -1,71 +0,0 @@
----
-description: Docs Testing Procedure
-globs: apps/docs/**/*.test.ts
-alwaysApply: false
----
-
-# Docs Test Requirements
-
-Rules for running tests in the docs application, ensuring proper Supabase setup and test execution.
-
-
-name: docs_test_requirements
-description: Standards for running tests in the docs application with proper Supabase setup
-filters:
- # Match test files in the docs app
- - type: file_extension
- pattern: "\\.(test|spec)\\.(ts|tsx)$"
- - type: path
- pattern: "^apps/docs/.*"
- # Match test execution events
- - type: event
- pattern: "test_execution"
-
-actions:
- - type: suggest
- message: |
- Before running tests in the docs app:
-
- 1. Check Supabase status:
- ```bash
- pnpm supabase status
- ```
-
- 2. If Supabase is not running:
- ```bash
- pnpm supabase start
- ```
-
- 3. Reset the database to ensure clean state:
- ```bash
- pnpm supabase db reset --local
- ```
-
- 4. Run the tests:
- ```bash
- pnpm run -F docs test:local:unwatch
- ```
-
- Important notes:
- - Always ensure Supabase is running before tests
- - Database must be reset to ensure clean state
- - Use test:local:unwatch to run tests without watch mode
- - Tests are located in apps/docs/**/*.{test,spec}.{ts,tsx}
-
-examples:
- - input: |
- # Bad: Running tests without proper setup
- pnpm run -F docs test
- pnpm run -F docs test:local
-
- # Good: Proper test execution sequence
- pnpm supabase status
- pnpm supabase start # if not running
- pnpm supabase db reset --local
- pnpm run -F docs test:local:unwatch
- output: "Correctly executed docs tests with proper Supabase setup"
-
-metadata:
- priority: high
- version: 1.0
-
diff --git a/.cursor/rules/docs-embeddings-generation.md b/.cursor/rules/docs/docs-embeddings-generation/RULE.md
similarity index 79%
rename from .cursor/rules/docs-embeddings-generation.md
rename to .cursor/rules/docs/docs-embeddings-generation/RULE.md
index 22ce5fd4359..6eab6f71510 100644
--- a/.cursor/rules/docs-embeddings-generation.md
+++ b/.cursor/rules/docs/docs-embeddings-generation/RULE.md
@@ -1,3 +1,10 @@
+---
+description: "Docs: embeddings generation pipeline (apps/docs/scripts/search)"
+globs:
+ - apps/docs/scripts/search/**/*.ts
+alwaysApply: false
+---
+
# Documentation Embeddings Generation System
## Overview
@@ -12,31 +19,34 @@ The documentation embeddings generation system processes various documentation s
## Architecture
### Main Entry Point
-- `generate-embeddings.ts` - Main script that orchestrates the entire process
+
+- `apps/docs/scripts/search/generate-embeddings.ts` - Main script that orchestrates the entire process
- Supports `--refresh` flag to force regeneration of all content
### Content Sources (`sources/` directory)
#### Base Classes
+
- `BaseLoader` - Abstract class for loading content from different sources
- `BaseSource` - Abstract class for processing and formatting content
#### Source Types
-1. **Markdown Sources** (`markdown.ts`)
+
+1. **Markdown Sources** (`apps/docs/scripts/search/sources/markdown.ts`)
- Processes `.mdx` files from guides and documentation
- Extracts frontmatter metadata and content sections
-2. **Reference Documentation** (`reference-doc.ts`)
+2. **Reference Documentation** (`apps/docs/scripts/search/sources/reference-doc.ts`)
- **OpenAPI References** - Management API documentation from OpenAPI specs
- **Client Library References** - JavaScript, Dart, Python, C#, Swift, Kotlin SDKs
- **CLI References** - Command-line interface documentation
- Processes YAML/JSON specs and matches with common sections
-3. **GitHub Discussions** (`github-discussion.ts`)
+3. **GitHub Discussions** (`apps/docs/scripts/search/sources/github-discussion.ts`)
- Fetches troubleshooting discussions from GitHub using GraphQL API
- Uses GitHub App authentication for access
-4. **Partner Integrations** (`partner-integrations.ts`)
+4. **Partner Integrations** (`apps/docs/scripts/search/sources/partner-integrations.ts`)
- Fetches approved partner integration documentation from Supabase database
- Technology integrations only (excludes agencies)
@@ -56,4 +66,3 @@ The documentation embeddings generation system processes various documentation s
- **`page`** table: Stores page metadata, content, checksum, version
- **`page_section`** table: Stores individual sections with embeddings, token counts
-
diff --git a/.cursor/rules/docs/docs-graphql/RULE.md b/.cursor/rules/docs/docs-graphql/RULE.md
new file mode 100644
index 00000000000..8b16f176dc4
--- /dev/null
+++ b/.cursor/rules/docs/docs-graphql/RULE.md
@@ -0,0 +1,132 @@
+---
+description: "Docs: GraphQL architecture for apps/docs/resources"
+globs:
+ - apps/docs/resources/**/*.ts
+alwaysApply: false
+---
+
+# Docs GraphQL Architecture
+
+## Overview
+
+The `apps/docs/resources` folder contains the GraphQL endpoint architecture for the docs GraphQL endpoint at `/api/graphql`. It follows a modular pattern where each top-level query is organized into its own folder with consistent file structure.
+
+## Architecture Pattern
+
+Each GraphQL query follows this structure:
+
+```
+resources/
+├── queryObject/
+│ ├── queryObjectModel.ts # Data models and business logic
+│ ├── queryObjectSchema.ts # GraphQL type definitions
+│ ├── queryObjectResolver.ts # Query resolver and arguments
+│ ├── queryObjectTypes.ts # TypeScript interfaces (optional)
+│ └── queryObjectSync.ts # Functions for syncing repo content to the database (optional)
+├── utils/
+│ ├── connections.ts # GraphQL connection/pagination utilities
+│ └── fields.ts # GraphQL field selection utilities
+├── rootSchema.ts # Main GraphQL schema with all queries
+└── rootSync.ts # Root sync script for syncing to database
+```
+
+## Example queries
+
+1. **searchDocs** (`globalSearch/`) - Vector-based search across all docs content
+2. **error** (`error/`) - Error code lookup for Supabase services
+3. **schema** - GraphQL schema introspection
+
+## Key Files
+
+### `rootSchema.ts`
+
+- Main GraphQL schema definition
+- Imports all resolvers and combines them into the root query
+- Defines the `RootQueryType` with all top-level fields
+
+### `utils/connections.ts`
+
+- Provides `createCollectionType()` for paginated collections
+- `GraphQLCollectionBuilder` for building collection responses
+- Standard pagination arguments and edge/node patterns
+
+### `utils/fields.ts`
+
+- `graphQLFields()` utility to analyze requested fields in resolvers
+- Used for optimizing data fetching based on what fields are actually requested
+
+## Creating a New Top-Level Query
+
+To add a new GraphQL query, follow these steps:
+
+### 1. Create Query Folder Structure
+
+```bash
+mkdir resources/newQuery
+touch resources/newQuery/newQueryModel.ts
+touch resources/newQuery/newQuerySchema.ts
+touch resources/newQuery/newQueryResolver.ts
+```
+
+### 2. Define GraphQL Schema (`newQuerySchema.ts`)
+
+```typescript
+import { GraphQLObjectType, GraphQLString } from 'graphql'
+
+export const GRAPHQL_FIELD_NEW_QUERY = 'newQuery' as const
+
+export const GraphQLObjectTypeNewQuery = new GraphQLObjectType({
+ name: 'NewQuery',
+ description: 'Description of what this query returns',
+ fields: {
+ id: {
+ type: GraphQLString,
+ description: 'Unique identifier',
+ },
+ // Add other fields...
+ },
+})
+```
+
+### 3. Create Data Model (`newQueryModel.ts`)
+
+> [!NOTE]
+> The data model should be agnostic to GraphQL. It may import argument types
+> from `~/__generated__/graphql`, but otherwise all functions and classes
+> should be unaware of whether they are called for GraphQL resolution.
+
+> [!TIP]
+> The types in `~/__generated__/graphql` for a new endpoint will not exist
+> until the code generation is run in the next step.
+
+```typescript
+import { type RootQueryTypeNewQueryArgs } from '~/__generated__/graphql'
+import { convertPostgrestToApiError, type ApiErrorGeneric } from '~/app/api/utils'
+import { Result } from '~/features/helpers.fn'
+import { supabase } from '~/lib/supabase'
+
+export class NewQueryModel {
+ constructor(
+ public readonly data: {
+ id: string
+ // other properties...
+ }
+ ) {}
+
+ static async loadData(
+ args: RootQueryTypeNewQueryArgs,
+ requestedFields: Array
+ ): Promise> {
+ // Implement data fetching logic
+ const result = new Result(
+ await supabase()
+ .from('your_table')
+ .select('*')
+ // Add filters based on args
+ )
+ .map((data) => data.map((item) => new NewQueryModel(item)))
+ .mapError(convertPostgrestToApiError)
+ return result
+ }
+}
+```
diff --git a/.cursor/rules/docs/docs-test-requirements/RULE.md b/.cursor/rules/docs/docs-test-requirements/RULE.md
new file mode 100644
index 00000000000..15cbda674e8
--- /dev/null
+++ b/.cursor/rules/docs/docs-test-requirements/RULE.md
@@ -0,0 +1,25 @@
+---
+description: "Docs: how to run tests locally (Supabase setup + correct commands)"
+globs:
+ - apps/docs/**/*.{test,spec}.{ts,tsx}
+alwaysApply: false
+---
+
+# Docs test requirements
+
+Before running tests for `apps/docs`, ensure local Supabase is available and the DB is in a known state.
+
+## Recommended sequence
+
+```bash
+pnpm supabase status
+pnpm supabase start # if not running
+pnpm supabase db reset --local
+pnpm run -F docs test:local:unwatch
+```
+
+## Notes
+
+- Always reset the local DB before running docs tests to avoid state leakage.
+- Prefer `test:local:unwatch` for non-watch CI-like runs.
+
diff --git a/.cursor/rules/studio-ui.mdc b/.cursor/rules/studio-ui.mdc
deleted file mode 100644
index 67db62a0ba1..00000000000
--- a/.cursor/rules/studio-ui.mdc
+++ /dev/null
@@ -1,253 +0,0 @@
----
-description: How to generate pages and interfaces in Studio, a web interface for managing Supabase projects
-globs:
-alwaysApply: true
----
-
-## Project Structure
-
-- Next.js app using pages router
-- Pages go in @apps/studio/pages
- - Project related pages go in @apps/studio/pages/projects/[ref]
- - Organization related pages go in @apps/studio/pages/org/[slug]
-- Studio specific components go in @apps/studio/components
- - Studio specific generic UI components go in @apps/studio/components/ui
- - Studio specific components related to individual pages go in @apps/studio/components/interfaces e.g. @apps/studio/components/interfaces/Auth
-- Generic helper functions go in @apps/studio/lib
-- Generic hooks go in @apps/studio/hooks
-
-## Component system
-
-Our primitive component system is in @packages/ui and is based off shadcn/ui components. These components can be shared across all @apps e.g. studio and docs. Do not introduce new ui components unless asked to.
-
-- UI components are imported from this package across apps e.g. import { Button, Badge } from 'ui'
-- Some components have a _Shadcn_ namespace appended to component name e.g. import { Input*Shadcn* } from 'ui'
-- We should be using _Shadcn_ components where possible
-- Before composing interfaces, read @packages/ui/index.tsx file for a full list of available components
-
-## Styling
-
-We use Tailwind for styling.
-
-- You should never use tailwind classes for colours and instead use classes we've defined ourselves
- - Backgrounds // most of the time you will not need to define a background
- - 'bg' used for main app surface background
- - 'bg-muted' for elevating content // you can use Card instead
- - 'bg-warning' for highlighting information that needs to be acted on
- - 'bg-destructive' for highlighting issues
- - Text
- - 'text-foreground' for primary text like headings
- - 'text-foreground-light' for body text
- - 'text-foreground-lighter' for subtle text
- - 'text-warning' for calling out information that needs action
- - 'text-destructive' for calling out when something went wrong
-- When needing to apply typography styles, read @apps/studio/styles/typography.scss and use one of the available classes instead of hard coding classes e.g. use "heading-default" instead of "text-sm font-medium"
-
-## Page structure
-
-When creating a new page follow these steps:
-
-- Create the page in @apps/studio/pages
-- Use the PageLayout component that has the following props
-
- ```jsx
- export interface NavigationItem {
- id?: string
- label: string
- href?: string
- icon?: ReactNode
- onClick?: () => void
- badge?: string
- active?: boolean
- }
-
- interface PageLayoutProps {
- children?: ReactNode
- title?: string | ReactNode
- subtitle?: string | ReactNode
- icon?: ReactNode
- breadcrumbs?: Array<{
- label?: string
- href?: string
- element?: ReactNode
- }>
- primaryActions?: ReactNode
- secondaryActions?: ReactNode
- navigationItems?: NavigationItem[]
- className?: string
- size?: 'default' | 'full' | 'large' | 'small'
- isCompact?: boolean
- }
- ```
-
-- If a page has page related actions, add them to primary and secondary action props e.g. Users page has "Create new user" action
-- If a page is within an existing section (e.g. Auth), you should use the related layout component e.g. AuthLayout
-- Create a new component in @apps/studio/components/interfaces for the contents of the page
-- Use ScaffoldContainer if the page should be center aligned in a container
-- Use ScaffoldSection, ScaffoldSectionTitle, ScaffoldSectionDescription if the page has multiple sections
-
-### Page example
-
-```jsx
-import { MyPageComponent } from 'components/interfaces/MyPage/MyPageComponent'
-import AuthLayout from './AuthLayout'
-import DefaultLayout from 'components/layouts/DefaultLayout'
-import { ScaffoldContainer } from 'components/layouts/Scaffold'
-import type { NextPageWithLayout } from 'types'
-
-const MyPage: NextPageWithLayout = () => {
- return (
-
-
-
- )
-}
-
-MyPage.getLayout = (page) => (
-
- {page}
-
-)
-
-export default MyPage
-
-export const MyPageComponent = () => (
-
-
- My page section
- A brief description of the purpose of the page
-
- // Content goes here
-
-)
-```
-
-## Forms
-
-- Build forms with `react-hook-form` + `zod`.
-- Use our `_Shadcn_` form primitives from `ui` and prefer `FormItemLayout` with layout="flex-row-reverse" for most controls (see `apps/studio/components/interfaces/Settings/Integrations/GithubIntegration/GitHubIntegrationConnectionForm.tsx`).
-- Keep imports from `ui` with `_Shadcn_` suffixes.
-- Forms should generally be wrapped in a Card unless specified
-
-### Example (single field)
-
-```tsx
-import { zodResolver } from '@hookform/resolvers/zod'
-import { useForm } from 'react-hook-form'
-import * as z from 'zod'
-
-import { Button, Form_Shadcn_, FormField_Shadcn_, FormControl_Shadcn_, Input_Shadcn_ } from 'ui'
-import { FormItemLayout } from 'ui-patterns/form/FormItemLayout/FormItemLayout'
-
-const profileSchema = z.object({
- username: z.string().min(2, 'Username must be at least 2 characters'),
-})
-
-export function ProfileForm() {
- const form = useForm>({
- resolver: zodResolver(profileSchema),
- defaultValues: { username: '' },
- mode: 'onSubmit',
- reValidateMode: 'onBlur',
- })
-
- function onSubmit(values: z.infer) {
- // handle values
- }
-
- return (
-
-
-
- )
-}
-```
-
-## Cards
-
-- Use cards when needing to group related pieces of information
-- Cards can have sections with CardContent
-- Use CardFooter for actions
-- Only use CardHeader and CardTitle if the card content has not been described by the surrounding content e.g. Page title or ScaffoldSectionTitle
-- Use CardHeader and CardTitle when you are using multiple Cards to group related pieces of content e.g. Primary branch, Persistent branches, Preview branches
-
-## Sheets
-
-- Use a sheet when needing to reveal more complicated forms or information relating to an object and context switching away to a new page would be disruptive e.g. we list auth providers, clicking an auth provider opens a sheet with information about that provider and a form to enable, user can close sheet to go back to providers list
-
-## Tables
-
-- Use the generic ui table components for most tables
-- Tables are generally contained witin a card
-- If a table has associated actions, they should go above on right hand side
-- If a table has associated search or filters, they should go above on left hand side
-- If a table is the main content of a page, and it does not have search or filters, you can add table actions to primary and secondary actions of PageLayout
-- If a table is the main content of a page section, and it does not have search or filters, you can add table actions to the right of ScaffoldSectionTitle
-- For simple lists of objects you can use ResourceList with ResourceListItem instead
-
-### Table example
-
-```jsx
-import { Table, TableBody, TableCaption, TableCell, TableHead, TableHeader, TableRow } from 'ui'
-
-;
- A list of your recent invoices.
-
-
- Invoice
- Status
- Method
- Amount
-
-
-
-
- INV001
- Paid
- Credit Card
- $250.00
-
-
-
-```
-
-## Alerts
-
-- Use Admonition component to alert users of important actions or restrictions in place
-- Place the Admonition either at the top of the contents of the page (below page title) or at the top of the related ScaffoldSection , below ScaffoldTitle
-- Use sparingly
-
-### Alert example
-
-```jsx
-
-```
diff --git a/.cursor/rules/studio-useStaticEffectEvent.mdc b/.cursor/rules/studio-useStaticEffectEvent.mdc
new file mode 100644
index 00000000000..07e82b5e4fa
--- /dev/null
+++ b/.cursor/rules/studio-useStaticEffectEvent.mdc
@@ -0,0 +1,161 @@
+---
+description: Guidelines for using the useStaticEffectEvent hook in Studio - a polyfill for React's useEffectEvent pattern
+alwaysApply: false
+---
+
+# useStaticEffectEvent Hook
+
+The `useStaticEffectEvent` hook (located at `apps/studio/hooks/useStaticEffectEvent.ts`) is a userland implementation of React's `useEffectEvent` pattern. It solves the stale closure problem by providing a stable callback reference that always accesses the latest props and state values.
+
+## What Problem Does It Solve?
+
+When using `useEffect`, you often need to access props or state inside your Effect, but you don't want changes to those values to re-run the Effect. Without `useStaticEffectEvent`, you'd face two bad options:
+
+1. **Add them to dependencies** - causes unnecessary Effect re-runs (teardown/reconnect cycles)
+2. **Omit from dependencies** - causes stale closure bugs where your callback uses outdated values
+
+```tsx
+// Problem: This Effect re-runs every time `theme` changes, even though
+// we only want to reconnect when `roomId` changes
+useEffect(() => {
+ const connection = createConnection(roomId)
+ connection.on('connected', () => {
+ showNotification('Connected!', theme) // `theme` causes unwanted re-runs
+ })
+ return () => connection.disconnect()
+}, [roomId, theme]) // Adding theme causes unnecessary reconnections
+```
+
+## When to Use useStaticEffectEvent
+
+Use `useStaticEffectEvent` when you need to:
+
+1. **Read latest state/props inside an Effect without re-triggering it**
+2. **Create stable callbacks that always use current values**
+3. **Avoid stale closure bugs in event handlers used within Effects**
+
+### Pattern 1: Syncing data without re-running on every change
+
+```tsx
+// ✅ Good - sync data when status changes, but always read latest state
+const syncApiPrivileges = useStaticEffectEvent(() => {
+ if (hasLoadedInitialData.current) return
+ if (!apiAccessStatus.isSuccess) return
+ if (!privilegesForTable) return
+
+ hasLoadedInitialData.current = true
+ setPrivileges(privilegesForTable.privileges)
+})
+
+useEffect(() => {
+ syncApiPrivileges()
+}, [apiAccessStatus.status, syncApiPrivileges])
+```
+
+### Pattern 2: Stable callbacks for async operations
+
+```tsx
+// ✅ Good - wrap complex async logic that reads many values
+const exportInternal = useStaticEffectEvent(
+ async ({ bypassConfirmation }: { bypassConfirmation: boolean }): Promise => {
+ if (!params.enabled) return
+ const { projectRef, connectionString, entity, totalRows } = params
+ // ... complex async logic using latest params
+ }
+)
+
+// This callback is stable and can be safely used in useCallback
+const exportInDesiredFormat = useCallback(
+ () => exportInternal({ bypassConfirmation: false }),
+ [exportInternal]
+)
+```
+
+### Pattern 3: Infinite scroll / pagination triggers
+
+```tsx
+// ✅ Good - always read latest pagination state when scrolling triggers fetch
+const fetchNext = useStaticEffectEvent(() => {
+ if (lastItem && lastItem.index >= items.length - 1 && hasNextPage && !isFetchingNextPage) {
+ fetchNextPage()
+ }
+})
+
+useEffect(fetchNext, [lastItem, fetchNext])
+```
+
+## When NOT to Use useStaticEffectEvent
+
+### Don't use it to avoid specifying legitimate dependencies
+
+```tsx
+// ❌ Bad - hiding the fact that this should re-run when roomId changes
+const connect = useStaticEffectEvent(() => {
+ const connection = createConnection(roomId)
+ connection.connect()
+})
+
+useEffect(() => {
+ connect() // BUG: Won't reconnect when roomId changes!
+}, [connect])
+
+// ✅ Good - roomId is a legitimate dependency
+useEffect(() => {
+ const connection = createConnection(roomId)
+ connection.connect()
+ return () => connection.disconnect()
+}, [roomId])
+```
+
+### Don't use it for simple event handlers outside Effects
+
+```tsx
+// ❌ Unnecessary - not used inside an Effect
+const handleClick = useStaticEffectEvent(() => {
+ console.log(count)
+})
+
+// ✅ Good - regular function or useCallback is fine
+const handleClick = () => {
+ console.log(count)
+}
+```
+
+## How It Works
+
+The hook uses a ref to store the latest callback and returns a stable wrapper function:
+
+```tsx
+export const useStaticEffectEvent = (callback: Callback) => {
+ const callbackRef = useRef(callback)
+
+ // Update the ref on every render with the latest callback
+ useLayoutEffect(() => {
+ callbackRef.current = callback
+ })
+
+ // Return a stable function that calls the latest callback
+ const eventFn = useCallback((...args: any) => {
+ return callbackRef.current(...args)
+ }, [])
+
+ return eventFn as unknown as Callback
+}
+```
+
+## Relationship to React's useEffectEvent
+
+This hook is a polyfill for React's experimental `useEffectEvent` (now stable in React 19.2). The core concept is identical:
+
+- Extract non-reactive logic into a stable function
+- Always access the latest props/state without adding them as Effect dependencies
+- Should only be called from within Effects
+
+When React's `useEffectEvent` becomes widely available, this hook can be replaced with the official API.
+
+## Rules
+
+1. **Only call the returned function inside Effects** (useEffect, useLayoutEffect)
+2. **Don't pass the function to other components or hooks** as a callback prop
+3. **Use for non-reactive logic only** - logic that reads values but shouldn't trigger re-runs
+4. **Include it in dependency arrays** when used in useEffect (the function is stable, so it won't cause re-runs)
diff --git a/.cursor/rules/studio/RULE.md b/.cursor/rules/studio/RULE.md
new file mode 100644
index 00000000000..60e3c155802
--- /dev/null
+++ b/.cursor/rules/studio/RULE.md
@@ -0,0 +1,33 @@
+---
+description: 'Studio: index rule for architecture, style, and UI composition patterns'
+globs:
+ - apps/studio/**/*.{ts,tsx}
+alwaysApply: false
+---
+
+# Studio
+
+Use the nested rules in this folder for focused guidance while working in `apps/studio/`.
+
+## Architecture and style
+
+- `studio/project-structure`
+- `studio/component-system`
+- `studio/styling`
+- `studio/best-practices`
+
+## UI composition (Design System patterns)
+
+- `studio/layout`
+- `studio/forms`
+- `studio/tables`
+- `studio/charts`
+- `studio/empty-states`
+- `studio/navigation`
+
+## Common UI building blocks
+
+- `studio/sheets`
+- `studio/cards`
+- `studio/alerts`
+- `studio/react-query`
diff --git a/.cursor/rules/studio/alerts/RULE.md b/.cursor/rules/studio/alerts/RULE.md
new file mode 100644
index 00000000000..feb55b0564b
--- /dev/null
+++ b/.cursor/rules/studio/alerts/RULE.md
@@ -0,0 +1,13 @@
+---
+description: "Studio: alert/admonition usage and placement"
+globs:
+ - apps/studio/**/*.{ts,tsx}
+alwaysApply: false
+---
+
+# Studio alerts
+
+- Use `Admonition` to call out important actions, restrictions, or critical context.
+- Place at the top of a page’s content (below the page title) or at the top of the relevant section (below the section title).
+- Use sparingly.
+
diff --git a/.cursor/rules/studio/best-practices/RULE.md b/.cursor/rules/studio/best-practices/RULE.md
new file mode 100644
index 00000000000..357265450a8
--- /dev/null
+++ b/.cursor/rules/studio/best-practices/RULE.md
@@ -0,0 +1,433 @@
+---
+description: "Studio: React and TypeScript best practices for maintainable Studio code"
+globs:
+ - apps/studio/**/*.{ts,tsx}
+alwaysApply: false
+---
+
+# Studio Best Practices
+
+## Boolean Handling
+
+### Assign complex conditions to descriptive variables
+
+When you have multiple conditions in a single expression, extract them into well-named boolean variables. This improves readability and makes the code self-documenting.
+
+```tsx
+// ❌ Bad - complex inline condition
+{
+ !isSchemaLocked && isTableLike(selectedTable) && canUpdateColumns && !isLoading && (
+
+ )
+}
+
+// ✅ Good - extract to descriptive variables
+const isTableEntity = isTableLike(selectedTable)
+const canShowAddButton = !isSchemaLocked && isTableEntity && canUpdateColumns && !isLoading
+
+{
+ canShowAddButton &&
+}
+```
+
+### Use consistent naming conventions for booleans
+
+- Use `is` prefix for state/identity: `isLoading`, `isPaused`, `isNewRecord`, `isError`
+- Use `has` prefix for possession: `hasPermission`, `hasShownModal`, `hasData`
+- Use `can` prefix for capability/permission: `canUpdateColumns`, `canDelete`, `canEdit`
+- Use `should` prefix for conditional behavior: `shouldFetch`, `shouldRender`, `shouldValidate`
+
+```tsx
+// ✅ Good examples from codebase
+const isNewRecord = column === undefined
+const isPaused = project?.status === PROJECT_STATUS.INACTIVE
+const isMatureProject = dayjs(project?.inserted_at).isBefore(dayjs().subtract(10, 'day'))
+const { can: canUpdateColumns } = useAsyncCheckPermissions(
+ PermissionAction.TENANT_SQL_ADMIN_WRITE,
+ 'columns'
+)
+```
+
+### Derive boolean state instead of storing it
+
+When a boolean can be computed from existing state, derive it rather than storing it separately.
+
+```tsx
+// ❌ Bad - storing derived state
+const [isFormValid, setIsFormValid] = useState(false)
+
+useEffect(() => {
+ setIsFormValid(name.length > 0 && email.includes('@'))
+}, [name, email])
+
+// ✅ Good - derive from existing state
+const isFormValid = name.length > 0 && email.includes('@')
+```
+
+## Component Structure
+
+### Break down large components
+
+Components should ideally be under 200-300 lines. If a component grows larger, consider splitting it.
+
+**Signs a component should be split:**
+
+- Multiple distinct UI sections
+- Complex conditional rendering logic
+- Multiple useState hooks for unrelated state
+- Difficult to understand at a glance
+
+```tsx
+// ❌ Bad - monolithic component with everything inline
+const UserDashboard = () => {
+ // 50 lines of hooks and state
+ // 100 lines of handlers
+ // 300 lines of JSX with nested conditions
+}
+
+// ✅ Good - split into focused sub-components
+const UserDashboard = () => {
+ return (
+
+
+
+
+
+
+ )
+}
+```
+
+### Co-locate related components
+
+Place sub-components in the same directory as the parent component. Use an index file for cleaner imports.
+
+```
+components/interfaces/Auth/Users/
+├── UserPanel.tsx
+├── UserOverview.tsx
+├── UserLogs.tsx
+├── Users.constants.ts
+└── index.ts
+```
+
+### Extract repeated JSX patterns
+
+If you find yourself copying similar JSX blocks, extract them into a component.
+
+```tsx
+// ❌ Bad - repeated pattern
+
+ Overview
+
+
+ Logs
+
+
+// ✅ Good - extract to component
+const PanelTab = ({ value, children }: { value: string; children: ReactNode }) => (
+
+ {children}
+
+)
+```
+
+## Loading and Error States
+
+### Use consistent loading/error/success pattern
+
+Follow a consistent pattern for handling async states:
+
+```tsx
+const { data, error, isLoading, isError, isSuccess } = useQuery()
+
+// Handle loading state first
+if (isLoading) {
+ return
+}
+
+// Handle error state
+if (isError) {
+ return
+}
+
+// Handle empty state if needed
+if (isSuccess && data.length === 0) {
+ return
+}
+
+// Render success state
+return
+```
+
+### Use early returns for guard clauses
+
+Prefer early returns over deeply nested conditionals:
+
+```tsx
+// ❌ Bad - deeply nested
+const Component = () => {
+ if (data) {
+ if (!isError) {
+ if (hasPermission) {
+ return
+ }
+ }
+ }
+ return null
+}
+
+// ✅ Good - early returns
+const Component = () => {
+ if (!data) return null
+ if (isError) return
+ if (!hasPermission) return
+
+ return
+}
+```
+
+## State Management
+
+### Keep state as local as possible
+
+Start with local state and lift up only when needed.
+
+```tsx
+// ✅ Good - state lives where it's used
+const SearchableList = () => {
+ const [filterString, setFilterString] = useState('')
+
+ const filteredItems = items.filter((item) => item.name.includes(filterString))
+
+ return (
+
+ setFilterString(e.target.value)} />
+
+
+ )
+}
+```
+
+### Group related state with objects or reducers
+
+When you have multiple related pieces of state, consider grouping them:
+
+```tsx
+// ❌ Bad - multiple related useState calls
+const [name, setName] = useState('')
+const [email, setEmail] = useState('')
+const [phone, setPhone] = useState('')
+
+// ✅ Good - grouped state for forms (use react-hook-form)
+const form = useForm({
+ defaultValues: { name: '', email: '', phone: '' },
+})
+```
+
+## Custom Hooks
+
+### Extract complex logic into custom hooks
+
+When logic becomes reusable or complex, extract it:
+
+```tsx
+// ✅ Good - extracted to custom hook
+export function useAsyncCheckPermissions(action: string, resource: string) {
+ const { permissions, isLoading, isSuccess } = useGetProjectPermissions()
+
+ const can = useMemo(() => {
+ if (!IS_PLATFORM) return true
+ if (!isSuccess || !permissions) return false
+ return doPermissionsCheck(permissions, action, resource)
+ }, [isSuccess, permissions, action, resource])
+
+ return { isLoading, isSuccess, can }
+}
+
+// Usage
+const { can: canUpdateColumns } = useAsyncCheckPermissions(
+ PermissionAction.TENANT_SQL_ADMIN_WRITE,
+ 'columns'
+)
+```
+
+### Return objects from hooks for better extensibility
+
+```tsx
+// ❌ Bad - returning array (hard to extend)
+const useToggle = () => {
+ const [value, setValue] = useState(false)
+ return [value, () => setValue((v) => !v)]
+}
+
+// ✅ Good - returning object (easy to extend)
+const useToggle = (initial = false) => {
+ const [value, setValue] = useState(initial)
+ return {
+ value,
+ toggle: () => setValue((v) => !v),
+ setTrue: () => setValue(true),
+ setFalse: () => setValue(false),
+ }
+}
+```
+
+## Event Handlers
+
+### Name handlers consistently
+
+Use `on` prefix for prop callbacks and `handle` prefix for internal handlers:
+
+```tsx
+interface Props {
+ onClose: () => void // Callback prop
+ onSave: (data: Data) => void
+}
+
+const Component = ({ onClose, onSave }: Props) => {
+ const handleSubmit = () => {
+ // Internal handler
+ // process data
+ onSave(data)
+ }
+
+ const handleCancel = () => {
+ // cleanup
+ onClose()
+ }
+}
+```
+
+### Avoid inline arrow functions for expensive operations
+
+```tsx
+// ❌ Bad - creates new function every render
+ handleItemClick(item)} />
+
+// ✅ Good - stable reference with useCallback
+const handleItemClick = useCallback(
+ (item: Item) => {
+ // handle click
+ },
+ [dependencies]
+)
+
+
+```
+
+## Conditional Rendering
+
+### Use appropriate patterns for different scenarios
+
+```tsx
+// Simple show/hide - use &&
+{
+ isVisible &&
+}
+
+// Binary choice - use ternary
+{
+ isLoading ? :
+}
+
+// Multiple conditions - use early returns or extracted component
+const StatusDisplay = ({ status }: { status: Status }) => {
+ if (status === 'loading') return
+ if (status === 'error') return
+ if (status === 'empty') return
+ return
+}
+```
+
+### Avoid nested ternaries
+
+```tsx
+// ❌ Bad - nested ternary
+{
+ isLoading ? : isError ? :
+}
+
+// ✅ Good - separate conditions or early returns
+if (isLoading) return
+if (isError) return
+return
+```
+
+## Performance
+
+### Use useMemo for expensive computations
+
+```tsx
+// ✅ Good - memoize expensive filtering
+const filteredItems = useMemo(
+ () => items.filter((item) => item.name.toLowerCase().includes(searchQuery.toLowerCase())),
+ [items, searchQuery]
+)
+```
+
+### Avoid premature optimization
+
+Don't wrap everything in useMemo/useCallback. Only optimize when:
+
+- You have measured a performance problem
+- The computation is genuinely expensive
+- The value is passed to memoized children
+
+## TypeScript
+
+### Define prop interfaces explicitly
+
+```tsx
+interface UserCardProps {
+ user: User
+ onEdit: (user: User) => void
+ onDelete: (userId: string) => void
+ isEditable?: boolean
+}
+
+export const UserCard = ({ user, onEdit, onDelete, isEditable = true }: UserCardProps) => {
+ // ...
+}
+```
+
+### Use discriminated unions for complex state
+
+```tsx
+type AsyncState =
+ | { status: 'idle' }
+ | { status: 'loading' }
+ | { status: 'success'; data: T }
+ | { status: 'error'; error: Error }
+```
+
+### Avoid type casting, prefer validation with zod
+
+Never use type casting (e.g., `as any`, `as Type`). Instead, validate values at runtime using zod schemas. This ensures type safety and catches runtime errors.
+
+```tsx
+// ❌ Bad - type casting bypasses type checking
+const user = apiResponse as User
+const data = unknownValue as string
+
+// ✅ Good - validate with zod schema
+const userSchema = z.object({
+ id: z.string(),
+ name: z.string(),
+ email: z.string().email(),
+})
+
+const user = userSchema.parse(apiResponse)
+const data = z.string().parse(unknownValue)
+
+// ✅ Good - safe parsing with error handling
+const result = userSchema.safeParse(apiResponse)
+if (result.success) {
+ const user = result.data
+} else {
+ // handle validation errors
+}
+```
diff --git a/.cursor/rules/studio/cards/RULE.md b/.cursor/rules/studio/cards/RULE.md
new file mode 100644
index 00000000000..efcb36733b1
--- /dev/null
+++ b/.cursor/rules/studio/cards/RULE.md
@@ -0,0 +1,14 @@
+---
+description: "Studio: Card usage for grouping related content and actions"
+globs:
+ - apps/studio/**/*.{ts,tsx}
+alwaysApply: false
+---
+
+# Studio cards
+
+- Use cards to group related pieces of information.
+- Use `CardContent` for sections and `CardFooter` for actions.
+- Only use `CardHeader`/`CardTitle` when the card content is not already described by surrounding content (page title, section title, etc).
+- Prefer headers/titles when multiple cards represent distinct groups (e.g. multiple settings groups).
+
diff --git a/.cursor/rules/studio/charts/RULE.md b/.cursor/rules/studio/charts/RULE.md
new file mode 100644
index 00000000000..558580db3e8
--- /dev/null
+++ b/.cursor/rules/studio/charts/RULE.md
@@ -0,0 +1,26 @@
+---
+description: "Studio: composable chart patterns built on Recharts and our chart presentational components"
+globs:
+ - apps/studio/**/*.{ts,tsx}
+alwaysApply: false
+---
+
+# Studio charts
+
+Use the Design System UI pattern docs as the source of truth:
+
+- Documentation: `apps/design-system/content/docs/ui-patterns/charts.mdx`
+- Demos:
+ - `apps/design-system/__registry__/default/block/chart-composed-demo.tsx`
+ - `apps/design-system/__registry__/default/block/chart-composed-basic.tsx`
+ - `apps/design-system/__registry__/default/block/chart-composed-states.tsx`
+ - `apps/design-system/__registry__/default/block/chart-composed-metrics.tsx`
+ - `apps/design-system/__registry__/default/block/chart-composed-actions.tsx`
+ - `apps/design-system/__registry__/default/block/chart-composed-table.tsx`
+
+## Best practices
+
+- Prefer provided chart building blocks over passing raw Recharts components to `ChartContent`.
+- Use `useChart` context flags for consistent loading/disabled handling.
+- Keep chart composition straightforward; avoid over-abstraction.
+
diff --git a/.cursor/rules/studio/component-system/RULE.md b/.cursor/rules/studio/component-system/RULE.md
new file mode 100644
index 00000000000..b6433123d0d
--- /dev/null
+++ b/.cursor/rules/studio/component-system/RULE.md
@@ -0,0 +1,16 @@
+---
+description: 'Studio: UI component system (packages/ui + shadcn primitives)'
+globs:
+ - apps/studio/**/*.{ts,tsx}
+ - packages/ui/**/*.{ts,tsx}
+alwaysApply: false
+---
+
+# Studio component system
+
+Our primitive component system lives in `packages/ui` and is based on shadcn/ui patterns.
+
+- Prefer using components exported from `ui` (e.g. `import { Button } from 'ui'`).
+- Prefer `_Shadcn_`-suffixed components for form components e.g. `Input_Shadcn_`.
+- Avoid introducing new primitives unless explicitly requested.
+- Browse available exports in `packages/ui/index.tsx` before composing new UI.
diff --git a/.cursor/rules/studio/empty-states/RULE.md b/.cursor/rules/studio/empty-states/RULE.md
new file mode 100644
index 00000000000..266213fe9a9
--- /dev/null
+++ b/.cursor/rules/studio/empty-states/RULE.md
@@ -0,0 +1,25 @@
+---
+description: 'Studio: empty state patterns (presentational vs informational vs zero-results vs missing route)'
+globs:
+ - apps/studio/**/*.{ts,tsx}
+alwaysApply: false
+---
+
+# Studio empty states
+
+Use the Design System UI pattern docs as the source of truth:
+
+- Documentation: `apps/design-system/content/docs/ui-patterns/empty-states.mdx`
+- Demos:
+ - `apps/design-system/registry/default/example/empty-state-presentational-icon.tsx`
+ - `apps/design-system/registry/default/example/empty-state-initial-state-informational.tsx`
+ - `apps/design-system/registry/default/example/empty-state-zero-items-table.tsx`
+ - `apps/design-system/registry/default/example/data-grid-empty-state.tsx`
+ - `apps/design-system/registry/default/example/empty-state-missing-route.tsx`
+
+## Quick guidance
+
+- Initial states: use presentational empty states when onboarding/value prop + a clear next action helps.
+- Data-heavy lists: prefer informational empty states that match the list/table layout.
+- Zero results: keep the UI consistent with the data state to avoid jarring transitions.
+- Missing routes: prefer a centered `Admonition` pattern.
diff --git a/.cursor/rules/studio/forms/RULE.md b/.cursor/rules/studio/forms/RULE.md
new file mode 100644
index 00000000000..b9b729111b5
--- /dev/null
+++ b/.cursor/rules/studio/forms/RULE.md
@@ -0,0 +1,35 @@
+---
+description: "Studio: form patterns (page layouts + side panels) and react-hook-form conventions"
+globs:
+ - apps/studio/**/*.{ts,tsx}
+alwaysApply: false
+---
+
+# Studio forms
+
+Use the Design System UI pattern docs as the source of truth:
+
+- Documentation: `apps/design-system/content/docs/ui-patterns/forms.mdx`
+- Demos:
+ - `apps/design-system/registry/default/example/form-patterns-pagelayout.tsx`
+ - `apps/design-system/registry/default/example/form-patterns-sidepanel.tsx`
+
+## Requirements
+
+- Build forms with `react-hook-form` + `zod`.
+- Use `FormItemLayout` instead of manually composing `FormItem`/`FormLabel`/`FormMessage`/`FormDescription`.
+- Wrap inputs with `FormControl_Shadcn_`.
+- Use `_Shadcn_` imports from `ui` for form primitives where available.
+
+## Layout selection
+
+- Page layouts: `FormItemLayout layout="flex-row-reverse"` inside `Card` (`CardContent` per field; `CardFooter` for actions).
+- Side panels (wide): `FormItemLayout layout="horizontal"` inside `SheetSection`.
+- Side panels (narrow, `size="sm"` or below): `FormItemLayout layout="vertical"`.
+
+## Actions and state
+
+- Handle dirty state (`form.formState.isDirty`) to show Cancel and to disable Save.
+- Show loading on submit buttons via `loading`.
+- When submit button is outside the `