Merge branch 'master' into chore/data-api-integration

This commit is contained in:
Saxon Fletcher committed 2026-02-03 16:12:09 +10:00
commit 531b98ff4a
1572 files changed
+175167 -116713

No files matched your search

+7
View File
@@ -0,0 +1,7 @@
#!/bin/bash
if [ "$CLAUDE_CODE_REMOTE" != "true" ]; then
exit 0
fi
pnpm install
+15
View File
@@ -0,0 +1,15 @@
{
"hooks": {
"SessionStart": [
{
"matcher": "startup",
"hooks": [
{
"type": "command",
"command": "\"$CLAUDE_PROJECT_DIR\"/.claude/scripts/install_pkgs.sh"
}
]
}
]
}
}
+233
View File
@@ -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
<Button aria-label="Configure API privileges">
<Settings />
</Button>
```
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 <path-to-trace.zip>
```
### 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/<file>.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
-267
View File
@@ -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<string>
): Promise<Result<NewQueryModel[], ApiErrorGeneric>> {
// 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<NewQueryModel[] | GraphQLError> {
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<Result<NewQueryModel[], ApiErrorGeneric>> {
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 <queryName>.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.
-71
View File
@@ -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.
<rule>
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
</rule>
@@ -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
+132
View File
@@ -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<string>
): Promise<Result<NewQueryModel[], ApiErrorGeneric>> {
// 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
}
}
```
@@ -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.
-409
View File
@@ -1,409 +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"
- When applying focus styles for keyboard navigation, read @apps/studio/styles/focus.scss for any appropriate classes for consistency with other focus styles
## 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 (
<ScaffoldContainer>
<MyPageComponent />
</ScaffoldContainer>
)
}
MyPage.getLayout = (page) => (
<DefaultLayout>
<AuthLayout>{page}</AuthLayout>
</DefaultLayout>
)
export default MyPage
export const MyPageComponent = () => (
<ScaffoldSection isFullWidth>
<div>
<ScaffoldSectionTitle>My page section</ScaffoldSectionTitle>
<ScaffoldSectionDescription>A brief description of the purpose of the page</ScaffoldSectionDescription>
</div>
// Content goes here
</ScaffoldSection>
)
```
## Forms
Forms in Supabase Studio should follow consistent patterns to ensure a cohesive user experience across settings pages and side panels.
### Core Principles
- Build forms with `react-hook-form` + `zod`
- Always use `FormItemLayout` instead of manually composing `FormItem`, `FormLabel`, `FormMessage`, and `FormDescription`
- Always wrap form inputs with `FormControl_Shadcn_` to ensure proper form integration
- Keep imports from `ui` with `_Shadcn_` suffixes
- Handle dirty state: Show cancel buttons and disable save buttons based on `form.formState.isDirty`
- Show loading states on submit buttons using the `loading` prop
- If the submit button is outside the form, add a `formId` variable outside the component, set it as `id` on the form element and `form` prop on the button
### Layout Selection
- **Page layouts**: Use `FormItemLayout` with `layout="flex-row-reverse"` for horizontal alignment. Forms should be wrapped in a `Card` with each form field in its own `CardContent`, and `CardFooter` for actions. The layout automatically handles consistent input widths (50% on md, 40% on xl, min-w-100).
- **Side panels (wide)**: Use `FormItemLayout` with `layout="horizontal"`. Use `SheetSection` to wrap each field group.
- **Side panels (narrow, size="sm" or below)**: Use `FormItemLayout` with `layout="vertical"`
### Page Layout Form Example
```tsx
import { zodResolver } from '@hookform/resolvers/zod'
import { useForm } from 'react-hook-form'
import * as z from 'zod'
import {
Button,
Card,
CardContent,
CardFooter,
Form_Shadcn_,
FormField_Shadcn_,
FormControl_Shadcn_,
Input_Shadcn_,
Switch,
} from 'ui'
import { FormItemLayout } from 'ui-patterns/form/FormItemLayout/FormItemLayout'
const formSchema = z.object({
name: z.string().min(1, 'Name is required'),
enableFeature: z.boolean(),
})
export function SettingsForm() {
const form = useForm<z.infer<typeof formSchema>>({
resolver: zodResolver(formSchema),
defaultValues: { name: '', enableFeature: false },
mode: 'onSubmit',
reValidateMode: 'onBlur',
})
function onSubmit(values: z.infer<typeof formSchema>) {
// handle mutation with onSuccess/onError toast
}
return (
<Form_Shadcn_ {...form}>
<form onSubmit={form.handleSubmit(onSubmit)}>
<Card>
<CardContent>
<FormField_Shadcn_
control={form.control}
name="name"
render={({ field }) => (
<FormItemLayout
layout="flex-row-reverse"
label="Name"
description="A descriptive name for this resource"
>
<FormControl_Shadcn_>
<Input_Shadcn_ {...field} placeholder="Enter name" />
</FormControl_Shadcn_>
</FormItemLayout>
)}
/>
</CardContent>
<CardContent>
<FormField_Shadcn_
control={form.control}
name="enableFeature"
render={({ field }) => (
<FormItemLayout
layout="flex-row-reverse"
label="Enable Feature"
description="Toggle this feature on or off"
>
<FormControl_Shadcn_>
<Switch checked={field.value} onCheckedChange={field.onChange} />
</FormControl_Shadcn_>
</FormItemLayout>
)}
/>
</CardContent>
<CardFooter className="justify-end space-x-2">
{form.formState.isDirty && (
<Button type="default" onClick={() => form.reset()}>
Cancel
</Button>
)}
<Button type="primary" htmlType="submit" disabled={!form.formState.isDirty}>
Submit
</Button>
</CardFooter>
</Card>
</form>
</Form_Shadcn_>
)
}
```
### Side Panel Form Example
```tsx
import { zodResolver } from '@hookform/resolvers/zod'
import { useState } from 'react'
import { useForm } from 'react-hook-form'
import * as z from 'zod'
import {
Button,
Form_Shadcn_,
FormField_Shadcn_,
FormControl_Shadcn_,
Input_Shadcn_,
Sheet,
SheetContent,
SheetFooter,
SheetHeader,
SheetSection,
SheetTitle,
} from 'ui'
import { FormItemLayout } from 'ui-patterns/form/FormItemLayout/FormItemLayout'
const formSchema = z.object({
name: z.string().min(1, 'Name is required'),
})
const formId = 'sidepanel-form'
export function CreateResourcePanel() {
const [open, setOpen] = useState(false)
const form = useForm<z.infer<typeof formSchema>>({
resolver: zodResolver(formSchema),
defaultValues: { name: '' },
})
function onSubmit(values: z.infer<typeof formSchema>) {
// handle mutation
setOpen(false)
}
return (
<Sheet open={open} onOpenChange={setOpen}>
<SheetContent size="lg" className="flex flex-col gap-0">
<SheetHeader>
<SheetTitle>Create Resource</SheetTitle>
</SheetHeader>
<Form_Shadcn_ {...form}>
<form
id={formId}
onSubmit={form.handleSubmit(onSubmit)}
className="overflow-auto flex-grow px-0"
>
<SheetSection>
<FormField_Shadcn_
control={form.control}
name="name"
render={({ field }) => (
<FormItemLayout layout="horizontal" label="Name" description="A descriptive name">
<FormControl_Shadcn_ className="col-span-6 min-w-100">
<Input_Shadcn_ {...field} placeholder="Enter name" />
</FormControl_Shadcn_>
</FormItemLayout>
)}
/>
</SheetSection>
</form>
</Form_Shadcn_>
<SheetFooter>
<Button type="default" onClick={() => setOpen(false)}>
Cancel
</Button>
<Button type="primary" form={formId} htmlType="submit">
Create
</Button>
</SheetFooter>
</SheetContent>
</Sheet>
)
}
```
### Common Form Field Types
- **Text Input**: `Input_Shadcn_` with `placeholder`
- **Password Input**: `Input_Shadcn_` with `type="password"`
- **Number Input**: `Input_Shadcn_` with `type="number"` and `onChange={(e) => field.onChange(Number(e.target.value))}`
- **Input with Units**: Wrap `Input_Shadcn_` with `PrePostTab` component: `<PrePostTab postTab="MB"><Input_Shadcn_ /></PrePostTab>`
- **Textarea**: `Textarea` component with `rows` and `className="resize-none"`
- **Switch**: `Switch` with `checked={field.value} onCheckedChange={field.onChange}`
- **Checkbox**: `Checkbox_Shadcn_` with label, use multiple for checkbox groups
- **Select**: `Select_Shadcn_` with `SelectTrigger_Shadcn_`, `SelectContent_Shadcn_`, `SelectItem_Shadcn_`
- **Multi-Select**: Use `MultiSelector` from `ui-patterns/multi-select`
- **Radio Group**: `RadioGroupStacked` with `RadioGroupStackedItem` for stacked options with descriptions
- **Date Picker**: `Calendar` inside `Popover_Shadcn_` with a trigger button
- **Copyable Input**: Use `Input` from `ui-patterns/DataInputs/Input` with `copy` and `readOnly` props
- **Field Array**: Use `useFieldArray` from `react-hook-form` for dynamic add/remove fields
- **Action Field**: Use `FormItemLayout` without form control, just buttons for navigation or performable actions. Wrap buttons in a div with `justify-end` to align them to the right
## 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
- Use `SheetContent` with `size="lg"` for forms that need horizontal layout
- Use `SheetHeader`, `SheetTitle`, `SheetSection`, and `SheetFooter` for consistent structure
- Place submit/cancel buttons in `SheetFooter`
- For forms in sheets, use `FormItemLayout` with `layout="horizontal"` for wider panels or `layout="vertical"` for narrow panels (size="sm" or below)
- See the Forms section for a complete side panel form example
## React Query
- When doing a mutation, always use the mutate function. Always use onSuccess and onError with a toast.success and toast.error.
- Use mutateAsync only if the mutation is part of multiple async actions. Wrap the mutateAsync call with try/catch block and add toast.success and toast.error.
## 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'
;<Table>
<TableCaption>A list of your recent invoices.</TableCaption>
<TableHeader>
<TableRow>
<TableHead className="w-[100px]">Invoice</TableHead>
<TableHead>Status</TableHead>
<TableHead>Method</TableHead>
<TableHead className="text-right">Amount</TableHead>
</TableRow>
</TableHeader>
<TableBody>
<TableRow>
<TableCell className="font-medium">INV001</TableCell>
<TableCell>Paid</TableCell>
<TableCell>Credit Card</TableCell>
<TableCell className="text-right">$250.00</TableCell>
</TableRow>
</TableBody>
</Table>
```
## 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
<Admonition
type="note"
title="No authentication logs available for this user"
description="Auth events such as logging in will be shown here"
/>
```
@@ -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<void> => {
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 extends Function>(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)
+33
View File
@@ -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`
+13
View File
@@ -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.
@@ -1,9 +1,8 @@
---
description: React best practices and coding standards for Studio
description: "Studio: React and TypeScript best practices for maintainable Studio code"
globs:
- apps/studio/**/*.tsx
- apps/studio/**/*.ts
alwaysApply: true
- apps/studio/**/*.{ts,tsx}
alwaysApply: false
---
# Studio Best Practices
@@ -307,15 +306,15 @@ const Component = ({ onClose, onSave }: Props) => {
```tsx
// ❌ Bad - creates new function every render
<ExpensiveList
items={items}
onItemClick={(item) => handleItemClick(item)}
/>
<ExpensiveList items={items} onItemClick={(item) => handleItemClick(item)} />
// ✅ Good - stable reference with useCallback
const handleItemClick = useCallback((item: Item) => {
// handle click
}, [dependencies])
const handleItemClick = useCallback(
(item: Item) => {
// handle click
},
[dependencies]
)
<ExpensiveList items={items} onItemClick={handleItemClick} />
```
+14
View File
@@ -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).
+26
View File
@@ -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.
@@ -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.
+25
View File
@@ -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.
+35
View File
@@ -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 `<form>`, set a stable `formId` and use the button’s `form` prop.
+28
View File
@@ -0,0 +1,28 @@
---
description: 'Studio: page layout patterns (PageContainer/PageHeader/PageSection) and sizing guidance. Use to learn how to create or update existing pages in Studio.'
globs:
- apps/studio/**/*.{ts,tsx}
alwaysApply: false
---
# Studio layout
Use the Design System UI pattern docs as the source of truth:
- Documentation: `apps/design-system/content/docs/ui-patterns/layout.mdx`
- Demos:
- `apps/design-system/registry/default/example/page-layout-settings.tsx`
- `apps/design-system/registry/default/example/page-layout-list.tsx`
- `apps/design-system/registry/default/example/page-layout-list-simple.tsx`
- `apps/design-system/registry/default/example/page-layout-detail.tsx`
## Guidelines
- Build pages using `PageContainer`, `PageHeader`, and `PageSection` for consistent spacing and max-widths.
- Choose `size` based on content:
- Settings/config: `size="default"`
- List/table-heavy: `size="large"`
- Full-screen experiences: `size="full"`
- For list pages:
- If filters/search exist, align table actions with filters (avoid `PageHeaderAside`/`PageSectionAside` for those actions).
- If no filters/search, actions can go in `PageHeaderAside` or `PageSectionAside` depending on context.
+19
View File
@@ -0,0 +1,19 @@
---
description: "Studio: navigation patterns (page-level NavMenu + URL-driven navigation)"
globs:
- apps/studio/**/*.{ts,tsx}
alwaysApply: false
---
# Studio navigation
Use the Design System UI pattern docs as the source of truth:
- Documentation: `apps/design-system/content/docs/ui-patterns/navigation.mdx`
## NavMenu
- Use `NavMenu` for a horizontal list of related views within a consistent page layout.
- Activating an item should trigger a URL change (no local-only tab state).
- See: `apps/design-system/content/docs/components/nav-menu.mdx`
@@ -0,0 +1,19 @@
---
description: "Studio: project structure and where code lives"
globs:
- apps/studio/**/*.{ts,tsx}
alwaysApply: false
---
# Studio project structure
- Studio is a Next.js app using the pages router.
- Pages live in `apps/studio/pages`.
- Project pages: `apps/studio/pages/projects/[ref]`
- Org pages: `apps/studio/pages/org/[slug]`
- Studio components live in `apps/studio/components`.
- Studio UI helpers: `apps/studio/components/ui`
- Interface/page components: `apps/studio/components/interfaces` (e.g. `apps/studio/components/interfaces/Auth`)
- Shared hooks: `apps/studio/hooks`
- Shared helpers: `apps/studio/lib`
+113
View File
@@ -0,0 +1,113 @@
---
description: 'Studio: data fetching conventions for queries/mutations (React Query hooks)'
globs:
- apps/studio/data/**/*.{ts,tsx}
- apps/studio/pages/**/*.{ts,tsx}
- apps/studio/components/**/*.{ts,tsx}
alwaysApply: false
---
# Studio queries & mutations (React Query)
Follow the `apps/studio/data/` patterns used by edge functions:
- Query hook: `apps/studio/data/edge-functions/edge-functions-query.ts`
- Mutation hook: `apps/studio/data/edge-functions/edge-functions-update-mutation.ts`
- Keys: `apps/studio/data/edge-functions/keys.ts`
- Page usage: `apps/studio/pages/project/[ref]/functions/index.tsx`
## Organize query keys
- Define a `keys.ts` per domain and export `*Keys` helpers (use array keys with `as const`).
- Do not inline query keys in components.
Example:
```ts
export const edgeFunctionsKeys = {
list: (projectRef: string | undefined) => ['projects', projectRef, 'edge-functions'] as const,
detail: (projectRef: string | undefined, slug: string | undefined) =>
['projects', projectRef, 'edge-function', slug, 'detail'] as const,
}
```
## Write a query hook
- Export `Variables`, `Data`, and `Error` types from the file.
- Implement a `getX(variables, signal?)` function that:
- throws if required variables are missing
- passes the `signal` through to the fetcher for cancellation
- calls `handleError(error)` and returns `data`
- Wrap it in `useXQuery()` using `useQuery`, `UseCustomQueryOptions`, and a domain key helper.
- Gate with `enabled` so the query doesn’t run until required variables exist (and platform-only queries should include `IS_PLATFORM`).
Template:
```ts
export type XVariables = { projectRef?: string }
export type XError = ResponseError
export async function getX({ projectRef }: XVariables, signal?: AbortSignal) {
if (!projectRef) throw new Error('projectRef is required')
const { data, error } = await get('/v1/projects/{ref}/x', {
params: { path: { ref: projectRef } },
signal,
})
if (error) handleError(error)
return data
}
export type XData = Awaited<ReturnType<typeof getX>>
export const useXQuery = <TData = XData>(
{ projectRef }: XVariables,
{ enabled = true, ...options }: UseCustomQueryOptions<XData, XError, TData> = {}
) =>
useQuery<XData, XError, TData>({
queryKey: xKeys.list(projectRef),
queryFn: ({ signal }) => getX({ projectRef }, signal),
enabled: IS_PLATFORM && enabled && typeof projectRef !== 'undefined',
...options,
})
```
## Write a mutation hook
- Export a `Variables` type that includes `projectRef`, identifiers (e.g. `slug`), and `payload`.
- Implement an `updateX(vars)` function that validates required variables and uses `handleError`.
- Prefer a `useXMutation()` wrapper that:
- accepts `UseCustomMutationOptions` (omit `mutationFn`)
- invalidates the relevant `list()` + `detail()` keys in `onSuccess` and `await`s them via `Promise.all`
- defaults to a `toast.error(...)` when `onError` isn’t provided
Template:
```ts
export const useXUpdateMutation = ({ onSuccess, onError, ...options } = {}) => {
const queryClient = useQueryClient()
return useMutation({
mutationFn: updateX,
async onSuccess(data, variables, context) {
await Promise.all([
queryClient.invalidateQueries({
queryKey: xKeys.detail(variables.projectRef, variables.slug),
}),
queryClient.invalidateQueries({ queryKey: xKeys.list(variables.projectRef) }),
])
await onSuccess?.(data, variables, context)
},
async onError(error, variables, context) {
if (onError === undefined) toast.error(`Failed to update: ${error.message}`)
else onError(error, variables, context)
},
...options,
})
}
```
## Component usage
- Prefer React Query’s v5 flags:
- `isPending` for initial load (often aliased to `isLoading`)
- `isFetching` for background refetches
- Render states explicitly (pending → error → success), like `apps/studio/pages/project/[ref]/functions/index.tsx`.
+23
View File
@@ -0,0 +1,23 @@
---
description: "Studio: side panels (Sheet) for context-preserving workflows"
globs:
- apps/studio/**/*.{ts,tsx}
alwaysApply: false
---
# Studio sheets
Use a `Sheet` when switching to a new page would be disruptive and the user should keep context (e.g. selecting an item from a list to edit details).
## Structure
- Prefer `SheetContent` with `size="lg"` for forms that need horizontal layout.
- Use `SheetHeader`, `SheetTitle`, `SheetSection`, and `SheetFooter` for consistent structure.
- Place submit/cancel actions in `SheetFooter`.
## Forms in sheets
- Prefer `FormItemLayout`:
- `layout="horizontal"` for wider sheets
- `layout="vertical"` for narrow sheets (`size="sm"` or below)
- See `@studio/forms` for the canonical patterns and demos.
+16
View File
@@ -0,0 +1,16 @@
---
description: "Studio: styling rules (Tailwind + semantic tokens + typography/focus utilities)"
globs:
- apps/studio/**/*.{ts,tsx,scss}
alwaysApply: false
---
# Studio styling
- Use Tailwind.
- Do not hardcode Tailwind color tokens; use our semantic classes:
- backgrounds: `bg`, `bg-muted`, `bg-warning`, `bg-destructive`
- text: `text-foreground`, `text-foreground-light`, `text-foreground-lighter`, `text-warning`, `text-destructive`
- Use existing typography utilities from `apps/studio/styles/typography.scss` instead of recreating styles.
- Use existing focus utilities from `apps/studio/styles/focus.scss` for consistent keyboard focus styling.
+29
View File
@@ -0,0 +1,29 @@
---
description: "Studio: table patterns (Table vs Data Table vs Data Grid) and placement of actions/filters"
globs:
- apps/studio/**/*.{ts,tsx}
alwaysApply: false
---
# Studio tables
Use the Design System UI pattern docs as the source of truth:
- Documentation: `apps/design-system/content/docs/ui-patterns/tables.mdx`
- Demos:
- `apps/design-system/registry/default/example/table-demo.tsx`
- `apps/design-system/registry/default/example/data-table-demo.tsx`
- `apps/design-system/registry/default/example/data-grid-demo.tsx`
## Choose the right pattern
- `Table`: simple, static, semantic table display.
- Data Table: TanStack-powered pattern for sorting/filtering/pagination; composed per use case.
- Data Grid: only when you need virtualization, column resizing, or complex cell editing.
## Actions and filters placement
- Actions: above the table, aligned right.
- Search/filters: above the table, aligned left.
- If the table is the primary page content and has no filters/search, actions can live in the page’s primary/secondary actions area.
@@ -1,9 +1,9 @@
---
description: E2E testing best practices for Playwright tests in Studio
description: "Testing: Playwright E2E best practices for Studio tests (avoid flake + race conditions)"
globs:
- e2e/studio/**/*.ts
- e2e/studio/**/*.spec.ts
alwaysApply: true
alwaysApply: false
---
# E2E Testing Best Practices
@@ -0,0 +1,10 @@
---
description: "Testing: unit/integration conventions for Studio test files"
globs:
- apps/studio/**/*.test.ts
- apps/studio/**/*.test.tsx
alwaysApply: false
---
Follow the guidelines in `apps/studio/tests/README.md` when writing tests for Studio.
@@ -1,6 +0,0 @@
---
description:
globs: apps/studio/**/*.test.ts,apps/studio/**/*.test.tsx
alwaysApply: false
---
Make sure to follow the guidelines in this file to write tests: [README.md](mdc:apps/studio/tests/README.md)
@@ -0,0 +1,50 @@
name: Authorize Vercel Deploys
# This workflow is triggered by the validate-pr workflow. When it's triggered, it will run the
# authorize-vercel-deploys.yml in master branch. If you want to change it, you'll have to merge it into master.
on:
# only run this workflow when the validate-pr workflow completes (successfully or not)
workflow_run:
workflows: ['Validate pull request']
types: [completed]
# Cancel old builds on new commit for same workflow + branch/PR.
concurrency:
group: ${{ github.workflow }}-${{ github.event.workflow_run.head_branch || github.ref }}
cancel-in-progress: true
permissions:
contents: read
jobs:
authorize-vercel-deploys:
runs-on: blacksmith-4vcpu-ubuntu-2404
steps:
# Checkout the master branch from the supabase repo and run that script to authorize Vercel deploys
- name: Check out repo
uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0
with:
ref: master
# fetch only the root files and scripts folder
sparse-checkout: |
scripts
- uses: pnpm/action-setup@41ff72655975bd51cab0327fa583b6e92b6d3061 # v4.2.0
name: Install pnpm
with:
run_install: false
- name: Setup node
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
with:
node-version-file: '.nvmrc'
cache: 'pnpm'
- name: Download dependencies
run: |
pnpm install --frozen-lockfile
- name: Authorize Vercel Deploys
run: |-
pnpm run authorize-vercel-deploys
env:
VERCEL_TOKEN: ${{ secrets.VERCEL_TOKEN }}
# The SHA of the commit that triggered the validate-pr workflow
HEAD_COMMIT_SHA: ${{ github.event.workflow_run.head_sha }}
+13 -6
View File
@@ -54,14 +54,21 @@ jobs:
echo "Generating new typespec snapshot for review..."
npx vitest run --update ./features/docs/Reference.typeSpec.test.ts
- name: Generate token
id: app-token
uses: actions/create-github-app-token@29824e69f54612133e76f7eaac726eef6c875baf
with:
app-id: ${{ secrets.GH_AUTOFIX_APP_ID }}
private-key: ${{ secrets.GH_AUTOFIX_PRIVATE_KEY }}
- name: Create pull request
uses: peter-evans/create-pull-request@c5a7806660adbe173f04e3e038b0ccdcd758773c # v6.1.0
with:
token: ${{ secrets.GITHUB_TOKEN }}
commit-message: 'docs: update js client libraries (${{ github.event.inputs.version }})'
title: 'docs: update js client libraries (${{ github.event.inputs.version }})'
token: ${{ steps.app-token.outputs.token }}
commit-message: 'docs: update js sdk docs (${{ github.event.inputs.version }})'
title: 'docs: update js sdk docs (${{ github.event.inputs.version }})'
body: |
Updates JS client libraries documentation following stable release.
Updates JS sdk documentation following stable release.
Ran `make` in apps/docs/spec to regenerate tsdoc files.
**Details:**
@@ -69,6 +76,6 @@ jobs:
- **Source:** `${{ github.event.inputs.source }}`
- **Changes:** Regenerated tsdoc files from latest spec files
🤖 Auto-generated from supabase-js-libs stable release.
branch: 'gha/update-js-libs-docs-${{ github.run_number }}'
🤖 Auto-generated from @supabase/supabase-js stable release.
branch: 'gha/update-js-sdk-docs-${{ github.run_number }}'
base: 'master'
+8 -1
View File
@@ -39,10 +39,17 @@ jobs:
working-directory: apps/docs/spec
run: make download.api.v1 dereference.api.v1 generate.sections.api.v1 format
- name: Generate token
id: app-token
uses: actions/create-github-app-token@29824e69f54612133e76f7eaac726eef6c875baf # v2.2.1
with:
app-id: ${{ secrets.GH_AUTOFIX_APP_ID }}
private-key: ${{ secrets.GH_AUTOFIX_PRIVATE_KEY }}
- name: Create pull request
uses: peter-evans/create-pull-request@c5a7806660adbe173f04e3e038b0ccdcd758773c # v6.1.0
with:
token: ${{ secrets.GITHUB_TOKEN }}
token: ${{ steps.app-token.outputs.token }}
commit-message: 'feat: update mgmt api docs'
title: 'feat: update mgmt api docs'
body: 'This PR updates mgmt api docs automatically.'
@@ -54,3 +54,4 @@ jobs:
author: 'github-docs-sync-bot <github-docs-sync-bot@supabase.com>'
branch: 'bot/docs-sync-troubleshooting'
branch-suffix: 'random'
labels: 'documentation'
+1 -1
View File
@@ -23,7 +23,7 @@ jobs:
- name: Generate token
id: app-token
uses: actions/create-github-app-token@v2
uses: actions/create-github-app-token@29824e69f54612133e76f7eaac726eef6c875baf # v2.2.1
with:
app-id: ${{ secrets.GH_AUTOFIX_APP_ID }}
private-key: ${{ secrets.GH_AUTOFIX_PRIVATE_KEY }}
@@ -35,9 +35,16 @@ jobs:
- name: Install deps
run: pnpm install --frozen-lockfile
- name: Generate token
id: app-token
uses: actions/create-github-app-token@29824e69f54612133e76f7eaac726eef6c875baf # v2.2.1
with:
app-id: ${{ secrets.GH_AUTOFIX_APP_ID }}
private-key: ${{ secrets.GH_AUTOFIX_PRIVATE_KEY }}
- name: Decrease ESLint ratchet baselines and open PR
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
GH_TOKEN: ${{ steps.app-token.outputs.token }}
DEFAULT_BRANCH: ${{ github.event.repository.default_branch }}
run: |
set -eo pipefail
+9 -2
View File
@@ -56,11 +56,18 @@ jobs:
- name: Install dependencies
run: pnpm install --no-frozen-lockfile
- name: Generate token
id: app-token
uses: actions/create-github-app-token@29824e69f54612133e76f7eaac726eef6c875baf # v2.2.1
with:
app-id: ${{ secrets.GH_AUTOFIX_APP_ID }}
private-key: ${{ secrets.GH_AUTOFIX_PRIVATE_KEY }}
- name: Create pull request
uses: peter-evans/create-pull-request@c5a7806660adbe173f04e3e038b0ccdcd758773c # v6.1.0
with:
token: ${{ secrets.GITHUB_TOKEN }}
token: ${{ steps.app-token.outputs.token }}
commit-message: 'feat: update @supabase/*-js libraries to v${{ github.event.inputs.version }}'
title: 'feat: update @supabase/*-js libraries to v${{ github.event.inputs.version }}'
body: |
+1
View File
@@ -1,5 +1,6 @@
name: Validate pull request
# This workflow will trigger the authorize-vercel-deploys workflow when it's finished.
on:
pull_request:
types: [opened, labeled, unlabeled, synchronize, ready_for_review]
+6 -2
View File
@@ -117,7 +117,11 @@ next-env.d.ts
.vercel
# AI assistant local files
.claude/
.claude/*
!.claude/settings.json
!.claude/scripts/
!.claude/skills/
.claude/skills/me-*
CLAUDE.md
#include template .env file for docker-compose
@@ -149,4 +153,4 @@ gcloud.json
# Sentry CLI config
**/.sentryclirc
keys.json
keys.json
-16
View File
@@ -1,16 +0,0 @@
{
"trailingComma": "es5",
"tabWidth": 2,
"semi": false,
"singleQuote": true,
"printWidth": 100,
"endOfLine": "lf",
"sqlKeywordCase": "lower",
"plugins": ["prettier-plugin-sql-cst"],
"overrides": [
{
"files": "**/*.json",
"options": { "parser": "json" }
}
]
}
+1 -1
View File
@@ -35,7 +35,7 @@ To ensure a positive and inclusive environment, please read our [code of conduct
You will need to install and configure the following dependencies on your machine to build [Supabase](https://supabase.com):
- [Git](https://git-scm.com/)
- [Node.js v22.x or higher](https://nodejs.org)
- [Node.js](https://nodejs.org) version as documented in [.nvmrc](./.nvmrc)
- [pnpm](https://pnpm.io/) version 9.x.x or higher
- [make](https://www.gnu.org/software/make/) or the equivalent to `build-essentials` for your OS
- [Docker](https://docs.docker.com/get-docker/) (to run studio locally)
+3 -3
View File
@@ -60,9 +60,9 @@ With that out of the way, there are several parts of this design system that nee
- `config/docs.ts`: list of components in the sidebar
- `content/docs`: the actual component documentation
- `registry/examples.ts`: list of example components
- `registry/default/example`: the actual example components
- `registry/charts.ts`: chart components
- `registry/fragments.ts`: fragment components
- `registry/fragments.ts`: list of fragment components
- `registry/charts.ts`: list of chart components
- `registry/default/example/*`: the actual example components
You will probably need to rebuild the design system’s registry after making new additions. You can do that via:
@@ -28,16 +28,38 @@ export default function ComposedChartBasic() {
},
]
const data = Array.from({ length: 46 }, (_, i) => {
const data = Array.from({ length: 40 }, (_, i) => {
const date = new Date()
date.setMinutes(date.getMinutes() - i * 5) // Each point 5 minutes apart
date.setMinutes(date.getMinutes() - i * 3) // Each point 3 minutes apart
const progress = i / 40
const standard_score = Math.floor(55 + progress * 55 + (Math.random() - 0.5) * 12)
const performance = Math.floor(35 + progress * 35 + (Math.random() - 0.5) * 10)
const efficiency = Math.floor(25 + progress * 25 + (Math.random() - 0.5) * 12)
return {
timestamp: date.toISOString(),
standard_score: Math.floor(Math.random() * 100),
standard_score: Math.max(0, Math.min(100, standard_score)),
performance: Math.max(0, Math.min(100, performance)),
efficiency: Math.max(0, Math.min(100, efficiency)),
}
}).reverse()
const chartConfig = {
standard_score: {
label: 'Standard Score',
color: 'hsl(var(--brand-default))',
},
performance: {
label: 'Performance',
color: 'hsl(var(--chart-2))',
},
efficiency: {
label: 'Efficiency',
color: 'hsl(var(--chart-5))',
},
}
useEffect(() => {
setTimeout(() => {
setIsLoading(false)
@@ -50,10 +72,8 @@ export default function ComposedChartBasic() {
<ChartCard>
<ChartHeader>
<ChartTitle tooltip="This is a tooltip">Standard Bar Chart</ChartTitle>
<ChartActions actions={actions} />
</ChartHeader>
<ChartContent
isEmpty={data.length === 0}
emptyState={
@@ -86,10 +106,8 @@ export default function ComposedChartBasic() {
<ChartCard>
<ChartHeader>
<ChartTitle tooltip="This is a tooltip">Standard Line Chart</ChartTitle>
<ChartActions actions={actions} />
</ChartHeader>
<ChartContent
isEmpty={data.length === 0}
emptyState={
@@ -105,6 +123,8 @@ export default function ComposedChartBasic() {
<ChartLine
data={data}
dataKey="standard_score"
dataKeys={['standard_score', 'performance', 'efficiency']}
config={chartConfig}
showGrid={true}
showYAxis={true}
YAxisProps={{
+22 -11
View File
@@ -159,17 +159,6 @@ export const Index: Record<string, any> = {
subcategory: "undefined",
chunks: []
},
"aspect-ratio-demo": {
name: "aspect-ratio-demo",
type: "components:example",
registryDependencies: ["aspect-ratio"],
component: React.lazy(() => import("@/registry/default/example/aspect-ratio-demo")),
source: "",
files: ["registry/default/example/aspect-ratio-demo.tsx"],
category: "undefined",
subcategory: "undefined",
chunks: []
},
"alert-dialog-destructive": {
name: "alert-dialog-destructive",
type: "components:example",
@@ -192,6 +181,17 @@ export const Index: Record<string, any> = {
subcategory: "undefined",
chunks: []
},
"aspect-ratio-demo": {
name: "aspect-ratio-demo",
type: "components:example",
registryDependencies: ["aspect-ratio"],
component: React.lazy(() => import("@/registry/default/example/aspect-ratio-demo")),
source: "",
files: ["registry/default/example/aspect-ratio-demo.tsx"],
category: "undefined",
subcategory: "undefined",
chunks: []
},
"avatar-demo": {
name: "avatar-demo",
type: "components:example",
@@ -2238,6 +2238,17 @@ export const Index: Record<string, any> = {
subcategory: "undefined",
chunks: []
},
"page-section-title-only": {
name: "page-section-title-only",
type: "components:example",
registryDependencies: undefined,
component: React.lazy(() => import("@/registry/default/example/page-section-title-only")),
source: "",
files: ["registry/default/example/page-section-title-only.tsx"],
category: "undefined",
subcategory: "undefined",
chunks: []
},
"page-section-with-aside": {
name: "page-section-with-aside",
type: "components:example",
+104 -1
View File
@@ -1,15 +1,118 @@
import '@/styles/globals.css'
import '../../studio/styles/typography.scss'
import type { Metadata } from 'next'
import type { Metadata, Viewport } from 'next'
import { ThemeProvider } from './Providers'
import { SonnerToaster } from './SonnerToast'
import { customFont, sourceCodePro } from './fonts'
const className = `${customFont.variable} ${sourceCodePro.variable}`
const BASE_PATH = process.env.NEXT_PUBLIC_BASE_PATH || '/design-system'
const genFaviconData = (basePath: string): Metadata['icons'] => ({
icon: {
url: `${basePath}/favicon/favicon.ico`,
type: 'image/x-icon',
},
shortcut: `${basePath}/favicon/favicon.ico`,
apple: `${basePath}/favicon/favicon.ico`,
other: [
{
rel: 'apple-touch-icon-precomposed',
url: `${basePath}/favicon/apple-icon-57x57.png`,
sizes: '57x57',
},
{
rel: 'apple-touch-icon-precomposed',
url: `${basePath}/favicon/apple-icon-60x60.png`,
sizes: '60x60',
},
{
rel: 'apple-touch-icon-precomposed',
url: `${basePath}/favicon/apple-icon-72x72.png`,
sizes: '72x72',
},
{
rel: 'apple-touch-icon-precomposed',
url: `${basePath}/favicon/apple-icon-76x76.png`,
sizes: '76x76',
},
{
rel: 'apple-touch-icon-precomposed',
url: `${basePath}/favicon/apple-icon-114x114.png`,
sizes: '114x114',
},
{
rel: 'apple-touch-icon-precomposed',
url: `${basePath}/favicon/apple-icon-120x120.png`,
sizes: '120x120',
},
{
rel: 'apple-touch-icon-precomposed',
url: `${basePath}/favicon/apple-icon-144x144.png`,
sizes: '144x144',
},
{
rel: 'apple-touch-icon-precomposed',
url: `${basePath}/favicon/apple-icon-152x152.png`,
sizes: '152x152',
},
{
rel: 'icon',
url: `${basePath}/favicon/favicon-16x16.png`,
type: 'image/png',
sizes: '16x16',
},
{
rel: 'icon',
url: `${basePath}/favicon/favicon-32x32.png`,
type: 'image/png',
sizes: '32x32',
},
{
rel: 'icon',
url: `${basePath}/favicon/favicon-48x48.png`,
type: 'image/png',
sizes: '48x48',
},
{
rel: 'icon',
url: `${basePath}/favicon/favicon-96x96.png`,
type: 'image/png',
sizes: '96x96',
},
{
rel: 'icon',
url: `${basePath}/favicon/favicon-128x128.png`,
type: 'image/png',
sizes: '128x128',
},
{
rel: 'icon',
url: `${basePath}/favicon/favicon-180x180.png`,
type: 'image/png',
sizes: '180x180',
},
{
rel: 'icon',
url: `${basePath}/favicon/favicon-196x196.png`,
type: 'image/png',
sizes: '196x196',
},
],
})
export const metadata: Metadata = {
applicationName: 'Supabase Design System',
title: 'Supabase Design System',
description: 'Design resources for building consistent user experiences at Supabase.',
icons: genFaviconData(BASE_PATH),
}
export const viewport: Viewport = {
themeColor: '#1E1E1E',
}
interface RootLayoutProps {
+3 -3
View File
@@ -61,7 +61,7 @@ export const docsConfig: DocsConfig = {
items: [
{
title: 'Introduction',
href: '/docs/ui-patterns/ui-patterns',
href: '/docs/ui-patterns/introduction',
items: [],
priority: true,
},
@@ -108,7 +108,7 @@ export const docsConfig: DocsConfig = {
items: [
{
title: 'Introduction',
href: '/docs/fragments/fragment-components',
href: '/docs/fragments/introduction',
items: [],
priority: true,
},
@@ -210,7 +210,7 @@ export const docsConfig: DocsConfig = {
items: [
{
title: 'Introduction',
href: '/docs/components/atom-components',
href: '/docs/components/introduction',
items: [],
priority: true,
},
@@ -199,11 +199,13 @@ Avoid adding other actions when using row-level navigation, as multiple interact
<ComponentPreview name="table-row-link" />
When implementing row-level navigation, pay close attention to [Accessibility](/accessibility#focus-management) requirements. The row must be keyboard accessible with proper focus management, including:
When implementing row-level navigation, pay close attention to [Accessibility](/accessibility#focus-management) requirements. The row must be keyboard accessible with proper focus management. Also consider these affordances:
- Handling `Enter` and `Space` key presses for activation
- Providing visual focus indicators using classes like `inset-focus`
- Supporting modifier keys (`Ctrl`/`Cmd`) for opening links in new tabs
- Handle `Enter` and `Space` key presses for activation
- Provide visual focus indicators using classes like `inset-focus`
- Support modifier keys (`Ctrl`/`Cmd`, middle-click) for opening links in new tabs
- Consider using the shared `createNavigationHandler` function to handle modifier keys
- Avoid bubbling up action events from _within_ the row
#### Row navigation with actions
@@ -85,130 +85,44 @@ interface FilterCondition {
### Component Props
| Prop | Type | Description |
| -------------------- | ------------------------------ | ---------------------------------------- |
| filterProperties | FilterProperty[] | Array of properties that can be filtered |
| filters | FilterGroup | Current filter state |
| onFilterChange | (filters: FilterGroup) => void | Callback when filters change |
| freeformText | string | Current free-form search text |
| onFreeformTextChange | (text: string) => void | Callback when free-form text changes |
| aiApiUrl | string? | Optional URL for AI-powered filtering |
| Prop | Type | Description |
| -------------------- | ------------------------------ | ----------------------------------------------- |
| filterProperties | FilterProperty[] | Array of properties that can be filtered |
| filters | FilterGroup | Current filter state |
| onFilterChange | (filters: FilterGroup) => void | Callback when filters change |
| freeformText | string | Current free-form search text |
| onFreeformTextChange | (text: string) => void | Callback when free-form text changes |
| actions | FilterBarAction[]? | Optional custom actions to show in the menu |
| isLoading | boolean? | If true, dims the bar while work is in progress |
## AI Integration
## Custom actions (e.g. AI)
The Filter Bar component supports AI-powered filtering through an optional API endpoint. When `aiApiUrl` is provided, the component will send natural language queries to be converted into structured filters.
### API Endpoint
The AI API endpoint should accept POST requests with the following structure:
```typescript
// Request body
interface AIFilterRequest {
prompt: string // Natural language query
filterProperties: FilterProperty[] // Available filter properties
}
// Response body
interface AIFilterResponse {
logicalOperator: 'AND' | 'OR'
conditions: (FilterCondition | FilterGroup)[]
}
```
### Example API Implementation
```typescript
import { generateObject } from 'ai'
import { openai } from '@ai-sdk/openai'
import { z } from 'zod'
// Define schemas for validation
const FilterProperty = z.object({
label: z.string(),
name: z.string(),
type: z.enum(['string', 'number', 'date', 'boolean']),
options: z.array(z.string()).optional(),
operators: z.array(z.string()).optional(),
})
const FilterCondition = z.object({
propertyName: z.string(),
value: z.union([z.string(), z.number(), z.boolean(), z.null()]),
operator: z.string(),
})
type FilterGroupType = {
logicalOperator: 'AND' | 'OR'
conditions: Array<z.infer<typeof FilterCondition> | FilterGroupType>
}
const FilterGroup: z.ZodType<FilterGroupType> = z.lazy(() =>
z.object({
logicalOperator: z.enum(['AND', 'OR']),
conditions: z.array(z.union([FilterCondition, FilterGroup])),
})
)
export async function POST(req: Request) {
const { prompt, filterProperties } = await req.json()
const filterPropertiesString = JSON.stringify(filterProperties)
try {
const { object } = await generateObject({
model: openai('gpt-4-mini'),
schema: FilterGroup,
prompt: `Generate a filter group based on the following prompt: "${prompt}".
Use only these filter properties: ${filterPropertiesString}.
Each property has its own set of valid operators defined in the operators field.
Return a filter group with a logical operator ('AND'/'OR') and an array of conditions.
Each condition can be either a filter condition or another filter group.
Filter conditions should have the structure: { propertyName: string, value: string | number | boolean | null, operator: string }.
Ensure that the generated filters use only the provided property names and their corresponding operators.`,
})
// Validate that all propertyNames exist in filterProperties
const validatePropertyNames = (group: FilterGroupType): boolean => {
return group.conditions.every((condition) => {
if ('logicalOperator' in condition) {
return validatePropertyNames(condition as FilterGroupType)
}
const property = filterProperties.find(
(p: z.infer<typeof FilterProperty>) => p.name === condition.propertyName
)
if (!property) return false
// Validate operator is valid for this property
return property.operators?.includes(condition.operator) ?? false
})
}
if (!validatePropertyNames(object)) {
throw new Error('Invalid property names or operators in generated filter')
}
// Zod will throw an error if the object doesn't match the schema
const validatedFilters = FilterGroup.parse(object)
return Response.json(validatedFilters)
} catch (error: any) {
console.error('Error in AI filtering:', error)
return Response.json({ error: error.message || 'AI filtering failed' }, { status: 500 })
}
}
```
### Usage with AI
You can append custom actions to the property menu. Each action receives the current free-form input value and the active group's path so you can plug in AI, saved queries, etc.
```tsx
export function FilterDemoWithAI() {
const [filters, setFilters] = useState<FilterGroup>(initialFilters)
const actions = [
{
value: 'ai-filter',
label: 'Filter by AI',
onSelect: async (inputValue, { path }) => {
const response = await fetch('/api/filter-ai', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ prompt: inputValue, path }),
})
const group = (await response.json()) as FilterGroup
// Replace your filter state at the provided path with the returned group
setFilters((prev) => updateGroupAtPath(prev, path, group))
},
},
]
return (
<FilterBar
filterProperties={filterProperties}
filters={filters}
onFilterChange={setFilters}
aiApiUrl="/api/filter-ai" // Enable AI filtering
/>
)
}
<FilterBar
filterProperties={filterProperties}
filters={filters}
onFilterChange={setFilters}
freeformText={freeformText}
onFreeformTextChange={setFreeformText}
actions={actions}
/>
```
@@ -14,11 +14,11 @@ fragment: true
## Sub-components
- `PageSection` - Root container with orientation variants (`horizontal` or `vertical`)
- `PageSectionMeta` - Meta wrapper for summary and aside (groups summary and aside together)
- `PageSectionSummary` - Container for section title and description (should be inside PageSectionMeta, has `flex-1`)
- `PageSectionMeta` - Meta wrapper for `PageSectionSummary` and optional `PageSectionAside`
- `PageSectionSummary` - Container for section title and description (should be inside `PageSectionMeta`, has `flex-1`)
- `PageSectionTitle` - Section heading (h2)
- `PageSectionDescription` - Supporting text below section title
- `PageSectionAside` - Container for section-level actions (should be inside PageSectionMeta, has `shrink-0`)
- `PageSectionAside` - Container for section-level actions (should be inside `PageSectionMeta`, has `shrink-0`)
- `PageSectionContent` - Container for the main section content
## Orientation Variants
@@ -45,3 +45,12 @@ fragment: true
peekCode
wide
/>
### Without Aside
<ComponentPreview
name="page-section-title-only"
description="PageSection with title only"
peekCode
wide
/>
@@ -7,10 +7,10 @@ UI patterns are reusable design solutions that combine multiple components from
These patterns help ensure consistency across Supabase products by establishing standard approaches for:
- **[Charts](/docs/ui-patterns/charts)**: Visualizing data consistently using standardized chart types and styling.
- **[Empty States](/docs/ui-patterns/empty-states)**: Communicating the absence of data and guiding users toward meaningful actions.
- **[Forms](/docs/ui-patterns/forms)**: Building cohesive form experiences in both page layouts and side panels.
- **[Layout](/docs/ui-patterns/layout)**: Creating consistent page structures with proper spacing, max-widths, and content organization.
- **[Navigation](/docs/ui-patterns/navigation)**: Organizing complex hierarchical navigation systems across multiple products and contexts.
- **[Charts](charts)**: Visualizing data consistently using standardized chart types and styling.
- **[Empty States](empty-states)**: Communicating the absence of data and guiding users toward meaningful actions.
- **[Forms](forms)**: Building cohesive form experiences in both page layouts and side panels.
- **[Layout](layout)**: Creating consistent page structures with proper spacing, max-widths, and content organization.
- **[Navigation](navigation)**: Organizing complex hierarchical navigation systems across multiple products and contexts.
UI patterns may incorporate external libraries (such as `react-markdown`, `reactflow`, `recharts`, etc) or compose various components from the `ui` package. They serve as blueprints for solving recurring design problems, ensuring that similar features across the application follow the same structural and interaction patterns.
@@ -75,6 +75,6 @@ Detail pages display dense or lengthy content split into multiple sections. The
## Components
- **[PageContainer](/docs/fragments/page-container)** - Container component providing consistent max-width and padding based on size variants
- **[PageHeader](/docs/fragments/page-header)** - Compound component for building page headers with breadcrumbs, icons, titles, descriptions, actions, and navigation
- **[PageSection](/docs/fragments/page-section)** - Compound component for organizing page content into distinct sections with title, description, and action areas
- **[Page Container](../fragments/page-container)**: Container component providing consistent max-width and padding based on size variants
- **[Page Header](../fragments/page-header)**: Compound component for building page headers with breadcrumbs, icons, titles, descriptions, actions, and navigation
- **[Page Section](../fragments/page-section)**: Compound component for organizing page content into distinct sections with title, description, and action areas
Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 584 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 765 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 971 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 794 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 854 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 971 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1007 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 KiB

@@ -0,0 +1,2 @@
<?xml version="1.0" encoding="utf-8"?>
<browserconfig><msapplication><tile><square70x70logo src="/favicon/ms-icon-70x70.png"/><square150x150logo src="/favicon/ms-icon-150x150.png"/><square310x310logo src="/favicon/ms-icon-310x310.png"/><TileColor>#ffffff</TileColor></tile></msapplication></browserconfig>
Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 314 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 558 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

@@ -0,0 +1,46 @@
{
"name": "Supabase Design System",
"short_name": "Supabase Design System",
"description": "Design resources for building consistent user experiences at Supabase.",
"display": "standalone",
"theme_color": "#1C1C1C",
"background_color": "#1C1C1C",
"icons": [
{
"src": "/favicon/android-icon-36x36.png",
"sizes": "36x36",
"type": "image/png",
"density": "0.75"
},
{
"src": "/favicon/android-icon-48x48.png",
"sizes": "48x48",
"type": "image/png",
"density": "1.0"
},
{
"src": "/favicon/android-icon-72x72.png",
"sizes": "72x72",
"type": "image/png",
"density": "1.5"
},
{
"src": "/favicon/android-icon-96x96.png",
"sizes": "96x96",
"type": "image/png",
"density": "2.0"
},
{
"src": "/favicon/android-icon-144x144.png",
"sizes": "144x144",
"type": "image/png",
"density": "3.0"
},
{
"src": "/favicon/android-icon-192x192.png",
"sizes": "192x192",
"type": "image/png",
"density": "4.0"
}
]
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 955 B

@@ -28,16 +28,38 @@ export default function ComposedChartBasic() {
},
]
const data = Array.from({ length: 46 }, (_, i) => {
const data = Array.from({ length: 40 }, (_, i) => {
const date = new Date()
date.setMinutes(date.getMinutes() - i * 5) // Each point 5 minutes apart
date.setMinutes(date.getMinutes() - i * 3) // Each point 3 minutes apart
const progress = i / 40
const standard_score = Math.floor(55 + progress * 55 + (Math.random() - 0.5) * 12)
const performance = Math.floor(35 + progress * 35 + (Math.random() - 0.5) * 10)
const efficiency = Math.floor(25 + progress * 25 + (Math.random() - 0.5) * 12)
return {
timestamp: date.toISOString(),
standard_score: Math.floor(Math.random() * 100),
standard_score: Math.max(0, Math.min(100, standard_score)),
performance: Math.max(0, Math.min(100, performance)),
efficiency: Math.max(0, Math.min(100, efficiency)),
}
}).reverse()
const chartConfig = {
standard_score: {
label: 'Standard Score',
color: 'hsl(var(--brand-default))',
},
performance: {
label: 'Performance',
color: 'hsl(var(--chart-2))',
},
efficiency: {
label: 'Efficiency',
color: 'hsl(var(--chart-5))',
},
}
useEffect(() => {
setTimeout(() => {
setIsLoading(false)
@@ -50,10 +72,8 @@ export default function ComposedChartBasic() {
<ChartCard>
<ChartHeader>
<ChartTitle tooltip="This is a tooltip">Standard Bar Chart</ChartTitle>
<ChartActions actions={actions} />
</ChartHeader>
<ChartContent
isEmpty={data.length === 0}
emptyState={
@@ -86,10 +106,8 @@ export default function ComposedChartBasic() {
<ChartCard>
<ChartHeader>
<ChartTitle tooltip="This is a tooltip">Standard Line Chart</ChartTitle>
<ChartActions actions={actions} />
</ChartHeader>
<ChartContent
isEmpty={data.length === 0}
emptyState={
@@ -105,6 +123,8 @@ export default function ComposedChartBasic() {
<ChartLine
data={data}
dataKey="standard_score"
dataKeys={['standard_score', 'performance', 'efficiency']}
config={chartConfig}
showGrid={true}
showYAxis={true}
YAxisProps={{
@@ -1,10 +1,11 @@
import { format } from 'date-fns'
import { useState } from 'react'
import { Button, Button_Shadcn_, Calendar, Input_Shadcn_ } from 'ui'
import { DateRange } from 'react-day-picker'
import { Button, Calendar } from 'ui'
import { CustomOptionProps, FilterBar, FilterGroup } from 'ui-patterns'
function CustomDatePicker({ onChange, onCancel, search }: CustomOptionProps) {
const [date, setDate] = useState<any | undefined>(
const [date, setDate] = useState<DateRange | undefined>(
search
? {
from: new Date(search),
@@ -46,46 +47,18 @@ function CustomDatePicker({ onChange, onCancel, search }: CustomOptionProps) {
)
}
function CustomTimePicker({ onChange, onCancel, search }: CustomOptionProps) {
const [time, setTime] = useState(search || '')
return (
<div className="space-y-4">
<h3 className="text-lg font-medium">Select Time</h3>
<Input_Shadcn_ type="time" value={time} onChange={(e) => setTime(e.target.value)} />
<div className="flex justify-end gap-2">
<Button_Shadcn_ variant="outline" onClick={onCancel}>
Cancel
</Button_Shadcn_>
<Button_Shadcn_ onClick={() => onChange(time)}>Apply</Button_Shadcn_>
</div>
</div>
)
}
function CustomRangePicker({ onChange, onCancel, search }: CustomOptionProps) {
const [range, setRange] = useState(search || '')
return (
<div className="space-y-4">
<h3 className="text-lg font-medium">Select Range</h3>
<Input_Shadcn_ type="range" value={range} onChange={(e) => setRange(e.target.value)} />
<div className="flex justify-end gap-2">
<Button_Shadcn_ variant="outline" onClick={onCancel}>
Cancel
</Button_Shadcn_>
<Button_Shadcn_ onClick={() => onChange(range)}>Apply</Button_Shadcn_>
</div>
</div>
)
}
const filterProperties = [
{
label: 'Name',
name: 'name',
type: 'string' as const,
operators: ['=', '!=', 'CONTAINS', 'STARTS WITH', 'ENDS WITH'],
operators: [
{ value: '=', label: 'Equals' },
{ value: '!=', label: 'Not equals' },
{ value: 'CONTAINS', label: 'Contains' },
{ value: 'STARTS WITH', label: 'Starts with' },
{ value: 'ENDS WITH', label: 'Ends with' },
],
},
{
label: 'Status',
@@ -138,7 +111,6 @@ const filterProperties = [
</div>
),
},
triggerOnPropertyClick: true,
operators: ['=', '!=', '>', '<', '>=', '<='],
},
]
@@ -0,0 +1,32 @@
import { Card, CardContent } from 'ui'
import {
PageSection,
PageSectionContent,
PageSectionMeta,
PageSectionSummary,
PageSectionTitle,
} from 'ui-patterns/PageSection'
export default function PageSectionTitleOnly() {
return (
<div className="w-full">
<PageSection>
<PageSectionMeta>
<PageSectionSummary>
<PageSectionTitle>Section Title</PageSectionTitle>
</PageSectionSummary>
</PageSectionMeta>
<PageSectionContent>
<Card>
<CardContent className="p-6">
<p className="text-sm text-foreground-light">
PageSectionSummary should still be wrapped in PageSectionMeta, as the latter is a
flex container that will allow the former to span its full width.
</p>
</CardContent>
</Card>
</PageSectionContent>
</PageSection>
</div>
)
}
@@ -22,11 +22,13 @@ const policies = [
},
]
// Studio: See also createNavigationHandler in apps/studio/lib/navigation.ts
// It handles all of the below, plus modifier clicks and middle mouse button clicks.
const handlePolicyNavigation = (
bucketId: string,
policyId: string,
event: React.MouseEvent | React.KeyboardEvent
) => {
const url = `/${bucketId}`
const url = `/${policyId}`
if (event.metaKey || event.ctrlKey) {
// window.open(`${url}`, '_blank') Disabled for demo purposes
} else {
@@ -20,6 +20,8 @@ const buckets = [
},
]
// Studio: See also createNavigationHandler in apps/studio/lib/navigation.ts
// It handles all of the below, plus modifier clicks and middle mouse button clicks.
const handleBucketNavigation = (
bucketId: string,
event: React.MouseEvent | React.KeyboardEvent
+5
View File
@@ -1263,6 +1263,11 @@ export const examples: Registry = [
type: 'components:example',
files: ['example/page-section-horizontal.tsx'],
},
{
name: 'page-section-title-only',
type: 'components:example',
files: ['example/page-section-title-only.tsx'],
},
{
name: 'page-section-with-aside',
type: 'components:example',
@@ -15,7 +15,13 @@ type FeedbackModalProps = {
function FeedbackModal({ visible, page, onCancel, onSubmit }: FeedbackModalProps) {
return (
<Modal hideFooter header="Leave a comment" visible={visible} onEscapeKeyDown={onCancel}>
<Modal
hideFooter
header="Leave a comment"
visible={visible}
onCancel={onCancel}
onEscapeKeyDown={onCancel}
>
<Form
initialValues={{ page, comment: '' }}
validateOnBlur
@@ -727,6 +727,7 @@ export const auth: NavMenuConstant = {
name: 'Server-Side Rendering',
url: '/guides/auth/server-side',
items: [
{ name: 'Overview', url: '/guides/auth/server-side' },
{ name: 'Creating a client', url: '/guides/auth/server-side/creating-a-client' },
{
name: 'Migrating from Auth Helpers',
@@ -1481,6 +1482,11 @@ export const api: NavMenuConstant = {
url: '/guides/api/rest/generating-types',
items: [],
},
{
name: 'Generating Python Types',
url: '/guides/api/rest/generating-python-types',
items: [],
},
{
name: 'Tools',
url: '/guides/api',
@@ -1493,6 +1499,7 @@ export const api: NavMenuConstant = {
{ name: 'Creating API routes', url: '/guides/api/creating-routes' },
{ name: 'How API Keys work', url: '/guides/api/api-keys' },
{ name: 'Securing your API', url: '/guides/api/securing-your-api' },
{ name: 'Error Codes', url: '/guides/api/rest/postgrest-error-codes' },
],
},
{
@@ -1663,7 +1670,14 @@ export const functions: NavMenuConstant = {
name: 'Integrations',
url: undefined,
items: [
{ name: 'Supabase Auth', url: '/guides/functions/auth' },
{
name: 'Supabase Auth',
url: '/guides/functions/auth',
items: [
{ name: 'Securing your functions', url: '/guides/functions/auth' },
{ name: 'Legacy JWT secret', url: '/guides/functions/auth-legacy-jwt' },
],
},
{ name: 'Supabase Database (Postgres)', url: '/guides/functions/connect-to-postgres' },
{ name: 'Supabase Storage', url: '/guides/functions/storage-caching' },
],
@@ -1836,6 +1850,10 @@ export const realtime: NavMenuConstant = {
name: 'Guides',
url: undefined,
items: [
{
name: 'Realtime Reports',
url: '/guides/realtime/reports' as `/${string}`,
},
{
name: 'Subscribing to Database Changes',
url: '/guides/realtime/subscribing-to-database-changes' as `/${string}`,
@@ -1858,7 +1876,7 @@ export const realtime: NavMenuConstant = {
name: 'Deep dive',
url: undefined,
items: [
{ name: 'Quotas', url: '/guides/realtime/quotas', enabled: billingEnabled },
{ name: 'Limits', url: '/guides/realtime/limits', enabled: billingEnabled },
{
name: 'Pricing',
url: '/guides/realtime/pricing' as `/${string}`,
@@ -58,6 +58,7 @@ const FunctionLink = memo(function FunctionLink({
*/
onClick={(e) => {
e.preventDefault()
menuState.setMenuActiveRefId(id)
history.pushState({}, '', url)
const reduceMotion = window.matchMedia('(prefers-reduced-motion: reduce)').matches
document.getElementById(slug)?.scrollIntoView({
@@ -0,0 +1 @@
This partial has {{ .my-var }}, {{ .another_var }}, and {{ .myVar123 }}.
@@ -0,0 +1 @@
This partial has {{ .var1 }}, {{ .var2 }}, and {{ .var3 }}.
@@ -0,0 +1,3 @@
If you are on a paid plan and have [Spend Cap](/docs/guides/platform/cost-control#spend-cap) disabled or your organization is on Team Plan or above, you will pay for any overages.
When you are exceeding your quotas while being on a Free Plan or having [Spend Cap](/docs/guides/platform/cost-control#spend-cap) enabled, you will get a notification to your billing email address and put under a grace period. For more details, refer to our [Fair Use Policy](/docs/guides/platform/billing-faq#fair-use-policy).
@@ -1,5 +1,3 @@
## Pricing
<Price price="5" /> per 1,000 origin images. You are only charged for usage exceeding your subscription
plan's quota.
@@ -23,9 +23,9 @@ className="rounded-lg border border-foreground/10 bg-surface-100 text-foreground
3. Authenticate with HTTP Basic Auth:
- **Username**: `service_role`
- **Password**: a service role secret (JWT) from [**Project Settings > JWT**](/dashboard/project/_/settings/jwt) or any other Secret API key from [**Project Settings > API keys** (opens in a new tab)](/dashboard/project/_/settings/api-keys)
- **Password**: a **Secret API key** (`sb_secret_...`). You can create/copy it in [**Project Settings → API Keys**](/dashboard/project/_/settings/api-keys). For more context, see [Understanding API keys](/docs/guides/api/api-keys).
Testing locally is as simple as running `curl` with your service role secret:
Testing locally is as simple as running `curl` with your Secret API key:
```bash
curl <project-url>/customer/v1/privileged/metrics \
@@ -35,7 +35,7 @@ className="rounded-lg border border-foreground/10 bg-surface-100 text-foreground
You can provision long-lived automation tokens in two ways:
- Create an account access token once at [**Account Settings > Access Tokens**](/dashboard/account/tokens) and reuse it wherever you configure observability tooling.
- **Optional**: programmatically exchange an access token for project API keys via the [Management API ](/docs/reference/api/management-projects-api-keys-retrieve').
- **Optional**: programmatically exchange an access token for project API keys via the [Management API](/docs/reference/api/management-projects-api-keys-retrieve).
```bash
# (Optional) Exchange an account access token for project API keys
@@ -4,6 +4,10 @@ Go to [database.new](https://database.new) and create a new Supabase project.
Alternatively, you can create a project using the Management API:
</StepHikeCompact.Details>
<StepHikeCompact.Code>
```bash
# First, get your access token from https://supabase.com/dashboard/account/tokens
export SUPABASE_ACCESS_TOKEN="your-access-token"
@@ -24,9 +28,13 @@ curl -X POST https://api.supabase.com/v1/projects \
}'
```
</StepHikeCompact.Code>
<StepHikeCompact.Details>
When your project is up and running, go to the [Table Editor](/dashboard/project/_/editor), create a new table and insert some data.
Alternatively, you can run the following snippet in your project's [SQL Editor](/dashboard/project/_/sql/new). This will create a `instruments` table with some sample data.
Alternatively, you can run the following snippet in your project's [SQL Editor](/dashboard/project/_/sql/new). This will create an `instruments` table with some sample data.
</StepHikeCompact.Details>
@@ -7,6 +7,17 @@ The next step requires a callback URL, which looks like this: `https://<project-
<Admonition type="note">
#### Local development
When testing OAuth locally with the Supabase CLI, ensure your OAuth provider
is configured with the local Supabase Auth callback URL:
http://localhost:54321/auth/v1/callback
If this callback URL is missing or misconfigured, OAuth sign-in may fail or not redirect correctly during local development.
See the [local development docs](/docs/guides/local-development) for more details.
For testing OAuth locally with the Supabase CLI see the [local development docs](/docs/guides/local-development).
</Admonition>
+3 -1
View File
@@ -29,7 +29,9 @@ Prepare you database with the relevant tables:
```sql
-- Enable the pgvector extension to work with embedding vectors
create extension vector;
create extension vector
with
schema extensions;
-- Create a table to store your documents
create table documents (
@@ -72,7 +72,7 @@ In general, embeddings with fewer dimensions perform best. See our [analysis on
In this example we'll generate a vector using Transformers.js, then store it in the database using the Supabase JavaScript client.
```js
import { pipeline } from '@xenova/transformers'
import { pipeline } from '@huggingface/transformers'
const generateEmbedding = await pipeline('feature-extraction', 'Supabase/gte-small')
const title = 'First post!'
@@ -18,6 +18,8 @@ Supabase provides client libraries for the REST and Realtime APIs. Some librarie
## Community libraries
{/* supa-mdx-lint-disable Rule003Spelling */}
| `Language` | `Source Code` | `Documentation` |
| ----------------------- | -------------------------------------------------------------------------------- | ------------------------------------------- |
| C# | [supabase-csharp](https://github.com/supabase-community/supabase-csharp) | [Docs](/docs/reference/csharp/introduction) |
@@ -25,3 +27,4 @@ Supabase provides client libraries for the REST and Realtime APIs. Some librarie
| Kotlin | [supabase-kt](https://github.com/supabase-community/supabase-kt) | [Docs](/docs/reference/kotlin/introduction) |
| Ruby | [supabase-rb](https://github.com/supabase-community/supabase-rb) | |
| Godot Engine (GDScript) | [supabase-gdscript](https://github.com/supabase-community/godot-engine.supabase) | |
| Elixir | [supabase-elixir](https://github.com/supabase-community/supabase-ex) | |
@@ -0,0 +1,156 @@
---
id: 'generating-python-types'
title: 'Generating Python Types'
description: 'How to generate Python types for your API and Supabase libraries.'
subtitle: 'How to generate Python types for your API and Supabase libraries.'
---
Supabase APIs are generated from your database, which means that we can use database introspection to generate type-safe API definitions.
## Generating types using Supabase CLI
The Supabase CLI is a single binary Go application that provides everything you need to setup a local development environment.
You can [install the CLI](https://www.npmjs.com/package/supabase) via npm or other supported package managers. The minimum required version of the CLI is [v2.66.0](https://github.com/supabase/cli/releases).
```bash
npm i supabase --save-dev
```
Login with your Personal Access Token:
```bash
npx supabase login
```
Before generating types, ensure you initialize your Supabase project:
```bash
npx supabase init
```
Generate types for your project to produce the `database_types.py` file:
```bash
npx supabase gen types --lang=python --project-id "$PROJECT_REF" --schema public > database.types.py
```
or in case of local development:
```bash
npx supabase gen types --lang=python --local > database_types.py
```
These types are generated from your database schema. Given a table `public.movies`, the generated types will look like:
```sql
create table public.movies (
id bigint generated always as identity primary key,
name text not null,
data jsonb null
);
```
```py ./database_types.py
class PublicMovies(BaseModel):
data: Optional[Json[Any]] = Field(alias="data")
id: int = Field(alias="id")
name: str = Field(alias="name")
class PublicMoviesInsert(TypedDict):
data: NotRequired[Annotated[Json[Any], Field(alias="data")]]
id: NotRequired[Annotated[int, Field(alias="id")]]
name: Annotated[str, Field(alias="name")]
class PublicMoviesUpdate(TypedDict):
data: NotRequired[Annotated[Json[Any], Field(alias="data")]]
id: NotRequired[Annotated[int, Field(alias="id")]]
name: NotRequired[Annotated[str, Field(alias="name")]]
```
## Types for select, insert and update
The `PublicMovies` class is used to parse `SELECT` results from the `movies` table, while `PublicMoviesInsert` and `PublicMoviesUpdate` are used to format and provide completion for arguments for `insert` and `update` respectively.
```py
from .database_types import PublicMovies, PublicMoviesInsert, PublicMoviesUpdate
from supabase import create_client
client = create_client("YOUR_SUPABASE_URL", "YOUR_SUPABASE_KEY")
movies = client.table("movies")
movies = supabase.table("movies")
# Select
selected = [PublicMovies(m) for m in movies.select("*").execute().data]
# Insert
inserted = [PublicMovies(m) for m in movies.insert(PublicMoviesInsert(name="foo", data="bar")) \
.execute().data]
# Update
updated = [PublicMovies(m) for m in movies.update(PublicMoviesUpdate(name="bar")) \
.eq("id", 5) \
.execute().data]
```
## Update types automatically with GitHub Actions
One way to keep your type definitions in sync with your database is to set up a GitHub action that runs on a schedule.
Add the following script to your `package.json` to run it using `npm run update-types`
```json
"update-types": "npx supabase gen types --lang=python --project-id \"$PROJECT_REF\" > database_types.py"
```
Create a file `.github/workflows/update-types.yml` with the following snippet to define the action along with the environment variables. This script will commit new type changes to your repo every night.
```yaml
name: Update database types
on:
schedule:
# sets the action to run daily. You can modify this to run the action more or less frequently
- cron: '0 0 * * *'
jobs:
update:
runs-on: ubuntu-latest
permissions:
contents: write
env:
SUPABASE_ACCESS_TOKEN: ${{ secrets.ACCESS_TOKEN }}
PROJECT_REF: <your-project-id>
steps:
- uses: actions/checkout@v4
with:
persist-credentials: false
fetch-depth: 0
- uses: actions/setup-node@v4
with:
node-version: 22
- run: npm run update-types
- name: check for file changes
id: git_status
run: |
echo "status=$(git status -s)" >> $GITHUB_OUTPUT
- name: Commit files
if: ${{contains(steps.git_status.outputs.status, ' ')}}
run: |
git add database_types.py
git config --local user.email "41898282+github-actions[bot]@users.noreply.github.com"
git config --local user.name "github-actions[bot]"
git commit -m "Update database types" -a
- name: Push changes
if: ${{contains(steps.git_status.outputs.status, ' ')}}
uses: ad-m/github-push-action@master
with:
github_token: ${{ secrets.GITHUB_TOKEN }}
branch: ${{ github.ref }}
```
## Resources
- [Generating Supabase types with GitHub Actions](https://blog.esteetey.dev/how-to-create-and-test-a-github-action-that-generates-types-from-supabase-database)
- [Generating TypeScript Types](/docs/guides/api/rest/generating-types)
Loaded 100 of 1572 files, more files were not shown because too many files have changed in this diff. Show more