mirror of
https://github.com/supabase/supabase.git
synced 2026-10-11 20:35:07 +03:00
Merge branch 'master' into iat/allow-spaces-name-filter
This commit is contained in:
commit
110954a435
712 files changed
+116187
-81110
No files matched your search
Executable
+7
@@ -0,0 +1,7 @@
|
||||
#!/bin/bash
|
||||
|
||||
if [ "$CLAUDE_CODE_REMOTE" != "true" ]; then
|
||||
exit 0
|
||||
fi
|
||||
|
||||
pnpm install
|
||||
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"hooks": {
|
||||
"SessionStart": [
|
||||
{
|
||||
"matcher": "startup",
|
||||
"hooks": [
|
||||
{
|
||||
"type": "command",
|
||||
"command": "\"$CLAUDE_PROJECT_DIR\"/.claude/scripts/install_pkgs.sh"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -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.
|
||||
@@ -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>
|
||||
+15
-6
@@ -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
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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)
|
||||
@@ -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`
|
||||
@@ -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.
|
||||
|
||||
+10
-11
@@ -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} />
|
||||
```
|
||||
@@ -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).
|
||||
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
|
||||
@@ -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.
|
||||
@@ -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`
|
||||
|
||||
@@ -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`.
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
|
||||
@@ -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)
|
||||
@@ -1,13 +1,16 @@
|
||||
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:
|
||||
pull_request:
|
||||
branches:
|
||||
- 'master'
|
||||
# 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
|
||||
# Cancel old builds on new commit for same workflow + branch/PR.
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
|
||||
group: ${{ github.workflow }}-${{ github.event.workflow_run.head_branch || github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
permissions:
|
||||
@@ -18,9 +21,11 @@ jobs:
|
||||
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
|
||||
@@ -41,3 +46,5 @@ jobs:
|
||||
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 }}
|
||||
@@ -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'
|
||||
@@ -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
|
||||
|
||||
@@ -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]
|
||||
|
||||
+4
-2
@@ -117,7 +117,9 @@ next-env.d.ts
|
||||
.vercel
|
||||
|
||||
# AI assistant local files
|
||||
.claude/
|
||||
.claude/*
|
||||
!.claude/settings.json
|
||||
!.claude/scripts/
|
||||
CLAUDE.md
|
||||
|
||||
#include template .env file for docker-compose
|
||||
@@ -149,4 +151,4 @@ gcloud.json
|
||||
# Sentry CLI config
|
||||
**/.sentryclirc
|
||||
|
||||
keys.json
|
||||
keys.json
|
||||
-16
@@ -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
@@ -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)
|
||||
|
||||
@@ -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={{
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
/>
|
||||
@@ -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
|
||||
@@ -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
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -1849,6 +1849,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}`,
|
||||
@@ -1871,7 +1875,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}`,
|
||||
|
||||
@@ -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>
|
||||
@@ -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!'
|
||||
|
||||
@@ -136,9 +136,9 @@ const { data, error } = await supabase.auth.verifyOtp({ email, token, type: 'ema
|
||||
- Create your own custom email link to redirect the user to a page where they can click on a button to confirm the action
|
||||
|
||||
```html
|
||||
<a href="{{ .SiteURL }}/confirm-signup?confirmation_url={{ .ConfirmationURL }}"
|
||||
>Confirm your signup</a
|
||||
>
|
||||
<a href="{{ .SiteURL }}/confirm-signup?confirmation_url={{ .ConfirmationURL }}">
|
||||
Confirm your signup
|
||||
</a>
|
||||
```
|
||||
|
||||
- The button should contain the actual confirmation link which can be obtained from parsing the `confirmation_url={{ .ConfirmationURL }}` query parameter in the URL.
|
||||
@@ -156,7 +156,8 @@ You can customize the email link in the email template to redirect the user to a
|
||||
```html
|
||||
<a
|
||||
href="https://api.example.com/v1/authenticate?token_hash={{ .TokenHash }}&type=invite&redirect_to={{ .RedirectTo }}"
|
||||
>Accept the invite
|
||||
>
|
||||
Accept the invite
|
||||
</a>
|
||||
```
|
||||
|
||||
|
||||
@@ -20,7 +20,7 @@ This section covers the [general configuration options](/dashboard/project/_/aut
|
||||
- [Audit Logs (BETA)](/dashboard/project/_/auth/audit-logs) to track and monitor auth events in your project.
|
||||
- [Performance](/dashboard/project/_/auth/performance) to configure and optimize authentication server settings.
|
||||
|
||||
Supabase Auth provides these [general configuration options](/dashboard/project/_/settings/auth) to control user access to your application:
|
||||
Supabase Auth provides these [general configuration options](/dashboard/project/_/auth/providers) to control user access to your application:
|
||||
|
||||
- **Allow new users to sign up**: Users will be able to sign up. If this config is disabled, only existing users can sign in.
|
||||
|
||||
|
||||
@@ -200,7 +200,7 @@ export default async function ConsentPage({
|
||||
}: {
|
||||
searchParams: { authorization_id?: string }
|
||||
}) {
|
||||
const authorizationId = searchParams.authorization_id
|
||||
const authorizationId = (await searchParams).authorization_id
|
||||
|
||||
if (!authorizationId) {
|
||||
return <div>Error: Missing authorization_id</div>
|
||||
|
||||
@@ -66,6 +66,8 @@ hideToc: true
|
||||
|
||||
Create a helper file `lib/supabase.ts` that exports a Supabase client using your Project URL and key.
|
||||
|
||||
Create a `.env` file and populate with your Supabase connection variables:
|
||||
|
||||
<ProjectConfigVariables variable="url" />
|
||||
<ProjectConfigVariables variable="publishable" />
|
||||
<ProjectConfigVariables variable="anon" />
|
||||
@@ -73,41 +75,12 @@ hideToc: true
|
||||
</StepHikeCompact.Details>
|
||||
<StepHikeCompact.Code>
|
||||
|
||||
<$CodeSample
|
||||
path="/auth/quickstarts/react-native/lib/supabase.ts"
|
||||
lines={[[1, -1]]}
|
||||
meta="name=lib/supabase.ts"
|
||||
/>
|
||||
|
||||
```ts name=lib/supabase.ts
|
||||
import { AppState, Platform } from 'react-native'
|
||||
import 'react-native-url-polyfill/auto'
|
||||
import AsyncStorage from '@react-native-async-storage/async-storage'
|
||||
import { createClient, processLock } from '@supabase/supabase-js'
|
||||
|
||||
const supabaseUrl = YOUR_REACT_NATIVE_SUPABASE_URL
|
||||
const supabaseAnonKey = YOUR_REACT_NATIVE_SUPABASE_PUBLISHABLE_KEY
|
||||
|
||||
export const supabase = createClient(supabaseUrl, supabaseAnonKey, {
|
||||
auth: {
|
||||
...(Platform.OS !== "web" ? { storage: AsyncStorage } : {}),
|
||||
autoRefreshToken: true,
|
||||
persistSession: true,
|
||||
detectSessionInUrl: false,
|
||||
lock: processLock,
|
||||
},
|
||||
})
|
||||
|
||||
// Tells Supabase Auth to continuously refresh the session automatically
|
||||
// if the app is in the foreground. When this is added, you will continue
|
||||
// to receive `onAuthStateChange` events with the `TOKEN_REFRESHED` or
|
||||
// `SIGNED_OUT` event if the user's session is terminated. This should
|
||||
// only be registered once.
|
||||
if (Platform.OS !== "web") {
|
||||
AppState.addEventListener('change', (state) => {
|
||||
if (state === 'active') {
|
||||
supabase.auth.startAutoRefresh()
|
||||
} else {
|
||||
supabase.auth.stopAutoRefresh()
|
||||
}
|
||||
})
|
||||
}
|
||||
```
|
||||
<$Partial path="api_settings_steps.mdx" variables={{ "framework": "exporeactnative", "tab": "mobiles" }} />
|
||||
|
||||
</StepHikeCompact.Code>
|
||||
@@ -123,91 +96,11 @@ hideToc: true
|
||||
|
||||
<StepHikeCompact.Code>
|
||||
|
||||
```tsx name=components/Auth.tsx
|
||||
import React, { useState } from 'react'
|
||||
import { Alert, StyleSheet, View } from 'react-native'
|
||||
import { supabase } from '../lib/supabase'
|
||||
import { Button, Input } from '@rneui/themed'
|
||||
|
||||
export default function Auth() {
|
||||
const [email, setEmail] = useState('')
|
||||
const [password, setPassword] = useState('')
|
||||
const [loading, setLoading] = useState(false)
|
||||
|
||||
async function signInWithEmail() {
|
||||
setLoading(true)
|
||||
const { error } = await supabase.auth.signInWithPassword({
|
||||
email: email,
|
||||
password: password,
|
||||
})
|
||||
|
||||
if (error) Alert.alert(error.message)
|
||||
setLoading(false)
|
||||
}
|
||||
|
||||
async function signUpWithEmail() {
|
||||
setLoading(true)
|
||||
const {
|
||||
data: { session },
|
||||
error,
|
||||
} = await supabase.auth.signUp({
|
||||
email: email,
|
||||
password: password,
|
||||
})
|
||||
|
||||
if (error) Alert.alert(error.message)
|
||||
if (!session) Alert.alert('Please check your inbox for email verification!')
|
||||
setLoading(false)
|
||||
}
|
||||
|
||||
return (
|
||||
<View style={styles.container}>
|
||||
<View style={[styles.verticallySpaced, styles.mt20]}>
|
||||
<Input
|
||||
label="Email"
|
||||
leftIcon={{ type: 'font-awesome', name: 'envelope' }}
|
||||
onChangeText={(text) => setEmail(text)}
|
||||
value={email}
|
||||
placeholder="email@address.com"
|
||||
autoCapitalize={'none'}
|
||||
/>
|
||||
</View>
|
||||
<View style={styles.verticallySpaced}>
|
||||
<Input
|
||||
label="Password"
|
||||
leftIcon={{ type: 'font-awesome', name: 'lock' }}
|
||||
onChangeText={(text) => setPassword(text)}
|
||||
value={password}
|
||||
secureTextEntry={true}
|
||||
placeholder="Password"
|
||||
autoCapitalize={'none'}
|
||||
/>
|
||||
</View>
|
||||
<View style={[styles.verticallySpaced, styles.mt20]}>
|
||||
<Button title="Sign in" disabled={loading} onPress={() => signInWithEmail()} />
|
||||
</View>
|
||||
<View style={styles.verticallySpaced}>
|
||||
<Button title="Sign up" disabled={loading} onPress={() => signUpWithEmail()} />
|
||||
</View>
|
||||
</View>
|
||||
)
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: {
|
||||
marginTop: 40,
|
||||
padding: 12,
|
||||
},
|
||||
verticallySpaced: {
|
||||
paddingTop: 4,
|
||||
paddingBottom: 4,
|
||||
alignSelf: 'stretch',
|
||||
},
|
||||
mt20: {
|
||||
marginTop: 20,
|
||||
},
|
||||
})
|
||||
```
|
||||
<$CodeSample
|
||||
path="/auth/quickstarts/react-native/components/Auth.tsx"
|
||||
lines={[[1, -1]]}
|
||||
meta="name=components/Auth.tsx"
|
||||
/>
|
||||
|
||||
</StepHikeCompact.Code>
|
||||
|
||||
@@ -222,35 +115,11 @@ hideToc: true
|
||||
|
||||
<StepHikeCompact.Code>
|
||||
|
||||
```tsx name=App.tsx
|
||||
import 'react-native-url-polyfill/auto'
|
||||
import { useState, useEffect } from 'react'
|
||||
import { supabase } from './lib/supabase'
|
||||
import Auth from './components/Auth'
|
||||
import { View, Text } from 'react-native'
|
||||
import { Session } from '@supabase/supabase-js'
|
||||
|
||||
export default function App() {
|
||||
const [session, setSession] = useState<Session | null>(null)
|
||||
|
||||
useEffect(() => {
|
||||
supabase.auth.getSession().then(({ data: { session } }) => {
|
||||
setSession(session)
|
||||
})
|
||||
|
||||
supabase.auth.onAuthStateChange((_event, session) => {
|
||||
setSession(session)
|
||||
})
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<View>
|
||||
<Auth />
|
||||
{session && session.user && <Text>{session.user.id}</Text>}
|
||||
</View>
|
||||
)
|
||||
}
|
||||
```
|
||||
<$CodeSample
|
||||
path="/auth/quickstarts/react-native/App.tsx"
|
||||
lines={[[1, -1]]}
|
||||
meta="name=App.tsx"
|
||||
/>
|
||||
|
||||
</StepHikeCompact.Code>
|
||||
|
||||
|
||||
@@ -746,6 +746,6 @@ language="typescript"
|
||||
|
||||
## Next steps
|
||||
|
||||
- Implement [Authentication using Email and Password](/docs/guides/auth/server-side/email-based-auth-with-pkce-flow-for-ssr)
|
||||
- Implement [Authentication using OAuth](/docs/guides/auth/server-side/oauth-with-pkce-flow-for-ssr)
|
||||
- [Learn more about SSR](/docs/guides/auth/server-side-rendering)
|
||||
- Implement [Authentication using Email and Password](/docs/guides/auth/passwords)
|
||||
- Implement [Authentication using OAuth](/docs/guides/auth/social-login)
|
||||
- [Learn more about SSR](/docs/guides/auth/server-side/advanced-guide)
|
||||
@@ -6,7 +6,7 @@ sidebar_label: 'Migrating to SSR from Auth Helpers'
|
||||
|
||||
The new `ssr` package takes the core concepts of the Auth Helpers and makes them available to any server language or framework. This page will guide you through migrating from the Auth Helpers package to `ssr`.
|
||||
|
||||
### Replacing Supabase packages
|
||||
## Replacing Supabase packages
|
||||
|
||||
<Tabs scrollable size="small" type="underlined" defaultActiveId="nextjs" queryGroup="framework">
|
||||
|
||||
@@ -37,14 +37,14 @@ npm uninstall @supabase/auth-helpers-remix
|
||||
npm install @supabase/ssr
|
||||
```
|
||||
|
||||
### Creating a client
|
||||
## Creating a client
|
||||
|
||||
The new `ssr` package exports two functions for creating a Supabase client. The `createBrowserClient` function is used in the client, and the `createServerClient` function is used in the server.
|
||||
|
||||
Check out the [Creating a client](/docs/guides/auth/server-side/creating-a-client) page for examples of creating a client in your framework.
|
||||
Read the [Creating a client](/docs/guides/auth/server-side/creating-a-client) page for examples of creating a client in your framework [and our migration guide](/docs/guides/troubleshooting/how-to-migrate-from-supabase-auth-helpers-to-ssr-package-5NRunM).
|
||||
|
||||
## Next steps
|
||||
|
||||
- Implement [Authentication using Email and Password](/docs/guides/auth/server-side/email-based-auth-with-pkce-flow-for-ssr)
|
||||
- Implement [Authentication using OAuth](/docs/guides/auth/server-side/oauth-with-pkce-flow-for-ssr)
|
||||
- [Learn more about SSR](/docs/guides/auth/server-side-rendering)
|
||||
- Implement [Authentication using Email and Password](/docs/guides/auth/passwords)
|
||||
- Implement [Authentication using OAuth](/docs/guides/auth/social-login)
|
||||
- [Learn more about SSR](/docs/guides/auth/server-side/advanced-guide)
|
||||
@@ -31,6 +31,19 @@ Setting up OAuth with Azure consists of four broad steps:
|
||||
|
||||
## Obtain a client ID and secret
|
||||
|
||||
### Local development with Azure OAuth
|
||||
|
||||
Azure does not allow `127.0.0.1` as a redirect URI hostname and requires
|
||||
the use of `localhost`.
|
||||
|
||||
To enable Azure OAuth during local Supabase development, configure the
|
||||
Supabase API external URL in your `config.toml`:
|
||||
|
||||
```toml
|
||||
[api]
|
||||
external_url = "http://localhost:54321"
|
||||
```
|
||||
|
||||
- Once your app has been registered, the client ID can be found under the [list of app registrations](https://portal.azure.com/#blade/Microsoft_AAD_IAM/ActiveDirectoryMenuBlade/RegisteredApps) under the column titled _Application (client) ID_.
|
||||
- You can also find it in the app overview screen.
|
||||
- Place the Client ID in the Azure configuration screen in the Supabase Auth dashboard.
|
||||
|
||||
@@ -8,9 +8,10 @@ To enable Facebook Auth for your project, you need to set up a Facebook OAuth ap
|
||||
|
||||
## Overview
|
||||
|
||||
Setting up Facebook logins for your application consists of 3 parts:
|
||||
Setting up Facebook logins for your application consists of 4 parts:
|
||||
|
||||
- Create and configure a Facebook Application on the [Facebook Developers Site](https://developers.facebook.com)
|
||||
- **Configure email permissions** in your Facebook app (required for Supabase Auth)
|
||||
- Add your Facebook keys to your [Supabase Project](/dashboard)
|
||||
- Add the login code to your [Supabase JS Client App](https://github.com/supabase/supabase-js)
|
||||
|
||||
@@ -35,19 +36,39 @@ Setting up Facebook logins for your application consists of 3 parts:
|
||||
|
||||
From the `Add Products to your App` screen:
|
||||
|
||||
- Click `Setup` under `Facebook Login`
|
||||
- Skip the Quickstart screen, instead, in the left sidebar, click `Settings` under `Facebook Login`
|
||||
- Enter your callback URI under `Valid OAuth Redirect URIs` on the `Facebook Login Settings` page
|
||||
- Enter this in the `Valid OAuth Redirect URIs` box
|
||||
- Click `Save Changes` at the bottom right
|
||||
- Click **Setup** under **Facebook Login**
|
||||
- Skip the Quickstart screen. Instead, in the left sidebar, click **Settings** under **Facebook Login**
|
||||
- Enter your callback URI under **Valid OAuth Redirect URIs** on the **Facebook Login Settings** page
|
||||
- Click **Save Changes** at the bottom right
|
||||
|
||||
Be aware that you have to set the right use case permissions to enable Third party applications to read the email address. To do so:
|
||||
<Admonition type="tip">
|
||||
|
||||
Under `Build Your App`, click on `Use Cases` screen. From there, do the following steps:
|
||||
Your callback URI follows this pattern: `https://<project-ref>.supabase.co/auth/v1/callback`
|
||||
|
||||
- Click the Edit button in `Authentication and Account Creation` on the right side. This action will lead to the other page.
|
||||
- `public_profile` is set by default, so make sure it and `email` have status of **Ready for testing** in the redirected page.
|
||||
- If not, click the **Add** button in email on right side.
|
||||
You can find your project's callback URI in the [Supabase Dashboard](/dashboard/project/_/auth/providers) under **Authentication > Providers > Facebook**.
|
||||
|
||||
</Admonition>
|
||||
|
||||
## Configure email permissions (required)
|
||||
|
||||
<Admonition type="caution">
|
||||
|
||||
This step is **required** for Supabase Auth to work correctly. Without email permissions, Facebook will not return the user's email address, which may cause authentication failures or incomplete user profiles.
|
||||
|
||||
</Admonition>
|
||||
|
||||
You must configure the email permission in your Facebook app's Use Cases:
|
||||
|
||||
1. In your Facebook app dashboard, click **Use Cases** under `Build Your App`
|
||||
2. Find **Authentication and Account Creation** and click the **Edit** button on the right
|
||||
3. Verify that both `public_profile` and `email` show status **Ready for testing**
|
||||
4. If `email` is not listed, click the **Add** button next to it
|
||||
|
||||
<Admonition type="tip">
|
||||
|
||||
You can verify the permissions are set correctly by checking that both `public_profile` and `email` appear with a green check mark or "Ready for testing" status.
|
||||
|
||||
</Admonition>
|
||||
|
||||
## Copy your Facebook app ID and secret
|
||||
|
||||
@@ -102,6 +123,13 @@ async function signInWithFacebook() {
|
||||
const { data, error } = await supabase.auth.signInWithOAuth({
|
||||
provider: 'facebook',
|
||||
})
|
||||
|
||||
if (error) {
|
||||
console.error('Error signing in with Facebook:', error.message)
|
||||
return
|
||||
}
|
||||
|
||||
// The user will be redirected to Facebook for authentication
|
||||
}
|
||||
```
|
||||
|
||||
@@ -129,9 +157,15 @@ First, add the Facebook SDK dependency to your `pubspec.yaml`:
|
||||
|
||||
```yaml
|
||||
dependencies:
|
||||
flutter_facebook_auth: ^7.0.1
|
||||
flutter_facebook_auth: ^7.0.0
|
||||
```
|
||||
|
||||
<Admonition type="tip">
|
||||
|
||||
Check [pub.dev](https://pub.dev/packages/flutter_facebook_auth) for the latest version of `flutter_facebook_auth`.
|
||||
|
||||
</Admonition>
|
||||
|
||||
Then implement the Facebook authentication:
|
||||
|
||||
```dart
|
||||
@@ -177,20 +211,40 @@ Make sure to configure your Facebook app properly and add the required permissio
|
||||
When your user signs in, call [`signInWithOAuth()`](/docs/reference/swift/auth-signinwithoauth) with `facebook` as the `provider`:
|
||||
|
||||
```swift
|
||||
func signInWithFacebook() async throws {
|
||||
try await supabase.auth.signInWithOAuth(
|
||||
provider: .facebook,
|
||||
redirectTo: URL(string: "my.scheme://my-host")!, // Optionally set the redirect link to bring back the user via deeplink.
|
||||
launchFlow: { url in
|
||||
// use url to start OAuth flow
|
||||
// and return a result url that contains the OAuth token.
|
||||
// ...
|
||||
return resultURL
|
||||
import SwiftUI
|
||||
|
||||
struct SignInWithFacebook: View {
|
||||
@Environment(\.webAuthenticationSession) var webAuthenticationSession
|
||||
|
||||
var body: some View {
|
||||
Button("Sign in with Facebook") {
|
||||
Task {
|
||||
do {
|
||||
try await supabase.auth.signInWithOAuth(
|
||||
provider: .facebook,
|
||||
redirectTo: URL(string: "my.scheme://my-host")!,
|
||||
launchFlow: { @MainActor url in
|
||||
try await webAuthenticationSession.authenticate(
|
||||
using: url,
|
||||
callbackURLScheme: "my.scheme"
|
||||
)
|
||||
}
|
||||
)
|
||||
} catch {
|
||||
print("Failed to sign in with Facebook: \(error)")
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
<Admonition type="note">
|
||||
|
||||
Make sure to configure your app's URL scheme in Xcode under **Target > Info > URL Types**. The callback URL scheme should match the scheme used in `redirectTo` (e.g., `my.scheme`).
|
||||
|
||||
</Admonition>
|
||||
|
||||
</TabPanel>
|
||||
</$Show>
|
||||
<$Show if="sdk:kotlin">
|
||||
@@ -228,6 +282,13 @@ const supabase = createClient('https://your-project.supabase.co', 'sb_publishabl
|
||||
// ---cut---
|
||||
async function signOut() {
|
||||
const { error } = await supabase.auth.signOut()
|
||||
|
||||
if (error) {
|
||||
console.error('Error signing out:', error.message)
|
||||
return
|
||||
}
|
||||
|
||||
// User has been signed out
|
||||
}
|
||||
```
|
||||
|
||||
@@ -271,8 +332,89 @@ suspend fun signOut() {
|
||||
</$Show>
|
||||
</Tabs>
|
||||
|
||||
Now, you should be able to login with Facebook and alert you to `Submit for Login Review` when users try to sign into your app. Follow the instructions there to make your app go live for full features and products.
|
||||
You can read more about App Review [here](https://developers.facebook.com/docs/app-review/).
|
||||
## Testing your integration
|
||||
|
||||
Facebook apps start in **Development** mode, which has the following limitations:
|
||||
|
||||
- Only users with a role on the app (administrators, developers, testers) can authenticate
|
||||
- Other users will see an "App Not Setup" error when trying to log in
|
||||
|
||||
To add test users:
|
||||
|
||||
1. Go to [developers.facebook.com](https://developers.facebook.com) and select your app
|
||||
2. Navigate to **App Roles > Roles**
|
||||
3. Add users as Testers, Developers, or Administrators
|
||||
4. Users must accept the invitation from their Facebook notification settings
|
||||
|
||||
<Admonition type="tip">
|
||||
|
||||
Development mode is sufficient for local development and testing. You only need to submit for App Review when you're ready to allow any Facebook user to authenticate with your app.
|
||||
|
||||
</Admonition>
|
||||
|
||||
## Going live with app review
|
||||
|
||||
Before your app can be used by the general public, you need to complete Facebook's App Review process:
|
||||
|
||||
1. **Complete App Settings**: In your Facebook app's **Settings > Basic**, fill in all required fields including:
|
||||
|
||||
- App Icon
|
||||
- Privacy Policy URL
|
||||
- Terms of Service URL (if applicable)
|
||||
- App Domain
|
||||
|
||||
2. **Request Permissions**: Navigate to **App Review > Permissions and Features** and request the permissions you need:
|
||||
|
||||
- `public_profile` - Usually pre-approved
|
||||
- `email` - Requires verification that your app needs email access
|
||||
|
||||
3. **Submit for Review**: Click **Submit for Review** and provide:
|
||||
|
||||
- Detailed instructions for how Facebook reviewers should test your login flow
|
||||
- A screencast video demonstrating the Facebook Login feature
|
||||
- Explanation of how user data will be used
|
||||
|
||||
4. **Wait for Approval**: Facebook typically reviews apps within 1-5 business days
|
||||
|
||||
<Admonition type="note">
|
||||
|
||||
If you only need basic authentication (name and profile picture), you may not need full App Review. Apps requesting only `public_profile` and `email` with the "Authenticate and request data from users with Facebook Login" use case can often go live without a detailed review.
|
||||
|
||||
</Admonition>
|
||||
|
||||
For more details, see the [Facebook App Review documentation](https://developers.facebook.com/docs/app-review/).
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### "App not setup" error
|
||||
|
||||
This error occurs when a user without a role on your app tries to log in while the app is in Development mode.
|
||||
|
||||
**Solution**: Either add the user as a tester in your Facebook app settings, or complete the App Review process to make your app available to all users.
|
||||
|
||||
### User's email not returned
|
||||
|
||||
Facebook only returns the email address if:
|
||||
|
||||
- The user has a confirmed email on their Facebook account
|
||||
- Your app has been granted the `email` permission
|
||||
- The `email` permission is marked as "Ready for testing" in **Use Cases > Authentication and Account Creation**
|
||||
|
||||
**Solution**: Check that the `email` permission is properly configured in your Facebook app's Use Cases settings.
|
||||
|
||||
### "Redirect URI mismatch" error
|
||||
|
||||
This error indicates the callback URL configured in Facebook doesn't match the one used during authentication.
|
||||
|
||||
**Solution**: Verify that the **Valid OAuth Redirect URIs** in your Facebook app settings exactly matches `https://<project-ref>.supabase.co/auth/v1/callback`. Make sure there are no trailing slashes or typos.
|
||||
|
||||
### Login works in development but not production
|
||||
|
||||
If login works locally but fails in production, check:
|
||||
|
||||
- Your production URL is added to **Valid OAuth Redirect URIs** in Facebook
|
||||
- The App ID and Secret in your Supabase dashboard match your Facebook app
|
||||
- Your Facebook app is in **Live** mode (not Development mode)
|
||||
|
||||
## Resources
|
||||
|
||||
|
||||
@@ -67,7 +67,7 @@ Regardless of whether you use application code or Google's pre-built solutions t
|
||||
- Add `http://localhost:<port>` while developing locally. Remember to remove this when your application [goes into production](/docs/guides/deployment/going-into-prod).
|
||||
1. Under **Authorized redirect URIs** add your Supabase project's callback URL.
|
||||
- Access it from the [Google provider page on the Dashboard](/dashboard/project/_/auth/providers?provider=Google).
|
||||
- For local development, use `http://localhost:3000/auth/v1/callback`.
|
||||
- For local development, use `http://127.0.0.1:54321/auth/v1/callback`.
|
||||
1. Click `Create` and make sure you save the Client ID and Client Secret.
|
||||
- Add these values to the [Google provider page on the Dashboard](/dashboard/project/_/auth/providers?provider=Google).
|
||||
|
||||
@@ -196,7 +196,7 @@ To use the Google provider in local development:
|
||||
```env
|
||||
SUPABASE_AUTH_EXTERNAL_GOOGLE_CLIENT_SECRET="<client-secret>"
|
||||
```
|
||||
2. Configure the provider:
|
||||
2. Configure the provider in `supabase/config.toml`:
|
||||
```toml
|
||||
[auth.external.google]
|
||||
enabled = true
|
||||
|
||||
@@ -18,7 +18,7 @@ provider will be deprecated in future releases.
|
||||
Setting up X / Twitter logins for your application consists of 3 parts:
|
||||
|
||||
- Create and configure an X Project and App on the [X Developer Dashboard](https://developer.x.com/en/portal/dashboard).
|
||||
- Add your X `API Key` and `API Secret Key` to your [Supabase Project](/dashboard).
|
||||
- Add your X OAuth 2.0 `Client ID` and `Client Secret` to your [Supabase Project](/dashboard).
|
||||
- Add the login code to your [Supabase JS Client App](https://github.com/supabase/supabase-js).
|
||||
|
||||
## Access your X developer account
|
||||
@@ -37,8 +37,7 @@ Setting up X / Twitter logins for your application consists of 3 parts:
|
||||
- Select your use case, click `Next`.
|
||||
- Enter a description for your project, click `Next`.
|
||||
- Enter a name for your app, click `Next`.
|
||||
- Copy and save your `API Key` (this is your `client_id`).
|
||||
- Copy and save your `API Secret Key` (this is your `client_secret`).
|
||||
- Copy and save your **API Key** and **API Secret Key** (these are used for OAuth 1.0a, which is being deprecated).
|
||||
- Click on `App settings` to proceed to next steps.
|
||||
- At the bottom, you will find `User authentication settings`. Click on `Set up`.
|
||||
- Under `User authentication settings`, you can configure `App permissions`.
|
||||
@@ -50,6 +49,11 @@ Setting up X / Twitter logins for your application consists of 3 parts:
|
||||
- Enter your `Terms of service URL`.
|
||||
- Enter your `Privacy policy URL`.
|
||||
- Click `Save`.
|
||||
- After saving, navigate to `Keys and tokens` on your App page.
|
||||
- Scroll to the bottom of the page and copy your **Client ID**.
|
||||
- Click the `Regenerate` button next to **Client Secret**.
|
||||
- In the confirmation modal, click `Yes, regenerate`.
|
||||
- Copy and save your **Client Secret**.
|
||||
|
||||
## Enter your X credentials into your Supabase project
|
||||
|
||||
@@ -68,8 +72,8 @@ curl -X PATCH "https://api.supabase.com/v1/projects/$PROJECT_REF/config/auth" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"external_x_enabled": true,
|
||||
"external_x_client_id": "your-x-api-key",
|
||||
"external_x_secret": "your-x-api-secret-key"
|
||||
"external_x_client_id": "your-x-client-id",
|
||||
"external_x_secret": "your-x-client-secret"
|
||||
}'
|
||||
```
|
||||
|
||||
@@ -120,6 +124,19 @@ Future<void> signInWithX() async {
|
||||
```
|
||||
|
||||
</TabPanel>
|
||||
<$Show if="sdk:swift">
|
||||
<TabPanel id="swift" label="Swift">
|
||||
|
||||
When your user signs in, call [`signInWithOAuth(provider:)`](/docs/reference/swift/auth-signinwithoauth) with `.x` as the `provider`:
|
||||
|
||||
```swift
|
||||
func signInWithX() async throws {
|
||||
try await supabase.auth.signInWithOAuth(provider: .x)
|
||||
}
|
||||
```
|
||||
|
||||
</TabPanel>
|
||||
</$Show>
|
||||
<$Show if="sdk:kotlin">
|
||||
<TabPanel id="kotlin" label="Kotlin">
|
||||
|
||||
@@ -173,6 +190,19 @@ Future<void> signOut() async {
|
||||
```
|
||||
|
||||
</TabPanel>
|
||||
<$Show if="sdk:swift">
|
||||
<TabPanel id="swift" label="Swift">
|
||||
|
||||
When your user signs out, call [signOut()](/docs/reference/swift/auth-signout) to remove them from the browser session:
|
||||
|
||||
```swift
|
||||
func signOut() async throws {
|
||||
try await supabase.auth.signOut()
|
||||
}
|
||||
```
|
||||
|
||||
</TabPanel>
|
||||
</$Show>
|
||||
<$Show if="sdk:kotlin">
|
||||
<TabPanel id="kotlin" label="Kotlin">
|
||||
|
||||
|
||||
@@ -10,9 +10,9 @@ How you connect to your database depends on where you're connecting from:
|
||||
|
||||
- For frontend applications, use the [Data API](#data-apis-and-client-libraries)
|
||||
- For Postgres clients, use a connection string
|
||||
- For single sessions (for example, database GUIs) or Postgres native commands (for example, using client applications like [pg_dump](https://www.postgresql.org/docs/current/app-pgdump.html) or specifying connections for [replication](/docs/guides/database/postgres/setup-replication-external)) use the [direct connection string](#direct-connection) if your environment supports IPv6
|
||||
- For persistent clients, and support for both IPv4 and IPv6, use [pooler session mode](#pooler-session-mode)
|
||||
- For temporary clients (for example, serverless or edge functions) use [pooler transaction mode](#pooler-transaction-mode)
|
||||
- For single sessions (for example, database GUIs) or Postgres native commands (for example, using client applications like [pg_dump](https://www.postgresql.org/docs/current/app-pgdump.html), [migrations](/docs/guides/deployment/database-migrations), [backup-restore](/docs/guides/platform/migrating-within-supabase/backup-restore), or specifying connections for [replication](/docs/guides/database/postgres/setup-replication-external)) use the [direct connection string](#direct-connection) if your environment supports IPv6. IPv4 available as [Add-on](/docs/guides/platform/ipv4-address).
|
||||
- For application traffic from persistent clients, and support for both IPv4 and IPv6, use [pooler session mode](#pooler-session-mode)
|
||||
- For application traffic from temporary clients (for example, serverless or edge functions) use [pooler transaction mode](#pooler-transaction-mode)
|
||||
|
||||
## Quickstarts
|
||||
|
||||
@@ -279,17 +279,20 @@ Because the dedicated pooler is hosted on the same machine as your database, it
|
||||
**Direct connection:**
|
||||
|
||||
- Best for: persistent backend services
|
||||
- Limitation: IPv6 only
|
||||
- Use for migrations, pg_dump, backup and management tools
|
||||
- Limitation: IPv6 only by default. IPv4 available as [Add-on](/docs/guides/platform/ipv4-address).
|
||||
|
||||
**Shared pooler:**
|
||||
|
||||
- Best for: general-purpose connections (supports IPv4 and IPv6)
|
||||
- Supavisor session mode → persistent backend that require IPv4
|
||||
- Supavisor transaction mode → serverless functions or short-lived tasks
|
||||
- Use for application runtime traffic (queries, writes)
|
||||
|
||||
**Dedicated pooler (paid tier):**
|
||||
|
||||
- Best for: high-performance apps that need dedicated resources
|
||||
- Use for application runtime traffic (queries, writes)
|
||||
- Uses PgBouncer
|
||||
|
||||
You can follow the decision flow in the connection method diagram to quickly choose the right option for your environment.
|
||||
|
||||
@@ -109,31 +109,41 @@ If a setting you need is not yet configurable, [share your use case with us](/da
|
||||
|
||||
The following parameters are available for overrides:
|
||||
|
||||
1. [checkpoint_timeout](https://www.postgresql.org/docs/current/runtime-config-wal.html#GUC-CHECKPOINT-TIMEOUT)
|
||||
2. [effective_cache_size](https://www.postgresql.org/docs/current/runtime-config-query.html#GUC-EFFECTIVE-CACHE-SIZE)
|
||||
3. [hot_standby_feedback](https://www.postgresql.org/docs/current/runtime-config-replication.html#GUC-HOT-STANDBY-FEEDBACK)
|
||||
4. [logical_decoding_work_mem](https://www.postgresql.org/docs/current/runtime-config-resource.html#GUC-LOGICAL-DECODING-WORK-MEM) (CLI only)
|
||||
5. [maintenance_work_mem](https://www.postgresql.org/docs/current/runtime-config-resource.html#GUC-MAINTENANCE-WORK-MEM)
|
||||
6. [max_connections](https://www.postgresql.org/docs/current/runtime-config-connection.html#GUC-MAX-CONNECTIONS) (CLI only. Be aware of [these considerations](/docs/guides/troubleshooting/how-to-change-max-database-connections-_BQ8P5) before modifying)
|
||||
7. [max_locks_per_transaction](https://www.postgresql.org/docs/current/runtime-config-locks.html#GUC-MAX-LOCKS-PER-TRANSACTION) (CLI only)
|
||||
8. [max_parallel_maintenance_workers](https://www.postgresql.org/docs/current/runtime-config-resource.html#GUC-MAX-PARALLEL-MAINTENANCE-WORKERS)
|
||||
9. [max_parallel_workers_per_gather](https://www.postgresql.org/docs/current/runtime-config-resource.html#GUC-MAX-PARALLEL-WORKERS-PER-GATHER)
|
||||
10. [max_parallel_workers](https://www.postgresql.org/docs/current/runtime-config-resource.html#GUC-MAX-PARALLEL-WORKERS)
|
||||
11. [max_replication_slots](https://www.postgresql.org/docs/current/runtime-config-replication.html#GUC-MAX-REPLICATION-SLOTS) (CLI only)
|
||||
12. [max_slot_wal_keep_size](https://www.postgresql.org/docs/current/runtime-config-replication.html#GUC-MAX-SLOT-WAL-KEEP-SIZE) (CLI only)
|
||||
13. [max_standby_archive_delay](https://www.postgresql.org/docs/current/runtime-config-replication.html#GUC-MAX-STANDBY-ARCHIVE-DELAY) (CLI only)
|
||||
14. [max_standby_streaming_delay](https://www.postgresql.org/docs/current/runtime-config-replication.html#GUC-MAX-STANDBY-STREAMING-DELAY) (CLI only)
|
||||
15. [max_wal_size](https://www.postgresql.org/docs/current/runtime-config-wal.html#GUC-MAX-WAL-SIZE) (CLI only)
|
||||
16. [max_wal_senders](https://www.postgresql.org/docs/current/runtime-config-replication.html#GUC-MAX-WAL-SENDERS) (CLI only)
|
||||
17. [max_worker_processes](https://www.postgresql.org/docs/current/runtime-config-resource.html#GUC-MAX-WORKER-PROCESSES) (CLI only)
|
||||
18. [session_replication_role](https://www.postgresql.org/docs/current/runtime-config-client.html#GUC-SESSION-REPLICATION-ROLE)
|
||||
19. [shared_buffers](https://www.postgresql.org/docs/current/runtime-config-resource.html#GUC-SHARED-BUFFERS) (CLI only)
|
||||
20. [statement_timeout](https://www.postgresql.org/docs/current/runtime-config-client.html#GUC-STATEMENT-TIMEOUT)
|
||||
21. [track_activity_query_size](https://www.postgresql.org/docs/current/runtime-config-statistics.html#GUC-TRACK-ACTIVITY-QUERY-SIZE)
|
||||
22. [track_commit_timestamp](https://www.postgresql.org/docs/current/runtime-config-replication.html#GUC-TRACK-COMMIT-TIMESTAMP)
|
||||
23. [wal_keep_size](https://www.postgresql.org/docs/current/runtime-config-replication.html#GUC-WAL-KEEP-SIZE) (CLI only)
|
||||
24. [wal_sender_timeout](https://www.postgresql.org/docs/current/runtime-config-replication.html#GUC-WAL-SENDER-TIMEOUT) (CLI only)
|
||||
25. [work_mem](https://www.postgresql.org/docs/current/runtime-config-resource.html#GUC-WORK-MEM)
|
||||
<Admonition type="note">
|
||||
|
||||
Parameters marked with **Restart: Yes** cause the CLI to automatically restart your database (and any read replicas) to apply the change. This may cause a brief interruption to active connections. You can use the [`--no-restart`](#managing-postgres-configuration-with-the-cli) flag to defer the restart.
|
||||
|
||||
</Admonition>
|
||||
|
||||
Use the examples below with `supabase --experimental --project-ref <project-ref> postgres-config update`:
|
||||
|
||||
| Parameter | Type | Restart | Example |
|
||||
| ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | --------- | ------- | --------------------------------------------- |
|
||||
| [checkpoint_timeout](https://www.postgresql.org/docs/current/runtime-config-wal.html#GUC-CHECKPOINT-TIMEOUT) | CLI only | No | `--config checkpoint_timeout=15min` |
|
||||
| [effective_cache_size](https://www.postgresql.org/docs/current/runtime-config-query.html#GUC-EFFECTIVE-CACHE-SIZE) | CLI + SQL | No | `--config effective_cache_size=8GB` |
|
||||
| [hot_standby_feedback](https://www.postgresql.org/docs/current/runtime-config-replication.html#GUC-HOT-STANDBY-FEEDBACK) | CLI only | No | `--config hot_standby_feedback=true` |
|
||||
| [logical_decoding_work_mem](https://www.postgresql.org/docs/current/runtime-config-resource.html#GUC-LOGICAL-DECODING-WORK-MEM) | CLI + SQL | No | `--config logical_decoding_work_mem=128MB` |
|
||||
| [maintenance_work_mem](https://www.postgresql.org/docs/current/runtime-config-resource.html#GUC-MAINTENANCE-WORK-MEM) | CLI + SQL | No | `--config maintenance_work_mem=512MB` |
|
||||
| [max_connections](https://www.postgresql.org/docs/current/runtime-config-connection.html#GUC-MAX-CONNECTIONS) (Be aware of [these considerations](/docs/guides/troubleshooting/how-to-change-max-database-connections-_BQ8P5)) | CLI only | Yes | `--config max_connections=200` |
|
||||
| [max_locks_per_transaction](https://www.postgresql.org/docs/current/runtime-config-locks.html#GUC-MAX-LOCKS-PER-TRANSACTION) | CLI only | Yes | `--config max_locks_per_transaction=128` |
|
||||
| [max_parallel_maintenance_workers](https://www.postgresql.org/docs/current/runtime-config-resource.html#GUC-MAX-PARALLEL-MAINTENANCE-WORKERS) | CLI + SQL | No | `--config max_parallel_maintenance_workers=2` |
|
||||
| [max_parallel_workers_per_gather](https://www.postgresql.org/docs/current/runtime-config-resource.html#GUC-MAX-PARALLEL-WORKERS-PER-GATHER) | CLI + SQL | No | `--config max_parallel_workers_per_gather=2` |
|
||||
| [max_parallel_workers](https://www.postgresql.org/docs/current/runtime-config-resource.html#GUC-MAX-PARALLEL-WORKERS) | CLI + SQL | No | `--config max_parallel_workers=4` |
|
||||
| [max_replication_slots](https://www.postgresql.org/docs/current/runtime-config-replication.html#GUC-MAX-REPLICATION-SLOTS) | CLI only | Yes | `--config max_replication_slots=10` |
|
||||
| [max_slot_wal_keep_size](https://www.postgresql.org/docs/current/runtime-config-replication.html#GUC-MAX-SLOT-WAL-KEEP-SIZE) | CLI only | No | `--config max_slot_wal_keep_size=4GB` |
|
||||
| [max_standby_archive_delay](https://www.postgresql.org/docs/current/runtime-config-replication.html#GUC-MAX-STANDBY-ARCHIVE-DELAY) | CLI only | No | `--config max_standby_archive_delay=30s` |
|
||||
| [max_standby_streaming_delay](https://www.postgresql.org/docs/current/runtime-config-replication.html#GUC-MAX-STANDBY-STREAMING-DELAY) | CLI only | No | `--config max_standby_streaming_delay=30s` |
|
||||
| [max_wal_size](https://www.postgresql.org/docs/current/runtime-config-wal.html#GUC-MAX-WAL-SIZE) | CLI only | No | `--config max_wal_size=2GB` |
|
||||
| [max_wal_senders](https://www.postgresql.org/docs/current/runtime-config-replication.html#GUC-MAX-WAL-SENDERS) | CLI only | Yes | `--config max_wal_senders=10` |
|
||||
| [max_worker_processes](https://www.postgresql.org/docs/current/runtime-config-resource.html#GUC-MAX-WORKER-PROCESSES) | CLI only | Yes | `--config max_worker_processes=8` |
|
||||
| [session_replication_role](https://www.postgresql.org/docs/current/runtime-config-client.html#GUC-SESSION-REPLICATION-ROLE) | CLI only | No | `--config session_replication_role=replica` |
|
||||
| [shared_buffers](https://www.postgresql.org/docs/current/runtime-config-resource.html#GUC-SHARED-BUFFERS) | CLI only | Yes | `--config shared_buffers=256MB` |
|
||||
| [statement_timeout](https://www.postgresql.org/docs/current/runtime-config-client.html#GUC-STATEMENT-TIMEOUT) | CLI + SQL | No | `--config statement_timeout=60s` |
|
||||
| [track_activity_query_size](https://www.postgresql.org/docs/current/runtime-config-statistics.html#GUC-TRACK-ACTIVITY-QUERY-SIZE) | CLI only | Yes | `--config track_activity_query_size=2048B` |
|
||||
| [track_commit_timestamp](https://www.postgresql.org/docs/current/runtime-config-replication.html#GUC-TRACK-COMMIT-TIMESTAMP) | CLI only | Yes | `--config track_commit_timestamp=true` |
|
||||
| [wal_keep_size](https://www.postgresql.org/docs/current/runtime-config-replication.html#GUC-WAL-KEEP-SIZE) | CLI only | No | `--config wal_keep_size=1GB` |
|
||||
| [wal_sender_timeout](https://www.postgresql.org/docs/current/runtime-config-replication.html#GUC-WAL-SENDER-TIMEOUT) | CLI only | No | `--config wal_sender_timeout=60s` |
|
||||
| [work_mem](https://www.postgresql.org/docs/current/runtime-config-resource.html#GUC-WORK-MEM) | CLI + SQL | No | `--config work_mem=64MB` |
|
||||
|
||||
#### Managing Postgres configuration with the CLI
|
||||
|
||||
|
||||
@@ -4,7 +4,11 @@ title: 'pgjwt: JSON Web Tokens'
|
||||
description: 'Encode and decode JWTs in PostgreSQL'
|
||||
---
|
||||
|
||||
{/* supa-mdx-lint-disable-next-line Rule004ExcludeWords */}
|
||||
<Admonition type="note">
|
||||
|
||||
Supabase creates and handles JWT for you. It is built into the platform. **If you use Postgres version 15 or earlier**, you don't need the pgjwt extension, and it is safe to disable. For more information on how Supabase handles JWTs, read the [Supabase and JWTs documentation](/docs/guides/auth/jwts#supabase-and-jwts)
|
||||
|
||||
</Admonition>
|
||||
|
||||
<Admonition type="deprecation">
|
||||
|
||||
|
||||
@@ -21,10 +21,12 @@ We highly recommend creating a `private` schema for storing tables that you do n
|
||||
|
||||
If your `public` schema is used by other tools as a default space, you might want to lock down this schema. This helps prevent accidental exposure of data that's automatically added to `public`.
|
||||
|
||||
There are two levels of security hardening for the Data API:
|
||||
There are several levels of security hardening for the Data API:
|
||||
|
||||
- Disabling the Data API entirely. This is recommended if you _never_ need to access your database via Supabase client libraries or the REST and GraphQL endpoints.
|
||||
- Removing the `public` schema from the Data API and replacing it with a custom schema (such as `api`).
|
||||
- [Disabling the Data API entirely](#disabling-the-data-api). This is recommended if you _never_ need to access your database via Supabase client libraries or the REST and GraphQL endpoints.
|
||||
- [Exposing a custom schema](#exposing-a-custom-schema-instead-of-public) instead of `public`, giving you explicit control over what is accessible.
|
||||
- [Automatically enabling RLS on new tables](#automatically-enabling-rls-on-new-tables) using an event trigger.
|
||||
- [Adjusting table-level grants](#table-level-grants) to control which roles can access specific tables.
|
||||
|
||||
## Disabling the Data API
|
||||
|
||||
@@ -72,3 +74,64 @@ Any data, views, or functions that should be exposed need to be deliberately put
|
||||
grant select on table api.<your_table> to anon;
|
||||
grant select, insert, update, delete on table api.<your_table> to authenticated;
|
||||
```
|
||||
|
||||
## Automatically enabling RLS on new tables
|
||||
|
||||
Tables created via the Supabase Dashboard have RLS enabled by default. However, if you or your team create tables using the SQL editor, migrations, or an external tool, RLS will not be enabled automatically.
|
||||
|
||||
You can use an [event trigger](/docs/guides/database/postgres/event-triggers#example-trigger-function---auto-enable-row-level-security) to automatically enable RLS whenever a new table is created in the `public` schema. This ensures that no table is accidentally left exposed without RLS protection.
|
||||
|
||||
## Table-level grants
|
||||
|
||||
By default, tables in the `public` schema are granted full access (`SELECT`, `INSERT`, `UPDATE`, `DELETE`) to the `anon` and `authenticated` roles. This allows the Data API to query those tables on behalf of users.
|
||||
|
||||
You can adjust these privileges on a per-table basis to restrict which operations each role can perform. For example, you might want to:
|
||||
|
||||
- Allow `anon` users to only `SELECT` from a table, preventing anonymous writes.
|
||||
- Prevent `anon` users from accessing a table entirely, making it available only to authenticated users.
|
||||
- Restrict `authenticated` users to `SELECT` and `INSERT` only, preventing updates and deletes.
|
||||
|
||||
<Admonition type="tip">
|
||||
|
||||
Table-level privileges work alongside [Row Level Security](/docs/guides/database/postgres/row-level-security). Privileges control _which operations_ are possible, while RLS policies control _which rows_ are accessible. For full protection, use both: restrict privileges to limit operation types, and use RLS policies to control row-level access.
|
||||
|
||||
</Admonition>
|
||||
|
||||
### Adjusting table-level grants via the Dashboard
|
||||
|
||||
<Admonition type="note">
|
||||
|
||||
Adjusting table-level privileges via the Dashboard is currently in beta and will be available via gradual roll-out.
|
||||
|
||||
</Admonition>
|
||||
|
||||
1. Go to [**Table Editor**](/dashboard/project/_/editor) in the Supabase Dashboard.
|
||||
2. Select the table you want to configure.
|
||||
3. Click the vertical dots icon to open the table menu and select "Edit table".
|
||||
4. Under **Data API Access**, click the settings icon to open **Adjust API privileges per role**.
|
||||
5. For each role (`anon` and `authenticated`), select or deselect the privileges you want to grant.
|
||||
6. Click **Save**.
|
||||
|
||||
### Adjusting table-level grants via SQL
|
||||
|
||||
You can also adjust privileges using SQL. For example, to allow only `SELECT` access for `anon` on a table:
|
||||
|
||||
```sql
|
||||
-- Revoke all existing privileges
|
||||
revoke all on table public.your_table from anon;
|
||||
|
||||
-- Grant only SELECT
|
||||
grant select on table public.your_table to anon;
|
||||
```
|
||||
|
||||
To remove all access for `anon` from a table:
|
||||
|
||||
```sql
|
||||
revoke all on table public.your_table from anon;
|
||||
```
|
||||
|
||||
To restore full access:
|
||||
|
||||
```sql
|
||||
grant select, insert, update, delete on table public.your_table to anon;
|
||||
```
|
||||
+1
-1
@@ -134,7 +134,7 @@ revoke all
|
||||
create policy "Allow auth admin to read user roles" ON public.user_roles
|
||||
as permissive for select
|
||||
to supabase_auth_admin
|
||||
using (true)
|
||||
using (true);
|
||||
```
|
||||
|
||||
</TabPanel>
|
||||
|
||||
@@ -534,6 +534,5 @@ This prevents the policy `( (select auth.uid()) = user_id )` from running for an
|
||||
## More resources
|
||||
|
||||
- [Testing your database](/docs/guides/database/testing)
|
||||
- [Row Level Security and Supabase Auth](/docs/guides/database/postgres/row-level-security)
|
||||
- [RLS Guide and Best Practices](https://github.com/orgs/supabase/discussions/14576)
|
||||
- Community repo on testing RLS using [pgTAP and dbdev](https://github.com/usebasejump/supabase-test-helpers/tree/main)
|
||||
@@ -18,7 +18,13 @@ You might use database replication for:
|
||||
|
||||
## Replication methods
|
||||
|
||||
Supabase supports two methods for replicating your database to external destinations:
|
||||
Supabase supports three methods for replicating your database to external destinations:
|
||||
|
||||
### Read Replicas
|
||||
|
||||
Additional databases that are kept in sync with your Primary database. These read-only databases can be deployed across multiple regions, for lower latency and better resource management.
|
||||
|
||||
- [Set up Read Replicas](/docs/guides/platform/read-replicas)
|
||||
|
||||
### Replication
|
||||
|
||||
|
||||
@@ -45,7 +45,7 @@ These conflicts can be resolved in the same way as normal Git Conflicts: merge o
|
||||
|
||||
### Changing production branch
|
||||
|
||||
It's not possible to change the Git branch used as the Production branch for Supabase Branching. The only way to change it is to disable and re-enable branching. See [Disable Branching](#disable-branching).
|
||||
You cannot change which project branch serves as the production branch — the base project that all branches are created from will always remain the production branch. However, you can update which GitHub branch is linked to your production branch. To do this, go to the [Integrations page](/dashboard/project/_/settings/integrations) and change the production branch name.
|
||||
|
||||
## Migration issues
|
||||
|
||||
|
||||
@@ -15,6 +15,12 @@ For this guide, we'll create a table called `employees` and see how we can make
|
||||
|
||||
You will need to [install](/docs/guides/local-development#quickstart) the Supabase CLI and start the local development stack.
|
||||
|
||||
<Admonition type="tip">
|
||||
|
||||
If a lock timeout error occurs, in your migration file, consider increasing your [`lock_timeout`](https://postgresqlco.nf/doc/en/param/lock_timeout/) setting.
|
||||
|
||||
</Admonition>
|
||||
|
||||
<StepHikeCompact>
|
||||
|
||||
<StepHikeCompact.Step step={1}>
|
||||
|
||||
@@ -84,10 +84,10 @@ After developing your project and deciding it's production ready, you should run
|
||||
| Create or Verify an MFA challenge | `/auth/v1/factors/:id/challenge` `/auth/v1/factors/:id/verify` | IP Address | 15 requests per minute (with bursts up to 30 requests) |
|
||||
| Anonymous sign-ins | `/auth/v1/signup`[^2] | IP Address | 30 requests per hour (with bursts up to 30 requests) |
|
||||
|
||||
### Realtime quotas
|
||||
### Realtime limits
|
||||
|
||||
- Review the [Realtime quotas](/docs/guides/realtime/quotas).
|
||||
- If you need quotas increased you can always [contact support](/dashboard/support/new).
|
||||
- Review the [Realtime limits](/docs/guides/realtime/limits).
|
||||
- If you need limits increased you can always [contact support](/dashboard/support/new).
|
||||
|
||||
### Abuse prevention
|
||||
|
||||
|
||||
@@ -380,22 +380,12 @@ grant all on all sequences in schema graphql to postgres, anon, authenticated, s
|
||||
|
||||
### Permission denied on `db push`
|
||||
|
||||
If you created a table through Supabase dashboard, and your new migration script contains `ALTER TABLE` statements, you might run into permission error when applying them on staging or production databases.
|
||||
|
||||
```bash
|
||||
ERROR: must be owner of table employees (SQLSTATE 42501); while executing migration <timestamp>
|
||||
```
|
||||
|
||||
This is because tables created through Supabase dashboard are owned by `supabase_admin` role while the migration scripts executed through CLI are under `postgres` role.
|
||||
|
||||
One way to solve this is to reassign the owner of those tables to `postgres` role. For example, if your table is named `users` in the public schema, you can run the following command to reassign owner.
|
||||
If you create a table using a custom database role, the default `postgres` user may lack permission to modify it. This can cause `42501` privilege errors during migrations. To resolve this, grant the 'postgres` user ownership of the custom role.
|
||||
|
||||
```sql
|
||||
ALTER TABLE users OWNER TO postgres;
|
||||
grant "custom_role" to "postgres";
|
||||
```
|
||||
|
||||
Apart from tables, you also need to reassign owner of other entities using their respective commands, including [types](https://www.postgresql.org/docs/current/sql-altertype.html), [functions](https://www.postgresql.org/docs/current/sql-alterroutine.html), and [schemas](https://www.postgresql.org/docs/current/sql-alterschema.html).
|
||||
|
||||
### Rebasing new migrations
|
||||
|
||||
Sometimes your teammate may merge a new migration file to git main branch, and now you need to rebase your local schema changes on top.
|
||||
|
||||
@@ -32,7 +32,10 @@ Following the [upcoming API key changes](https://github.com/orgs/supabase/discus
|
||||
|
||||
## Integrating with Supabase Auth
|
||||
|
||||
The simplest way to secure your endpoints is by using Supabase Auth to verify users.
|
||||
Important notes to consider:
|
||||
|
||||
- This is done _inside_ the `Deno.serve()` callback argument, so that the Authorization header is set for each request.
|
||||
- Use `Deno.env.get('SUPABASE_URL')` to get the URL associated with your project. Using a value such as `http://localhost:54321` for local development will fail due to Docker containerization.
|
||||
|
||||
<$Partial path="api_settings.mdx" variables={{ "framework": "", "tab": "" }} />
|
||||
|
||||
|
||||
@@ -79,7 +79,7 @@ supabase functions deploy resend --no-verify-jwt
|
||||
|
||||
<Admonition type="caution">
|
||||
|
||||
When you deploy to Supabase, make sure that your `RESEND_API_KEY` is set in [Edge Function Secrets Management](/dashboard/project/_/settings/functions)
|
||||
When you deploy to Supabase, make sure that your `RESEND_API_KEY` is set in [Edge Function Secrets Management](/dashboard/project/_/functions/secrets)
|
||||
|
||||
</Admonition>
|
||||
|
||||
|
||||
@@ -95,7 +95,7 @@ You will also need to set secrets for your production Edge Functions. You can do
|
||||
|
||||
**Using the Dashboard**:
|
||||
|
||||
1. Visit [Edge Function Secrets Management](/dashboard/project/_/settings/functions) page in your Dashboard.
|
||||
1. Visit [Edge Function Secrets Management](/dashboard/project/_/functions/secrets) page in your Dashboard.
|
||||
2. Add the Key and Value for your secret and press Save
|
||||
|
||||
<Image
|
||||
|
||||
@@ -14,3 +14,16 @@ Use the "include file" feature from your AI tool to include the prompt when chat
|
||||
## Prompts
|
||||
|
||||
<AiPromptsIndex />
|
||||
|
||||
## Use in different environments
|
||||
|
||||
You can load these prompts into various tools. Here are common options and where to place the prompt:
|
||||
|
||||
| Environment | Where to put prompt | Installation instructions |
|
||||
| -------------- | ---------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------- |
|
||||
| Cursor | Project rules (`.cursor/rules/*.md` or `.mdx`) | [Configure project rules](https://docs.cursor.com/en/context/rules) |
|
||||
| GitHub Copilot | `.github/copilot-instructions.md` | [Custom instructions in Copilot](https://code.visualstudio.com/docs/copilot/copilot-customization#_custom-instructions) |
|
||||
| JetBrains IDEs | `guidelines.md` | [Customize guidelines](https://www.jetbrains.com/help/junie/customize-guidelines.html) |
|
||||
| Gemini CLI | `GEMINI.md` | [Gemini CLI codelab](https://codelabs.developers.google.com/gemini-cli-hands-on) |
|
||||
| VS Code | `.instructions.md` | Configure `.instructions.md` |
|
||||
| Windsurf | `guidelines.md` | Configure `guidelines.md` |
|
||||
@@ -12,17 +12,18 @@ This guide covers MCP servers that do not require authentication. Auth support f
|
||||
|
||||
</Admonition>
|
||||
|
||||
## Deploy your MCP server
|
||||
|
||||
### Prerequisites
|
||||
## Prerequisites
|
||||
|
||||
Before you begin, make sure you have:
|
||||
|
||||
- [Docker](https://docs.docker.com/get-docker/) installed (required for local Supabase development)
|
||||
- [Docker](https://docs.docker.com/get-docker/) or a compatible runtime installed and running (required for local development)
|
||||
- [Deno](https://deno.land/) installed (Supabase Edge Functions runtime)
|
||||
- [Supabase CLI](/docs/guides/cli/getting-started) installed
|
||||
- [Supabase CLI](/docs/guides/local-development) installed and authenticated
|
||||
- [Node.js 20 or later](https://nodejs.org/) (required by Supabase CLI)
|
||||
|
||||
### Create a new project
|
||||
## Deploy your MCP server
|
||||
|
||||
### Step 1: Create a new project
|
||||
|
||||
Start by creating a new Supabase project:
|
||||
|
||||
@@ -32,7 +33,15 @@ cd my-mcp-server
|
||||
supabase init
|
||||
```
|
||||
|
||||
### Create the MCP server function
|
||||
<Admonition type="note">
|
||||
|
||||
After this step, you should have a project directory with a `supabase` folder containing `config.toml` and an empty `functions` directory.
|
||||
|
||||
</Admonition>
|
||||
|
||||
---
|
||||
|
||||
### Step 2: Create the MCP server function
|
||||
|
||||
Create a new Edge Function for your MCP server:
|
||||
|
||||
@@ -40,35 +49,22 @@ Create a new Edge Function for your MCP server:
|
||||
supabase functions new mcp
|
||||
```
|
||||
|
||||
Create a `deno.json` file in `supabase/functions/mcp/` with the required dependencies:
|
||||
|
||||
```json
|
||||
{
|
||||
"imports": {
|
||||
"@hono/mcp": "npm:@hono/mcp@^0.1.1",
|
||||
"@modelcontextprotocol/sdk": "npm:@modelcontextprotocol/sdk@^1.24.3",
|
||||
"hono": "npm:hono@^4.9.2",
|
||||
"zod": "npm:zod@^4.1.13"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
<Admonition type="tip">
|
||||
|
||||
This tutorial uses the [official MCP TypeScript SDK](https://github.com/modelcontextprotocol/typescript-sdk), but you can use any MCP framework that's compatible with the [Edge Runtime](/docs/guides/functions), such as [mcp-lite](https://github.com/fiberplane/mcp-lite), [mcp-use](https://github.com/mcp-use/mcp-use), or [mcp-handler](https://github.com/vercel/mcp-handler).
|
||||
This tutorial uses the [official MCP TypeScript SDK](https://github.com/modelcontextprotocol/typescript-sdk) with the `WebStandardStreamableHTTPServerTransport`, but you can use any MCP framework that's compatible with the [Edge Runtime](/docs/guides/functions), such as [mcp-lite](https://github.com/fiberplane/mcp-lite) or [mcp-handler](https://github.com/vercel/mcp-handler).
|
||||
|
||||
</Admonition>
|
||||
|
||||
Replace the contents of `supabase/functions/mcp/index.ts` with:
|
||||
|
||||
```ts
|
||||
```ts name=supabase/functions/mcp/index.ts
|
||||
// Setup type definitions for built-in Supabase Runtime APIs
|
||||
import 'jsr:@supabase/functions-js/edge-runtime.d.ts'
|
||||
|
||||
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'
|
||||
import { StreamableHTTPTransport } from '@hono/mcp'
|
||||
import { Hono } from 'hono'
|
||||
import { z } from 'zod'
|
||||
import { McpServer } from 'npm:@modelcontextprotocol/sdk@1.25.3/server/mcp.js'
|
||||
import { WebStandardStreamableHTTPServerTransport } from 'npm:@modelcontextprotocol/sdk@1.25.3/server/webStandardStreamableHttp.js'
|
||||
import { Hono } from 'npm:hono@^4.9.7'
|
||||
import { z } from 'npm:zod@^4.1.13'
|
||||
|
||||
// Create Hono app
|
||||
const app = new Hono()
|
||||
@@ -92,17 +88,31 @@ server.registerTool(
|
||||
})
|
||||
)
|
||||
|
||||
// Handle MCP requests at the root path
|
||||
app.all('/', async (c) => {
|
||||
const transport = new StreamableHTTPTransport()
|
||||
// Handle MCP requests
|
||||
app.all('*', async (c) => {
|
||||
const transport = new WebStandardStreamableHTTPServerTransport()
|
||||
await server.connect(transport)
|
||||
return transport.handleRequest(c)
|
||||
return transport.handleRequest(c.req.raw)
|
||||
})
|
||||
|
||||
Deno.serve(app.fetch)
|
||||
```
|
||||
|
||||
### Local development
|
||||
<Admonition type="note">
|
||||
|
||||
After this step, you should have a new file at `supabase/functions/mcp/index.ts`.
|
||||
|
||||
</Admonition>
|
||||
|
||||
<Admonition type="caution">
|
||||
|
||||
Within Edge Functions, paths are prefixed with the function name. If your function is named something other than `mcp`, configure Hono with a base path: `new Hono().basePath('/your-function-name')`.
|
||||
|
||||
</Admonition>
|
||||
|
||||
---
|
||||
|
||||
### Step 3: Test locally
|
||||
|
||||
Start the Supabase local development stack:
|
||||
|
||||
@@ -128,7 +138,44 @@ The `--no-verify-jwt` flag disables JWT verification at the Edge Function layer
|
||||
|
||||
</Admonition>
|
||||
|
||||
### Test your MCP server
|
||||
#### Test with curl
|
||||
|
||||
You can also test your MCP server directly with curl. Call the `add` tool:
|
||||
|
||||
```bash
|
||||
curl -X POST 'http://localhost:54321/functions/v1/mcp' \
|
||||
-H 'Content-Type: application/json' \
|
||||
-H 'Accept: application/json, text/event-stream' \
|
||||
-d '{
|
||||
"jsonrpc": "2.0",
|
||||
"id": 1,
|
||||
"method": "tools/call",
|
||||
"params": {
|
||||
"name": "add",
|
||||
"arguments": {
|
||||
"a": 5,
|
||||
"b": 3
|
||||
}
|
||||
}
|
||||
}'
|
||||
```
|
||||
|
||||
<Admonition type="note">
|
||||
|
||||
The MCP Streamable HTTP transport requires the `Accept: application/json, text/event-stream` header to indicate the client supports both JSON and Server-Sent Events responses.
|
||||
|
||||
</Admonition>
|
||||
|
||||
**Expected response:**
|
||||
|
||||
The response uses Server-Sent Events (SSE) format:
|
||||
|
||||
```
|
||||
event: message
|
||||
data: {"result":{"content":[{"type":"text","text":"8"}]},"jsonrpc":"2.0","id":1}
|
||||
```
|
||||
|
||||
#### Test with MCP Inspector
|
||||
|
||||
Test your server with the official [MCP Inspector](https://github.com/modelcontextprotocol/inspector):
|
||||
|
||||
@@ -138,7 +185,13 @@ npx -y @modelcontextprotocol/inspector
|
||||
|
||||
Use the local endpoint `http://localhost:54321/functions/v1/mcp` in the inspector UI to explore available tools and test them interactively.
|
||||
|
||||
### Deploy to production
|
||||
<Admonition type="note">
|
||||
|
||||
After this step, you should have your MCP server running locally and be able to test the `add` tool in the MCP Inspector.
|
||||
|
||||
</Admonition>
|
||||
|
||||
### Step 4: Deploy to production
|
||||
|
||||
When you're ready to deploy, link your project and deploy the function:
|
||||
|
||||
@@ -155,11 +208,17 @@ https://<your-project-ref>.supabase.co/functions/v1/mcp
|
||||
|
||||
Update your MCP client configuration to use the production URL.
|
||||
|
||||
<Admonition type="note">
|
||||
|
||||
After this step, you have a fully deployed MCP server accessible from anywhere. You can test it using the MCP Inspector with your production URL.
|
||||
|
||||
</Admonition>
|
||||
|
||||
## Examples
|
||||
|
||||
You can find ready-to-use MCP server implementations here:
|
||||
|
||||
- [MCP server examples on GitHub](https://github.com/supabase/supabase/tree/master/examples/edge-functions/supabase/functions/mcp/)
|
||||
- [Simple MCP server](https://github.com/supabase/supabase/tree/master/examples/edge-functions/supabase/functions/mcp/simple-mcp-server) - Basic unauthenticated example
|
||||
|
||||
## Resources
|
||||
|
||||
@@ -168,5 +227,4 @@ You can find ready-to-use MCP server implementations here:
|
||||
- [Supabase Edge Functions](/docs/guides/functions)
|
||||
- [OAuth 2.1 Server](/docs/guides/auth/oauth-server)
|
||||
- [MCP Authentication](/docs/guides/auth/oauth-server/mcp-authentication)
|
||||
- [MCP server examples on GitHub](https://github.com/supabase/supabase/tree/master/examples/edge-functions/supabase/functions/mcp)
|
||||
- [Building MCP servers with mcp-lite](/docs/guides/functions/examples/mcp-server-mcp-lite) - Alternative lightweight framework
|
||||
@@ -22,7 +22,13 @@ Choose your Supabase platform, project, and MCP client and follow the installati
|
||||
|
||||
### Next steps
|
||||
|
||||
Your AI tool is now connected to your Supabase project or account using remote MCP. Try asking the AI tool to query your database using natural language commands.
|
||||
Your MCP client automatically redirects you to log in to Supabase during setup. This opens a browser window where you can log in to your Supabase account and grant access to the MCP client. Be sure to choose the organization that contains the project you wish to work with.
|
||||
|
||||
After you log in, check that the MCP server is connected. For instance, in Cursor, navigate to **Settings > Cursor Settings > Tools & MCP**. Depending on the client, you may need to restart it to connect and detect all tools after authorization.
|
||||
|
||||
To verify the client has access to the MCP server tools, try asking it to query your project or database using natural language. For example: "What tables are there in the database? Use MCP tools."
|
||||
|
||||
For curated, ready-to-use prompts that work well with IDEs and AI agents, see our [AI Prompts](/guides/getting-started/ai-prompts) collection.
|
||||
|
||||
## Manual authentication
|
||||
|
||||
@@ -106,3 +112,7 @@ We recommend the following best practices to mitigate security risks when using
|
||||
- **Project scoping**: Scope your MCP server to a [specific project](https://github.com/supabase-community/supabase-mcp#project-scoped-mode), limiting access to only that project's resources. This prevents LLMs from accessing data from other projects in your Supabase account.
|
||||
- **Branching**: Use Supabase's [branching feature](/docs/guides/deployment/branching) to create a development branch for your database. This allows you to test changes in a safe environment before merging them to production.
|
||||
- **Feature groups**: The server allows you to enable or disable specific [tool groups](https://github.com/supabase-community/supabase-mcp#feature-groups), so you can control which tools are available to the LLM. This helps reduce the attack surface and limits the actions that LLMs can perform to only those that you need.
|
||||
|
||||
## On GitHub
|
||||
|
||||
The MCP server repository is available at [github.com/supabase-community/supabase-mcp](https://github.com/supabase-community/supabase-mcp).
|
||||
@@ -45,7 +45,7 @@ hideToc: true
|
||||
<StepHikeCompact.Code>
|
||||
|
||||
```bash name=Terminal
|
||||
cd my-app && npx expo install @supabase/supabase-js @react-native-async-storage/async-storage react-native-url-polyfill
|
||||
cd my-app && npx expo install @supabase/supabase-js react-native-url-polyfill expo-sqlite
|
||||
```
|
||||
|
||||
</StepHikeCompact.Code>
|
||||
@@ -82,7 +82,7 @@ hideToc: true
|
||||
|
||||
Create a helper file at `lib/supabase.ts` to initialize the Supabase client using the environment variables.
|
||||
|
||||
The code below uses [AsyncStorage](https://www.npmjs.com/package/@react-native-async-storage/async-storage) to persist the user session in your app.
|
||||
The code below uses Expo's localStorage polyfill to persist authentication sessions.
|
||||
|
||||
</StepHikeCompact.Details>
|
||||
|
||||
@@ -91,14 +91,14 @@ hideToc: true
|
||||
```ts name=lib/supabase.ts
|
||||
import 'react-native-url-polyfill/auto'
|
||||
import { createClient } from '@supabase/supabase-js'
|
||||
import AsyncStorage from '@react-native-async-storage/async-storage'
|
||||
import 'expo-sqlite/localStorage/install';
|
||||
|
||||
const supabaseUrl = process.env.EXPO_PUBLIC_SUPABASE_URL
|
||||
const supabaseAnonKey = process.env.EXPO_PUBLIC_SUPABASE_PUBLISHABLE_KEY
|
||||
|
||||
export const supabase = createClient(supabaseUrl, supabaseAnonKey, {
|
||||
auth: {
|
||||
storage: AsyncStorage,
|
||||
storage: localStorage,
|
||||
autoRefreshToken: true,
|
||||
persistSession: true,
|
||||
detectSessionInUrl: false,
|
||||
|
||||
@@ -27,9 +27,11 @@ hideToc: true
|
||||
|
||||
<StepHikeCompact.Details title="Install the Supabase client library">
|
||||
|
||||
Install Supabase package dependency using Xcode by following Apple's [tutorial](https://developer.apple.com/documentation/xcode/adding-package-dependencies-to-your-app).
|
||||
Add the [supabase-swift](https://github.com/supabase/supabase-swift) package to your app using the Swift Package Manager.
|
||||
|
||||
Make sure to add `Supabase` product package as dependency to the application.
|
||||
In Xcode, navigate to **File > Add Package Dependencies...** and enter the repository URL `https://github.com/supabase/supabase-swift` in the search bar. For detailed instructions, see Apple's [tutorial on adding package dependencies](https://developer.apple.com/documentation/xcode/adding-package-dependencies-to-your-app).
|
||||
|
||||
Make sure to add `Supabase` product package as a dependency to your application target.
|
||||
|
||||
</StepHikeCompact.Details>
|
||||
|
||||
@@ -76,7 +78,7 @@ hideToc: true
|
||||
|
||||
<StepHikeCompact.Code>
|
||||
|
||||
```swift name=Supabase.swift
|
||||
```swift name=Instrument.swift
|
||||
struct Instrument: Decodable, Identifiable {
|
||||
let id: Int
|
||||
let name: String
|
||||
@@ -100,6 +102,8 @@ hideToc: true
|
||||
<StepHikeCompact.Code>
|
||||
|
||||
```swift name=ContentView.swift
|
||||
import SwiftUI
|
||||
|
||||
struct ContentView: View {
|
||||
|
||||
@State var instruments: [Instrument] = []
|
||||
@@ -138,3 +142,12 @@ hideToc: true
|
||||
</StepHikeCompact.Step>
|
||||
|
||||
</StepHikeCompact>
|
||||
|
||||
## Setting up deep links
|
||||
|
||||
If you want to implement authentication features like magic links or OAuth, you need to set up deep links to redirect users back to your app. For instructions on configuring custom URL schemes for your iOS app, see the [deep linking guide](/docs/guides/auth/native-mobile-deep-linking?platform=swift).
|
||||
|
||||
## Next steps
|
||||
|
||||
- Learn how to build a complete user management app with authentication in the [Swift tutorial](/docs/guides/getting-started/tutorials/with-swift)
|
||||
- Explore the [supabase-swift](https://github.com/supabase/supabase-swift) library on GitHub
|
||||
@@ -21,11 +21,11 @@ Start with building the Angular app from scratch.
|
||||
|
||||
### Initialize an Angular app
|
||||
|
||||
You can use the [Angular CLI](https://angular.io/cli) to initialize
|
||||
an app called `supabase-angular`. The command sets some defaults, that you change to suit your needs:
|
||||
You can use the [Angular CLI](https://angular.io/cli) to initialize an app called `supabase-angular`.
|
||||
The command sets some defaults, that you change to suit your needs:
|
||||
|
||||
```bash
|
||||
npx ng new supabase-angular --routing false --style css --standalone false --zoneless true --ssr false
|
||||
npx ng new supabase-angular --routing false --style css --standalone false --ssr false
|
||||
cd supabase-angular
|
||||
```
|
||||
|
||||
@@ -35,105 +35,26 @@ Then, install the only additional dependency: [supabase-js](https://github.com/s
|
||||
npm install @supabase/supabase-js
|
||||
```
|
||||
|
||||
Finally, save the environment variables in the `src/environments/environment.ts` file.
|
||||
Finally, save the environment variables in a new `src/environments/environment.ts` file.
|
||||
You need to create the `src/environments` directory first.
|
||||
All you need are the API URL and the key that you copied [earlier](#get-api-details).
|
||||
The application exposes these variables in the browser, and that's fine as you have [Row Level Security](/docs/guides/auth#row-level-security) enabled on the Database.
|
||||
|
||||
<$CodeTabs>
|
||||
|
||||
```ts name=src/environments/environment.ts
|
||||
export const environment = {
|
||||
production: false,
|
||||
supabaseUrl: 'YOUR_SUPABASE_URL',
|
||||
supabaseKey: 'YOUR_SUPABASE_KEY',
|
||||
}
|
||||
```
|
||||
|
||||
</$CodeTabs>
|
||||
<$CodeSample
|
||||
path="/user-management/angular-user-management/src/environments/environment.ts"
|
||||
lines={[[1, -1]]}
|
||||
meta="name=src/environments/environment.ts"
|
||||
/>
|
||||
|
||||
Now you have the API credentials in place, create a `SupabaseService` with `ng g s supabase` and add the following code to initialize the Supabase client and implement functions to communicate with the Supabase API.
|
||||
|
||||
<$CodeTabs>
|
||||
<$CodeSample
|
||||
path="/user-management/angular-user-management/src/app/supabase.service.ts"
|
||||
lines={[[1, -1]]}
|
||||
meta="name=src/app/supabase.service.ts"
|
||||
/>
|
||||
|
||||
```ts name=src/app/supabase.service.ts
|
||||
import { Injectable } from '@angular/core'
|
||||
import {
|
||||
AuthChangeEvent,
|
||||
AuthSession,
|
||||
createClient,
|
||||
Session,
|
||||
SupabaseClient,
|
||||
User,
|
||||
} from '@supabase/supabase-js'
|
||||
import { environment } from '../environments/environment'
|
||||
|
||||
export interface Profile {
|
||||
id?: string
|
||||
username: string
|
||||
website: string
|
||||
avatar_url: string
|
||||
}
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root',
|
||||
})
|
||||
export class SupabaseService {
|
||||
private supabase: SupabaseClient
|
||||
_session: AuthSession | null = null
|
||||
|
||||
constructor() {
|
||||
this.supabase = createClient(environment.supabaseUrl, environment.supabaseKey)
|
||||
}
|
||||
|
||||
get session() {
|
||||
this.supabase.auth.getSession().then(({ data }) => {
|
||||
this._session = data.session
|
||||
})
|
||||
return this._session
|
||||
}
|
||||
|
||||
profile(user: User) {
|
||||
return this.supabase
|
||||
.from('profiles')
|
||||
.select(`username, website, avatar_url`)
|
||||
.eq('id', user.id)
|
||||
.single()
|
||||
}
|
||||
|
||||
authChanges(callback: (event: AuthChangeEvent, session: Session | null) => void) {
|
||||
return this.supabase.auth.onAuthStateChange(callback)
|
||||
}
|
||||
|
||||
signIn(email: string) {
|
||||
return this.supabase.auth.signInWithOtp({ email })
|
||||
}
|
||||
|
||||
signOut() {
|
||||
return this.supabase.auth.signOut()
|
||||
}
|
||||
|
||||
updateProfile(profile: Profile) {
|
||||
const update = {
|
||||
...profile,
|
||||
updated_at: new Date(),
|
||||
}
|
||||
|
||||
return this.supabase.from('profiles').upsert(update)
|
||||
}
|
||||
|
||||
downLoadImage(path: string) {
|
||||
return this.supabase.storage.from('avatars').download(path)
|
||||
}
|
||||
|
||||
uploadAvatar(filePath: string, file: File) {
|
||||
return this.supabase.storage.from('avatars').upload(filePath, file)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
</$CodeTabs>
|
||||
|
||||
Optionally, update `src/styles.css` [with the following styles](https://raw.githubusercontent.com/supabase/supabase/master/examples/user-management/angular-user-management/src/styles.css) to style the app.
|
||||
Optionally, update `src/styles.css` to style the app. You can find the full contents of this file [in the example repository](https://github.com/supabase/supabase/tree/master/examples/user-management/angular-user-management/src/styles.css).
|
||||
|
||||
### Set up a login component
|
||||
|
||||
@@ -143,75 +64,17 @@ Create an `AuthComponent` with the `ng g c auth` Angular CLI command and add the
|
||||
|
||||
<$CodeTabs>
|
||||
|
||||
```ts name=src/app/auth/auth.ts
|
||||
import { Component } from '@angular/core'
|
||||
import { FormBuilder, FormGroup } from '@angular/forms'
|
||||
import { SupabaseService } from '../supabase.service'
|
||||
<$CodeSample
|
||||
path="/user-management/angular-user-management/src/app/auth/auth.component.ts"
|
||||
lines={[[1, -1]]}
|
||||
meta="name=src/app/auth/auth.component.ts"
|
||||
/>
|
||||
|
||||
@Component({
|
||||
selector: 'app-auth',
|
||||
templateUrl: './auth.html',
|
||||
styleUrls: ['./auth.css'],
|
||||
standalone: false,
|
||||
})
|
||||
export class AuthComponent {
|
||||
signInForm!: FormGroup
|
||||
constructor(
|
||||
private readonly supabase: SupabaseService,
|
||||
private readonly formBuilder: FormBuilder
|
||||
) {}
|
||||
|
||||
loading = false
|
||||
ngOnInit() {
|
||||
this.signInForm = this.formBuilder.group({
|
||||
email: '',
|
||||
})
|
||||
}
|
||||
|
||||
async onSubmit(): Promise<void> {
|
||||
try {
|
||||
this.loading = true
|
||||
const email = this.signInForm.value.email as string
|
||||
const { error } = await this.supabase.signIn(email)
|
||||
if (error) throw error
|
||||
alert('Check your email for the login link!')
|
||||
} catch (error) {
|
||||
if (error instanceof Error) {
|
||||
alert(error.message)
|
||||
}
|
||||
} finally {
|
||||
this.signInForm.reset()
|
||||
this.loading = false
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
```html name=src/app/auth/auth.html
|
||||
<div class="row flex-center flex">
|
||||
<div class="col-6 form-widget" aria-live="polite">
|
||||
<h1 class="header">Supabase + Angular</h1>
|
||||
<p class="description">Sign in via magic link with your email below</p>
|
||||
<form [formGroup]="signInForm" (ngSubmit)="onSubmit()" class="form-widget">
|
||||
<div>
|
||||
<label for="email">Email</label>
|
||||
<input
|
||||
id="email"
|
||||
formControlName="email"
|
||||
class="inputField"
|
||||
type="email"
|
||||
placeholder="Your email"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<button type="submit" class="button block" [disabled]="loading">
|
||||
{{ loading ? "Loading" : "Send magic link" }}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
```
|
||||
<$CodeSample
|
||||
path="/user-management/angular-user-management/src/app/auth/auth.component.html"
|
||||
lines={[[1, -1]]}
|
||||
meta="name=src/app/auth/auth.component.html"
|
||||
/>
|
||||
|
||||
</$CodeTabs>
|
||||
|
||||
@@ -222,138 +85,42 @@ Create an `AccountComponent` with the `ng g c account` Angular CLI command and a
|
||||
|
||||
<$CodeTabs>
|
||||
|
||||
```ts name=src/app/account/account.ts
|
||||
import { Component, Input, OnInit } from '@angular/core'
|
||||
import { FormBuilder, FormGroup } from '@angular/forms'
|
||||
import { AuthSession } from '@supabase/supabase-js'
|
||||
import { Profile, SupabaseService } from '../supabase.service'
|
||||
<$CodeSample
|
||||
path="/user-management/angular-user-management/src/app/account/account.component.ts"
|
||||
lines={[[1, -1]]}
|
||||
meta="name=src/app/account/account.component.ts"
|
||||
/>
|
||||
|
||||
@Component({
|
||||
selector: 'app-account',
|
||||
templateUrl: './account.html',
|
||||
styleUrls: ['./account.css'],
|
||||
standalone: false,
|
||||
})
|
||||
export class AccountComponent implements OnInit {
|
||||
loading = false
|
||||
profile!: Profile
|
||||
updateProfileForm!: FormGroup
|
||||
<$CodeSample
|
||||
path="/user-management/angular-user-management/src/app/account/account.component.html"
|
||||
lines={[[1, -1]]}
|
||||
meta="name=src/app/account/account.component.html"
|
||||
/>
|
||||
|
||||
get avatarUrl() {
|
||||
return this.updateProfileForm.value.avatar_url as string
|
||||
}
|
||||
async updateAvatar(event: string): Promise<void> {
|
||||
this.updateProfileForm.patchValue({
|
||||
avatar_url: event,
|
||||
})
|
||||
await this.updateProfile()
|
||||
}
|
||||
</$CodeTabs>
|
||||
|
||||
@Input()
|
||||
session!: AuthSession
|
||||
## Profile photos
|
||||
|
||||
constructor(
|
||||
private readonly supabase: SupabaseService,
|
||||
private formBuilder: FormBuilder
|
||||
) {
|
||||
this.updateProfileForm = this.formBuilder.group({
|
||||
username: '',
|
||||
website: '',
|
||||
avatar_url: '',
|
||||
})
|
||||
}
|
||||
Every Supabase project is configured with [Storage](/docs/guides/storage) for managing large files like photos and videos.
|
||||
|
||||
async ngOnInit(): Promise<void> {
|
||||
await this.getProfile()
|
||||
### Create an upload widget
|
||||
|
||||
const { username, website, avatar_url } = this.profile
|
||||
this.updateProfileForm.patchValue({
|
||||
username,
|
||||
website,
|
||||
avatar_url,
|
||||
})
|
||||
}
|
||||
Create an avatar for the user so that they can upload a profile photo.
|
||||
Create an `AvatarComponent` with `ng g c avatar` Angular CLI command and add the following code.
|
||||
|
||||
async getProfile() {
|
||||
try {
|
||||
this.loading = true
|
||||
const { user } = this.session
|
||||
const { data: profile, error, status } = await this.supabase.profile(user)
|
||||
<$CodeTabs>
|
||||
|
||||
if (error && status !== 406) {
|
||||
throw error
|
||||
}
|
||||
<$CodeSample
|
||||
path="/user-management/angular-user-management/src/app/avatar/avatar.component.ts"
|
||||
lines={[[1, -1]]}
|
||||
meta="name=src/app/avatar/avatar.component.ts"
|
||||
/>
|
||||
|
||||
if (profile) {
|
||||
this.profile = profile
|
||||
}
|
||||
} catch (error) {
|
||||
if (error instanceof Error) {
|
||||
alert(error.message)
|
||||
}
|
||||
} finally {
|
||||
this.loading = false
|
||||
}
|
||||
}
|
||||
|
||||
async updateProfile(): Promise<void> {
|
||||
try {
|
||||
this.loading = true
|
||||
const { user } = this.session
|
||||
|
||||
const username = this.updateProfileForm.value.username as string
|
||||
const website = this.updateProfileForm.value.website as string
|
||||
const avatar_url = this.updateProfileForm.value.avatar_url as string
|
||||
|
||||
const { error } = await this.supabase.updateProfile({
|
||||
id: user.id,
|
||||
username,
|
||||
website,
|
||||
avatar_url,
|
||||
})
|
||||
if (error) throw error
|
||||
} catch (error) {
|
||||
if (error instanceof Error) {
|
||||
alert(error.message)
|
||||
}
|
||||
} finally {
|
||||
this.loading = false
|
||||
}
|
||||
}
|
||||
|
||||
async signOut() {
|
||||
await this.supabase.signOut()
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
```html name=src/app/account/account.html
|
||||
<form [formGroup]="updateProfileForm" (ngSubmit)="updateProfile()" class="form-widget">
|
||||
<app-avatar [avatarUrl]="this.avatarUrl" (upload)="updateAvatar($event)"> </app-avatar>
|
||||
<div>
|
||||
<label for="email">Email</label>
|
||||
<input id="email" type="text" [value]="session.user.email" disabled />
|
||||
</div>
|
||||
<div>
|
||||
<label for="username">Name</label>
|
||||
<input formControlName="username" id="username" type="text" />
|
||||
</div>
|
||||
<div>
|
||||
<label for="website">Website</label>
|
||||
<input formControlName="website" id="website" type="url" />
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<button type="submit" class="button primary block" [disabled]="loading">
|
||||
{{ loading ? "Loading ..." : "Update" }}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<button class="button block" (click)="signOut()">Sign Out</button>
|
||||
</div>
|
||||
</form>
|
||||
```
|
||||
<$CodeSample
|
||||
path="/user-management/angular-user-management/src/app/avatar/avatar.component.html"
|
||||
lines={[[1, -1]]}
|
||||
meta="name=src/app/avatar/avatar.component.html"
|
||||
/>
|
||||
|
||||
</$CodeTabs>
|
||||
|
||||
@@ -363,65 +130,27 @@ Now you have all the components in place, update `AppComponent`:
|
||||
|
||||
<$CodeTabs>
|
||||
|
||||
```ts name=src/app/app.ts
|
||||
import { Component, OnInit } from '@angular/core'
|
||||
import { SupabaseService } from './supabase.service'
|
||||
<$CodeSample
|
||||
path="/user-management/angular-user-management/src/app/app.component.ts"
|
||||
lines={[[1, -1]]}
|
||||
meta="name=src/app/app.component.ts"
|
||||
/>
|
||||
|
||||
@Component({
|
||||
selector: 'app-root',
|
||||
templateUrl: './app.html',
|
||||
styleUrls: ['./app.css'],
|
||||
standalone: false,
|
||||
})
|
||||
export class AppComponent implements OnInit {
|
||||
constructor(private readonly supabase: SupabaseService) {}
|
||||
|
||||
title = 'angular-user-management'
|
||||
session: any
|
||||
|
||||
ngOnInit() {
|
||||
this.session = this.supabase.session
|
||||
this.supabase.authChanges((_, session) => (this.session = session))
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
```html name=src/app/app.html
|
||||
<div class="container" style="padding: 50px 0 100px 0">
|
||||
<app-account *ngIf="session; else auth" [session]="session"></app-account>
|
||||
<ng-template #auth>
|
||||
<app-auth></app-auth>
|
||||
</ng-template>
|
||||
</div>
|
||||
```
|
||||
<$CodeSample
|
||||
path="/user-management/angular-user-management/src/app/app.component.html"
|
||||
lines={[[1, -1]]}
|
||||
meta="name=src/app/app.component.html"
|
||||
/>
|
||||
|
||||
</$CodeTabs>
|
||||
|
||||
You also need to change `app.module.ts` to include the `ReactiveFormsModule` from the `@angular/forms` package.
|
||||
|
||||
<$CodeTabs>
|
||||
|
||||
```ts name=src/app/app.module.ts
|
||||
import { NgModule } from '@angular/core'
|
||||
import { BrowserModule } from '@angular/platform-browser'
|
||||
|
||||
import { AppComponent } from './app'
|
||||
import { AuthComponent } from './auth/auth'
|
||||
import { AccountComponent } from './account/account'
|
||||
import { ReactiveFormsModule } from '@angular/forms'
|
||||
import { AvatarComponent } from './avatar/avatar'
|
||||
|
||||
@NgModule({
|
||||
declarations: [AppComponent, AuthComponent, AccountComponent, AvatarComponent],
|
||||
imports: [BrowserModule, ReactiveFormsModule],
|
||||
providers: [],
|
||||
bootstrap: [AppComponent],
|
||||
exports: [AppComponent, AuthComponent, AccountComponent, AvatarComponent],
|
||||
})
|
||||
export class AppModule {}
|
||||
```
|
||||
|
||||
</$CodeTabs>
|
||||
<$CodeSample
|
||||
path="/user-management/angular-user-management/src/app/app.module.ts"
|
||||
lines={[[1, -1]]}
|
||||
meta="name=src/app/app.module.ts"
|
||||
/>
|
||||
|
||||
Once that's done, run the application in a terminal:
|
||||
|
||||
@@ -433,152 +162,4 @@ Open the browser to [localhost:4200](http://localhost:4200) and you should see t
|
||||
|
||||

|
||||
|
||||
## Bonus: Profile photos
|
||||
|
||||
Every Supabase project is configured with [Storage](/docs/guides/storage) for managing large files like photos and videos.
|
||||
|
||||
### Create an upload widget
|
||||
|
||||
Create an avatar for the user so that they can upload a profile photo.
|
||||
Create an `AvatarComponent` with `ng g c avatar` Angular CLI command and add the following code.
|
||||
|
||||
<$CodeTabs>
|
||||
|
||||
```ts name=src/app/avatar/avatar.ts
|
||||
import { Component, EventEmitter, Input, Output } from '@angular/core'
|
||||
import { SafeResourceUrl, DomSanitizer } from '@angular/platform-browser'
|
||||
import { SupabaseService } from '../supabase.service'
|
||||
|
||||
@Component({
|
||||
selector: 'app-avatar',
|
||||
templateUrl: './avatar.html',
|
||||
styleUrls: ['./avatar.css'],
|
||||
standalone: false,
|
||||
})
|
||||
export class AvatarComponent {
|
||||
_avatarUrl: SafeResourceUrl | undefined
|
||||
uploading = false
|
||||
|
||||
@Input()
|
||||
set avatarUrl(url: string | null) {
|
||||
if (url) {
|
||||
this.downloadImage(url)
|
||||
}
|
||||
}
|
||||
|
||||
@Output() upload = new EventEmitter<string>()
|
||||
|
||||
constructor(
|
||||
private readonly supabase: SupabaseService,
|
||||
private readonly dom: DomSanitizer
|
||||
) {}
|
||||
|
||||
async downloadImage(path: string) {
|
||||
try {
|
||||
const { data } = await this.supabase.downLoadImage(path)
|
||||
if (data instanceof Blob) {
|
||||
this._avatarUrl = this.dom.bypassSecurityTrustResourceUrl(URL.createObjectURL(data))
|
||||
}
|
||||
} catch (error) {
|
||||
if (error instanceof Error) {
|
||||
console.error('Error downloading image: ', error.message)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async uploadAvatar(event: any) {
|
||||
try {
|
||||
this.uploading = true
|
||||
if (!event.target.files || event.target.files.length === 0) {
|
||||
throw new Error('You must select an image to upload.')
|
||||
}
|
||||
|
||||
const file = event.target.files[0]
|
||||
const fileExt = file.name.split('.').pop()
|
||||
const filePath = `${Math.random()}.${fileExt}`
|
||||
|
||||
await this.supabase.uploadAvatar(filePath, file)
|
||||
this.upload.emit(filePath)
|
||||
} catch (error) {
|
||||
if (error instanceof Error) {
|
||||
alert(error.message)
|
||||
}
|
||||
} finally {
|
||||
this.uploading = false
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
```html name=src/app/avatar/avatar.html
|
||||
<div>
|
||||
<img
|
||||
*ngIf="_avatarUrl"
|
||||
[src]="_avatarUrl"
|
||||
alt="Avatar"
|
||||
class="avatar image"
|
||||
style="height: 150px; width: 150px"
|
||||
/>
|
||||
</div>
|
||||
<div *ngIf="!_avatarUrl" class="avatar no-image" style="height: 150px; width: 150px"></div>
|
||||
<div style="width: 150px">
|
||||
<label class="button primary block" for="single">
|
||||
{{ uploading ? "Uploading ..." : "Upload" }}
|
||||
</label>
|
||||
<input
|
||||
style="visibility: hidden; position: absolute"
|
||||
type="file"
|
||||
id="single"
|
||||
accept="image/*"
|
||||
(change)="uploadAvatar($event)"
|
||||
[disabled]="uploading"
|
||||
/>
|
||||
</div>
|
||||
```
|
||||
|
||||
</$CodeTabs>
|
||||
|
||||
### Add the new widget
|
||||
|
||||
And then we can add the widget on top of the `AccountComponent` HTML template:
|
||||
|
||||
<$CodeTabs>
|
||||
|
||||
```html name=src/app/account.html
|
||||
<form [formGroup]="updateProfileForm" (ngSubmit)="updateProfile()" class="form-widget">
|
||||
<app-avatar [avatarUrl]="this.avatarUrl" (upload)="updateAvatar($event)"></app-avatar>
|
||||
<!-- input fields -->
|
||||
</form>
|
||||
```
|
||||
|
||||
</$CodeTabs>
|
||||
|
||||
And add an `updateAvatar` function along with an `avatarUrl` getter to the `AccountComponent` typescript file:
|
||||
|
||||
<$CodeTabs>
|
||||
|
||||
```ts name=src/app/account.ts
|
||||
@Component({
|
||||
selector: 'app-account',
|
||||
templateUrl: './account.html',
|
||||
styleUrls: ['./account.css'],
|
||||
})
|
||||
export class AccountComponent implements OnInit {
|
||||
// ...
|
||||
get avatarUrl() {
|
||||
return this.updateProfileForm.value.avatar_url as string
|
||||
}
|
||||
|
||||
async updateAvatar(event: string): Promise<void> {
|
||||
this.updateProfileForm.patchValue({
|
||||
avatar_url: event,
|
||||
})
|
||||
await this.updateProfile()
|
||||
}
|
||||
// ...
|
||||
}
|
||||
```
|
||||
|
||||
</$CodeTabs>
|
||||
|
||||
At this stage you have a fully functional application!
|
||||
@@ -34,7 +34,7 @@ cd expo-user-management
|
||||
Then let's install the additional dependencies: [supabase-js](https://github.com/supabase/supabase-js)
|
||||
|
||||
```bash
|
||||
npx expo install @supabase/supabase-js @react-native-async-storage/async-storage @rneui/themed
|
||||
npx expo install @supabase/supabase-js @rneui/themed expo-sqlite
|
||||
```
|
||||
|
||||
Now let's create a helper file to initialize the Supabase client.
|
||||
@@ -46,15 +46,15 @@ These variables are safe to expose in your Expo app since Supabase has
|
||||
scrollable
|
||||
size="large"
|
||||
type="underlined"
|
||||
defaultActiveId="async-storage"
|
||||
defaultActiveId="local-storage"
|
||||
queryGroup="auth-store"
|
||||
>
|
||||
<TabPanel id="async-storage" label="AsyncStorage">
|
||||
<TabPanel id="local-storage" label="LocalStorage">
|
||||
|
||||
<$CodeTabs>
|
||||
|
||||
```ts name=lib/supabase.ts
|
||||
import AsyncStorage from '@react-native-async-storage/async-storage'
|
||||
import 'expo-sqlite/localStorage/install';
|
||||
import { createClient } from '@supabase/supabase-js'
|
||||
|
||||
const supabaseUrl = YOUR_REACT_NATIVE_SUPABASE_URL
|
||||
@@ -62,7 +62,7 @@ These variables are safe to expose in your Expo app since Supabase has
|
||||
|
||||
export const supabase = createClient(supabaseUrl, supabasePublishableKey, {
|
||||
auth: {
|
||||
storage: AsyncStorage,
|
||||
storage: localStorage,
|
||||
autoRefreshToken: true,
|
||||
persistSession: true,
|
||||
detectSessionInUrl: false,
|
||||
|
||||
@@ -173,5 +173,5 @@ As mentioned in the Postgres [documentation](https://postgresqlco.nf/doc/en/para
|
||||
|
||||
### Constraints
|
||||
|
||||
- After **any** disk attribute change, there is a cooldown period of approximately six hours before you can make further adjustments. During this time, no changes are allowed. If you encounter throttling, you’ll need to wait until the cooldown period concludes before making additional modifications.
|
||||
- You can modify disk attributes up to **four times** within a rolling 24-hour window. A new modification can be initiated as soon as the previous one completes. If you reach this limit, you will encounter throttling and must wait for the rolling 24-hour window to permit further adjustments.
|
||||
- You can increase disk size but cannot decrease it.
|
||||
@@ -71,6 +71,7 @@ Vacuum operations can temporarily increase resource utilization, which may adver
|
||||
</Admonition>
|
||||
|
||||
Supabase projects have automatic vacuuming enabled, which ensures that these operations are performed regularly to keep the database healthy and performant.
|
||||
|
||||
It is possible to [fine-tune](https://www.percona.com/blog/2018/08/10/tuning-autovacuum-in-postgresql-and-autovacuum-internals/) the [autovacuum parameters](https://www.enterprisedb.com/blog/postgresql-vacuum-and-analyze-best-practice-tips), or [manually initiate](https://www.postgresql.org/docs/current/sql-vacuum.html) vacuum operations.
|
||||
Running a manual vacuum after deleting large amounts of data from your DB could help reduce the database size reported by Postgres.
|
||||
|
||||
@@ -86,7 +87,9 @@ Supabase uses network-attached storage to balance performance with scalability.
|
||||
|
||||
Projects on the Pro Plan and higher have auto-scaling disks.
|
||||
|
||||
Disk size expands automatically when the database reaches 90% of the allocated disk size. The disk is expanded to be 50% larger (for example, 8 GB -> 12 GB). Auto-scaling can only take place once every 6 hours. If within those 6 hours you reach 95% of the disk space, your project will enter read-only mode.
|
||||
Disk size expands automatically when the database reaches 90% of the allocated disk size. The disk is expanded to be 50% larger (for example, 8 GB -> 12 GB).
|
||||
|
||||
Auto-scaling is limited to four modifications within a rolling 24-hour window. While a new modification can be initiated immediately after the previous one completes, reaching the daily quota of four resizes will prevent further scaling until the rolling window allows it. If you reach 95% disk utilization and have exhausted your modification quota, your project will enter read-only mode.
|
||||
|
||||
<Admonition type="note">
|
||||
|
||||
@@ -100,7 +103,7 @@ Disk size can also be manually expanded on the [Database Settings page](/dashboa
|
||||
|
||||
You may want to import a lot of data into your database which requires multiple disk expansions. for example, uploading more than 1.5x the current size of your database storage will put your database into [read-only mode](#read-only-mode). If so, it is highly recommended you increase the disk size manually on the [Database Settings page](/dashboard/project/_/database/settings).
|
||||
|
||||
Due to restrictions on the underlying cloud provider, disk expansions can occur only once every six hours. During the six hour cool down window, the disk cannot be resized again.
|
||||
Due to restrictions on the underlying cloud provider, disk modifications are limited to four operations within a rolling 24-hour window. While a new modification can be initiated as soon as the previous one completes, you will be unable to make further adjustments if you reach this daily quota until the rolling 24-hour window permits it.
|
||||
|
||||
</Admonition>
|
||||
|
||||
|
||||
@@ -9,6 +9,12 @@ You are charged for having the feature [Advanced Multi-Factor Authentication Pho
|
||||
|
||||
<Admonition type="note">
|
||||
|
||||
The Advanced MFA Phone add-on is **not** covered by the [Spend Cap](/docs/guides/platform/cost-control#spend-cap).
|
||||
|
||||
</Admonition>
|
||||
|
||||
<Admonition type="note">
|
||||
|
||||
Additional charges apply for each SMS or WhatsApp message sent, depending on your third-party messaging provider (such as Twilio or MessageBird).
|
||||
|
||||
</Admonition>
|
||||
@@ -71,3 +77,19 @@ All projects have MFA Phone activated throughout the entire billing cycle.
|
||||
| **Subtotal** | | **<Price price="150" />** |
|
||||
| Compute Credits | | -<Price price="10" /> |
|
||||
| **Total** | | **<Price price="140" />** |
|
||||
|
||||
### Add-on disabled after a day
|
||||
|
||||
Project add-ons are billed in arrears based on how many hours you used them.
|
||||
If you remove the MFA Phone add-on, you are no longer billed from the time of removal onward.
|
||||
|
||||
| Line Item | Hours | Costs |
|
||||
| ----------------------------- | ----- | --------------------------- |
|
||||
| Pro Plan | - | <Price price="25" /> |
|
||||
| | | |
|
||||
| Compute Hours Micro Project 1 | 744 | <Price price="10" /> |
|
||||
| MFA Phone Hours Project 1 | 24 | <Price price="2.46" /> |
|
||||
| | | |
|
||||
| **Subtotal** | | **<Price price="37.46" />** |
|
||||
| Compute Credits | | -<Price price="10" /> |
|
||||
| **Total** | | **<Price price="27.46" />** |
|
||||
@@ -7,7 +7,11 @@ title: 'Manage Branching usage'
|
||||
|
||||
Each [Preview branch](/docs/guides/deployment/branching) is a separate environment with all Supabase services (Database, Auth, Storage, etc.). You're charged for usage within that environment—such as [Compute](/docs/guides/platform/manage-your-usage/compute), [Disk Size](/docs/guides/platform/manage-your-usage/disk-size), [Egress](/docs/guides/platform/manage-your-usage/egress), and [Storage](/docs/guides/platform/manage-your-usage/storage-size)—just like the project you branched from.
|
||||
|
||||
Usage by Preview branches counts toward your subscription plan's quota.
|
||||
<Admonition type="note">
|
||||
|
||||
Usage by Preview branches counts toward your subscription plan's quota. Branches are **not** covered by the [Spend Cap](/docs/guides/platform/cost-control#spend-cap).
|
||||
|
||||
</Admonition>
|
||||
|
||||
## How charges are calculated
|
||||
|
||||
|
||||
@@ -7,7 +7,11 @@ title: 'Manage Compute usage'
|
||||
|
||||
Each project on the Supabase platform includes a dedicated Postgres instance running on its own server. You are charged for the [Compute](/docs/guides/platform/compute-and-disk#compute) resources of that server, independent of your database usage.
|
||||
|
||||
Paused projects do not count towards Compute usage.
|
||||
<Admonition type="note">
|
||||
|
||||
Paused projects do not count towards Compute usage. Compute Hours are **not** covered by the [Spend Cap](/docs/guides/platform/cost-control#spend-cap).
|
||||
|
||||
</Admonition>
|
||||
|
||||
## How charges are calculated
|
||||
|
||||
@@ -104,6 +108,22 @@ The project's Compute size changes throughout the billing cycle.
|
||||
| Compute Credits | | -<Price price="10" /> |
|
||||
| **Total** | | **<Price price="29" />** |
|
||||
|
||||
### Projects not running for full month
|
||||
|
||||
One project is running for the entire month, two other projects were launched and deleted within a few days.
|
||||
We only bill for the hours while the project was running and billing stops once a project is deleted.
|
||||
Compute is always billed in arrears when your billing cycle resets.
|
||||
|
||||
| Line Item | Hours | Costs |
|
||||
| ----------------------------- | ----- | --------------------------- |
|
||||
| Pro Plan | - | <Price price="25" /> |
|
||||
| Compute Hours Micro Project 1 | 744 | <Price price="10" /> |
|
||||
| Compute Hours Micro Project 2 | 20 | <Price price="0.27" /> |
|
||||
| Compute Hours Micro Project 3 | 70 | <Price price="0.94" /> |
|
||||
| **Subtotal** | | **<Price price="36.21" />** |
|
||||
| Compute Credits | | -<Price price="10" /> |
|
||||
| **Total** | | **<Price price="26.21" />** |
|
||||
|
||||
## View usage
|
||||
|
||||
You can view Compute usage on the [organization's usage page](/dashboard/org/_/usage). The page shows the usage of all projects by default. To view the usage for a specific project, select it from the dropdown. You can also select a different time period.
|
||||
|
||||
@@ -7,6 +7,12 @@ title: 'Manage Custom Domain usage'
|
||||
|
||||
You can configure a [custom domain](/docs/guides/platform/custom-domains) for a project by enabling the [Custom Domain add-on](/dashboard/project/_/settings/addons?panel=customDomain). You are charged for all custom domains configured across your projects.
|
||||
|
||||
<Admonition type="note">
|
||||
|
||||
Custom Domains are **not** covered by the [Spend Cap](/docs/guides/platform/cost-control#spend-cap).
|
||||
|
||||
</Admonition>
|
||||
|
||||
## How charges are calculated
|
||||
|
||||
Custom domains are charged by the hour, meaning you are charged for the exact number of hours that a custom domain is active. If a custom domain is active for part of an hour, you are still charged for the full hour.
|
||||
@@ -63,6 +69,22 @@ All projects have a custom domain activated throughout the entire billing cycle.
|
||||
| Compute Credits | | -<Price price="10" /> |
|
||||
| **Total** | | **<Price price="55" />** |
|
||||
|
||||
### Add-on disabled after a day
|
||||
|
||||
Project add-ons are billed in arrears based on how many hours you used them.
|
||||
If you remove the custom domain add-on, you are no longer billed from the time of removal onward.
|
||||
|
||||
| Line Item | Hours | Costs |
|
||||
| ----------------------------- | ----- | --------------------------- |
|
||||
| Pro Plan | - | <Price price="25" /> |
|
||||
| | | |
|
||||
| Compute Hours Micro Project 1 | 744 | <Price price="10" /> |
|
||||
| Custom Domain Hours Project 1 | 24 | <Price price="0.33" /> |
|
||||
| | | |
|
||||
| **Subtotal** | | **<Price price="35.33" />** |
|
||||
| Compute Credits | | -<Price price="10" /> |
|
||||
| **Total** | | **<Price price="25.33" />** |
|
||||
|
||||
## Optimize usage
|
||||
|
||||
- Regularly check your projects and remove custom domains that are no longer needed
|
||||
|
||||
@@ -11,6 +11,12 @@ Refer to our [disk guide](/docs/guides/platform/compute-and-disk#disk) for detai
|
||||
|
||||
<Admonition type="note">
|
||||
|
||||
Disk IOPS Hours are **not** covered by the [Spend Cap](/docs/guides/platform/cost-control#spend-cap).
|
||||
|
||||
</Admonition>
|
||||
|
||||
<Admonition type="note">
|
||||
|
||||
Launching a Read Replica creates an additional database with its own dedicated disk. Read Replicas inherit the primary database's disk IOPS settings. You are charged for the provisioned IOPS of the Read Replica. Refer to [Manage Read Replica usage](/docs/guides/platform/manage-your-usage/read-replicas) for details on billing.
|
||||
|
||||
</Admonition>
|
||||
|
||||
@@ -130,3 +130,7 @@ To see how your disk usage is distributed across Database, WAL, and System categ
|
||||
## Reduce Disk size
|
||||
|
||||
To see how you can downsize your disk, refer to [Reducing disk size](/docs/guides/platform/database-size#reducing-disk-size)
|
||||
|
||||
## Exceeding Quotas
|
||||
|
||||
<$Partial path="billing/exceeding_usage_quotas.mdx" />
|
||||
@@ -11,6 +11,12 @@ Refer to our [disk guide](/docs/guides/platform/compute-and-disk#disk) for detai
|
||||
|
||||
<Admonition type="note">
|
||||
|
||||
Disk Throughput is **not** covered by the [Spend Cap](/docs/guides/platform/cost-control#spend-cap).
|
||||
|
||||
</Admonition>
|
||||
|
||||
<Admonition type="note">
|
||||
|
||||
Launching a Read Replica creates an additional database with its own dedicated disk. Read Replicas inherit the primary database's disk throughput settings. You are charged for the provisioned throughput of the Read Replica.
|
||||
|
||||
</Admonition>
|
||||
|
||||
@@ -81,3 +81,7 @@ In the Edge Function Invocations section, you can see how many invocations your
|
||||
}}
|
||||
zoomable
|
||||
/>
|
||||
|
||||
## Exceeding Quotas
|
||||
|
||||
<$Partial path="billing/exceeding_usage_quotas.mdx" />
|
||||
@@ -195,3 +195,7 @@ In the [Logs Explorer](/dashboard/project/_/logs/explorer) you can access Edge L
|
||||
- For update or insert queries, configure your ORM or queries to not return the entire row if not needed
|
||||
- When running manual backups through Supavisor, remove unneeded tables and/or reduce the frequency
|
||||
- Refer to the [Storage Optimizations guide](/docs/guides/storage/production/scaling#egress) for tips on reducing Storage Egress
|
||||
|
||||
## Exceeding Quotas
|
||||
|
||||
<$Partial path="billing/exceeding_usage_quotas.mdx" />
|
||||
@@ -9,6 +9,12 @@ You can assign a dedicated [IPv4 address](/docs/guides/platform/ipv4-address) to
|
||||
|
||||
<Admonition type="note">
|
||||
|
||||
IPv4 Hours are **not** covered by the [Spend Cap](/docs/guides/platform/cost-control#spend-cap).
|
||||
|
||||
</Admonition>
|
||||
|
||||
<Admonition type="note">
|
||||
|
||||
If the primary database has a dedicated IPv4 address configured, its Read Replicas are also assigned one, with charges for each.
|
||||
|
||||
</Admonition>
|
||||
@@ -93,6 +99,22 @@ The project has two Read Replicas and the IPv4 add-on enabled throughout the ent
|
||||
| Compute Credits | | -<Price price="10" /> |
|
||||
| **Total** | | **<Price price="72" />** |
|
||||
|
||||
### Add-on disabled after a day
|
||||
|
||||
Project add-ons are billed in arrears based on how many hours you used them.
|
||||
If you remove the IPv4 add-on, you are no longer billed from the time of removal onward.
|
||||
|
||||
| Line Item | Hours | Costs |
|
||||
| ----------------------------- | ----- | --------------------------- |
|
||||
| Pro Plan | - | <Price price="25" /> |
|
||||
| | | |
|
||||
| Compute Hours Micro Project 1 | 744 | <Price price="10" /> |
|
||||
| IPv4 Hours Project 1 | 24 | <Price price="0.13" /> |
|
||||
| | | |
|
||||
| **Subtotal** | | **<Price price="35.13" />** |
|
||||
| Compute Credits | | -<Price price="10" /> |
|
||||
| **Total** | | **<Price price="25.13" />** |
|
||||
|
||||
## Optimize usage
|
||||
|
||||
To see whether your database actually needs a dedicated IPv4 address, refer to [When you need the IPv4 add-on](/docs/guides/platform/ipv4-address#when-you-need-the-ipv4-add-on).
|
||||
@@ -7,6 +7,12 @@ title: 'Manage Log Drain usage'
|
||||
|
||||
You can configure log drains in the [project settings](/dashboard/project/_/settings/log-drains) to send logs to one or more destinations. You are charged for each log drain that is configured (referred to as [Log Drain Hours](/docs/guides/platform/manage-your-usage/log-drains#log-drain-hours)), the log events sent (referred to as [Log Drain Events](/docs/guides/platform/manage-your-usage/log-drains#log-drain-events)), and the [Egress](/docs/guides/platform/manage-your-usage/egress) incurred by the export—across all your projects.
|
||||
|
||||
<Admonition type="note">
|
||||
|
||||
Log Drains are **not** covered by the [Spend Cap](/docs/guides/platform/cost-control#spend-cap).
|
||||
|
||||
</Admonition>
|
||||
|
||||
## Log Drain Hours
|
||||
|
||||
### How charges are calculated
|
||||
@@ -77,6 +83,25 @@ The project has two log drains configured throughout the entire billing cycle wi
|
||||
| Compute Credits | | -<Price price="10" /> |
|
||||
| **Total** | | **<Price price="720.14" />** |
|
||||
|
||||
### Add-on disabled after a day
|
||||
|
||||
Project add-ons are billed in arrears based on how many hours you used them.
|
||||
If you remove the log drain add-on, you are no longer billed from the time of removal onward.
|
||||
|
||||
| Line Item | Hours | Costs |
|
||||
| ----------------------------- | -------- | --------------------------- |
|
||||
| Pro Plan | - | <Price price="25" /> |
|
||||
| | | |
|
||||
| Compute Hours Micro Project 1 | 744 | <Price price="10" /> |
|
||||
| | | |
|
||||
| Log Drain Hours Drain 1 | 24 | <Price price="1.97" /> |
|
||||
| Log Drain Events Drain 1 | 0 events | <Price price="0" /> |
|
||||
| Egress Drain 1 | 0 GB | <Price price="0" /> |
|
||||
| | | |
|
||||
| **Subtotal** | | **<Price price="36.97" />** |
|
||||
| Compute Credits | | -<Price price="10" /> |
|
||||
| **Total** | | **<Price price="26.97" />** |
|
||||
|
||||
## View usage
|
||||
|
||||
You can view Log Drain Events usage on the [organization's usage page](/dashboard/org/_/usage). The page shows the usage of all projects by default. To view the usage for a specific project, select it from the dropdown. You can also select a different time period.
|
||||
|
||||
@@ -136,3 +136,7 @@ In the Monthly Active SSO Users section, you can see the usage for the selected
|
||||
dark: '/docs/img/guides/platform/usage-mau-sso--dark.png',
|
||||
}}
|
||||
/>
|
||||
|
||||
## Exceeding Quotas
|
||||
|
||||
<$Partial path="billing/exceeding_usage_quotas.mdx" />
|
||||
+4
@@ -118,3 +118,7 @@ You can view Monthly Active Third-Party Users usage on the [organization's usage
|
||||
dark: '/docs/img/guides/platform/usage-mau-third-party--dark.png',
|
||||
}}
|
||||
/>
|
||||
|
||||
## Exceeding Quotas
|
||||
|
||||
<$Partial path="billing/exceeding_usage_quotas.mdx" />
|
||||
Loaded 100 of 712 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user