Merge branch 'master' into jeremiasmenichelli/ifcs-83-add-observability-to-searchv2-function-2

This commit is contained in:
Jeremias Menichelli authored and GitHub committed 2026-10-02 11:17:26 -03:00
commit e8c799c0f4
113 files changed
+9195 -4677

No files matched your search

@@ -0,0 +1,43 @@
name: ESLint Config Tests
on:
pull_request:
branches: ['master']
paths:
- 'packages/eslint-config-supabase/**/*'
concurrency:
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
cancel-in-progress: true
permissions:
contents: read
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with:
persist-credentials: false
sparse-checkout: |
packages/eslint-config-supabase
patches
- uses: pnpm/action-setup@0ebf47130e4866e96fce0953f49152a61190b271 # v6.0.9
name: Install pnpm
with:
run_install: false
- name: Use Node.js
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
with:
node-version-file: '.nvmrc'
cache: 'pnpm'
- name: Install deps
run: pnpm install --frozen-lockfile
- name: Run tests
run: pnpm --filter eslint-config-supabase run test
+109
View File
@@ -0,0 +1,109 @@
name: Decrease lint ratchet baselines
on:
schedule:
- cron: '0 0 * * SUN'
workflow_dispatch:
permissions:
contents: write
pull-requests: write
jobs:
decrease-baselines:
runs-on: blacksmith-4vcpu-ubuntu-2404
steps:
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with:
persist-credentials: false
sparse-checkout: |
apps/www
apps/docs
apps/design-system
apps/ui-library
apps/learn
packages
patches
- uses: pnpm/action-setup@0ebf47130e4866e96fce0953f49152a61190b271 # v6.0.9
name: Install pnpm
with:
run_install: false
- name: Use Node.js
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
with:
node-version-file: '.nvmrc'
cache: 'pnpm'
- name: Install deps
run: pnpm install --frozen-lockfile
- name: Generate token
id: app-token
uses: actions/create-github-app-token@bcd2ba49218906704ab6c1aa796996da409d3eb1 # v3.2.0
with:
client-id: ${{ vars.GH_AUTOFIX_APP_CLIENT_ID }}
private-key: ${{ secrets.GH_AUTOFIX_PRIVATE_KEY }}
permission-contents: write
permission-pull-requests: write
- name: Decrease ESLint ratchet baselines and open PR
env:
GH_TOKEN: ${{ steps.app-token.outputs.token }}
DEFAULT_BRANCH: ${{ github.event.repository.default_branch }}
run: |
set -eo pipefail
DEFAULT_BRANCH=${DEFAULT_BRANCH:-master}
BRANCH="bot/decrease-lint-ratchet-baselines"
git fetch origin "$DEFAULT_BRANCH" --depth=1
if git ls-remote --exit-code --heads origin "$BRANCH" > /dev/null 2>&1; then
git fetch origin "$BRANCH":"$BRANCH" --depth=1
git switch "$BRANCH"
git reset --hard "origin/$DEFAULT_BRANCH"
else
git switch --create "$BRANCH" "origin/$DEFAULT_BRANCH"
fi
# Keep going past an app that regressed so the others still get decreased;
# the job fails at the end if any app did.
failed=""
APPS="www docs design-system ui-library learn"
for app in $APPS; do
pnpm --filter "./apps/$app" run lint:ratchet --decrease-baselines || failed="$failed $app"
done
if git diff --quiet; then
echo "No baseline updates detected."
if [ -n "$failed" ]; then
echo "::error title=Ratchet regressions::New violations on $DEFAULT_BRANCH in:$failed"
exit 1
fi
exit 0
fi
git config user.name 'github-actions[bot]'
git config user.email 'github-actions[bot]@users.noreply.github.com'
for app in $APPS; do git add "apps/$app/.github/eslint-rule-baselines.json"; done
git commit --message "chore: decrease lint ratchet baselines"
git -c credential.helper= push --force "https://x-access-token:${GH_TOKEN}@github.com/${GITHUB_REPOSITORY}.git" "HEAD:${BRANCH}"
pr_url=$(gh pr list --state open --head "$BRANCH" --json url --jq '.[0].url // ""' 2>/dev/null || echo "")
if [ -z "$pr_url" ]; then
gh pr create \
--title "[bot] Decrease lint ratchet baselines" \
--body "Automated weekly decrease of lint ratchet baselines." \
--base "$DEFAULT_BRANCH" \
--head "$BRANCH"
else
gh pr comment "$pr_url" --body "Updated lint ratchet baselines with the latest weekly decreases."
fi
if [ -n "$failed" ]; then
echo "::error title=Ratchet regressions::New violations on $DEFAULT_BRANCH in:$failed"
exit 1
fi
+85
View File
@@ -0,0 +1,85 @@
name: Ratchet lint checks
on:
pull_request:
branches:
- master
concurrency:
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
cancel-in-progress: true
permissions:
contents: read
pull-requests: read
jobs:
changes:
runs-on: ubuntu-latest
outputs:
apps: ${{ steps.filter.outputs.changes }}
steps:
- uses: dorny/paths-filter@de90cc6fb38fc0963ad72b210f1f284cd68cea36 # v3.0.2
id: filter
with:
filters: |
shared: &shared
- 'packages/**'
- 'pnpm-lock.yaml'
www:
- *shared
- 'apps/www/**'
docs:
- *shared
- 'apps/docs/**'
design-system:
- *shared
- 'apps/design-system/**'
ui-library:
- *shared
- 'apps/ui-library/**'
learn:
- *shared
- 'apps/learn/**'
ratchet:
needs: changes
if: ${{ needs.changes.outputs.apps != '[]' }}
# Uses larger hosted runner as it significantly decreases build times
runs-on: blacksmith-4vcpu-ubuntu-2404
strategy:
fail-fast: false
matrix:
app: ${{ fromJSON(needs.changes.outputs.apps) }}
exclude:
- app: shared
steps:
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with:
persist-credentials: false
sparse-checkout: |
apps/${{ matrix.app }}
packages
patches
- uses: pnpm/action-setup@0ebf47130e4866e96fce0953f49152a61190b271 # v6.0.9
name: Install pnpm
with:
run_install: false
- name: Use Node.js
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
with:
node-version-file: '.nvmrc'
cache: 'pnpm'
- name: Install deps
run: pnpm install --frozen-lockfile
# pnpm runs the script from the app directory, which is where the
# baselines were generated, so per-file keys line up.
- name: Run ratchet script
env:
APP: ${{ matrix.app }}
run: pnpm --filter "./apps/$APP" run lint:ratchet
@@ -103,7 +103,7 @@ jobs:
run: |
set -euo pipefail
zero_rules=$(jq -r '.rules | to_entries | map(select(.value == 0) | .key) | join(", ")' apps/studio/.github/eslint-rule-baselines.json)
zero_rules=$(jq -r '.rules | to_entries | map(select(.value == 0 and (.key | startswith("shadcn/") | not)) | .key) | join(", ")' apps/studio/.github/eslint-rule-baselines.json)
if [ -z "$zero_rules" ]; then
echo "No rules dropped to a baseline of 0; nothing to notify."
@@ -6,6 +6,8 @@ on:
- master
paths:
- 'apps/studio/**'
- 'packages/**'
- 'pnpm-lock.yaml'
concurrency:
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
+1 -1
View File
@@ -42,7 +42,7 @@ pnpm api:codegen # platform Management API types → packages/api-ty
## CI
Every PR must pass typecheck + lint (one workflow), Prettier, and a typos check. Other checks are path-filtered: Studio unit tests/build and the lint ratchet (ESLint warning count must not increase) run on `apps/studio/**` changes; app-specific test suites run on their own paths.
Every PR must pass typecheck + lint (one workflow), Prettier, and a typos check. Other checks are path-filtered: Studio unit tests/build and the lint ratchet (ESLint warning count must not increase) run on `apps/studio/**` changes and the Tailwind class rules (`shadcn/*`) for `www`, `docs`, `design-system`, `ui-library`, and `learn`; app-specific test suites run on their own paths.
Never hand-edit generated files: `packages/api-types/types/**`, `**/routeTree.gen.ts`, `**/__generated__/**`, `apps/docs/features/docs/generated/**`, `apps/www/.generated/**`, `supabase/functions/common/database-types.ts`, `apps/docs/content/_partials/access-control/scoped_pat_*.mdx` (run `make -C apps/docs/spec generate.partials.access-control`).
+56
View File
@@ -0,0 +1,56 @@
{
"rules": {
"shadcn/no-arbitrary-values": 42,
"shadcn/no-unknown-classes": 8,
"shadcn/no-raw-colors": 22
},
"ruleFiles": {
"shadcn/no-arbitrary-values": {
"components/code-block-wrapper.tsx": 1,
"components/code-fragment.tsx": 7,
"components/color-palette.tsx": 2,
"components/command-menu.tsx": 1,
"components/component-preview.tsx": 12,
"components/mdx-components.tsx": 2,
"registry/default/example/admonition-button-split.tsx": 2,
"registry/default/example/assistant-chat-commands.tsx": 2,
"registry/default/example/button-split-dropdown.tsx": 2,
"registry/default/example/chart-tooltip-demo.tsx": 2,
"registry/default/example/command-dialog.tsx": 1,
"registry/default/example/connect-interstitial-shared.tsx": 2,
"registry/default/example/drawer-demo.tsx": 1,
"registry/default/example/page-layout-edge-function.tsx": 1,
"registry/default/example/toc-demo.tsx": 1,
"registry/default/example/toc-single-demo.tsx": 1,
"registry/default/example/typography-inline-code.tsx": 2
},
"shadcn/no-unknown-classes": {
"components/code-block-wrapper.tsx": 1,
"components/code-fragment.tsx": 1,
"components/component-preview.tsx": 1,
"components/mdx-components.tsx": 2,
"registry/default/example/data-grid-demo.tsx": 1,
"registry/default/example/data-grid-empty-state.tsx": 1,
"registry/default/example/form-patterns-sidepanel.tsx": 1
},
"shadcn/no-raw-colors": {
"components/code-block-wrapper.tsx": 2,
"components/copy-button.tsx": 3,
"registry/default/example/calendar-form.tsx": 1,
"registry/default/example/calendar-react-hook-form.tsx": 1,
"registry/default/example/checkbox-form-multiple.tsx": 1,
"registry/default/example/checkbox-form-single.tsx": 1,
"registry/default/example/combobox-form.tsx": 1,
"registry/default/example/connect-interstitial-shared.tsx": 3,
"registry/default/example/date-picker-form.tsx": 1,
"registry/default/example/input-form.tsx": 1,
"registry/default/example/input-otp-form.tsx": 1,
"registry/default/example/radio-group-card-form.tsx": 1,
"registry/default/example/radio-group-form.tsx": 1,
"registry/default/example/radio-group-stacked-form.tsx": 1,
"registry/default/example/select-form.tsx": 1,
"registry/default/example/switch-form.tsx": 1,
"registry/default/example/textarea-form.tsx": 1
}
}
}
+1
View File
@@ -14,6 +14,7 @@
"build": "run-p build:registry build:content && pnpm build:next",
"start": "next start",
"lint": "eslint .",
"lint:ratchet": "tsx node_modules/eslint-config-supabase/ratchet-eslint-rules.ts --rules-file node_modules/eslint-config-supabase/ratchet-rules.json",
"clean": "rimraf .next .turbo tsconfig.tsbuildinfo .contentlayer .velite __registry__",
"typecheck": "run-p build:registry build:content && tsc --noEmit -p tsconfig.json"
},
+39
View File
@@ -0,0 +1,39 @@
{
"rules": {
"shadcn/no-arbitrary-values": 38,
"shadcn/no-unknown-classes": 19,
"shadcn/no-raw-colors": 3
},
"ruleFiles": {
"shadcn/no-arbitrary-values": {
"app/contributing/ContributingToC.tsx": 1,
"components/Feedback/Feedback.tsx": 15,
"components/GuidesSidebar.tsx": 1,
"components/StepHike/index.tsx": 1,
"features/docs/Reference.sections.tsx": 4,
"features/docs/Reference.ui.tsx": 4,
"features/ui/AgentWatchSchedule.tsx": 2,
"features/ui/CodeBlock/CodeBlock.client.tsx": 7,
"features/ui/PromptPanel.tsx": 3
},
"shadcn/no-unknown-classes": {
"app/guides/getting-started/ai-skills/AiSkillsIndex.tsx": 1,
"app/guides/local-development/cli/config/page.tsx": 1,
"components/GithubCard.tsx": 1,
"components/Navigation/NavigationMenu/GlobalMobileMenu.tsx": 1,
"components/Navigation/NavigationMenu/NavigationMenuCliList.tsx": 2,
"components/Navigation/NavigationMenu/NavigationMenuRefListItems.tsx": 2,
"components/Navigation/SideBar.tsx": 1,
"components/Params.tsx": 1,
"components/ProjectConfigVariables/ProjectConfigVariables.tsx": 2,
"features/docs/Reference.sections.tsx": 2,
"layouts/MainSkeleton.tsx": 1,
"layouts/ref/RefSubLayout.tsx": 2,
"layouts/ref/RefSubLayoutNonFunc.tsx": 2
},
"shadcn/no-raw-colors": {
"features/ui/CodeBlock/CodeBlock.tsx": 1,
"features/ui/PromptPanel.tsx": 2
}
}
}
@@ -2519,6 +2519,14 @@ export const local_development: NavMenuConstant = {
url: undefined,
items: [
{ name: 'Database migrations', url: '/guides/local-development/database-migrations' },
{
name: 'Running multiple local projects',
url: '/guides/local-development/running-multiple-local-projects' as `/${string}`,
},
{
name: 'Docker and native runtimes',
url: '/guides/local-development/docker-and-native-runtimes' as `/${string}`,
},
{
name: 'Declarative database schemas',
url: '/guides/local-development/declarative-database-schemas' as `/${string}`,
@@ -3093,16 +3101,16 @@ export const telemetry: NavMenuConstant = {
],
},
{
name: 'Hire an agent',
name: 'Agent prompts',
items: [
{ name: 'Set up an agent', url: '/guides/observability/automate-with-agents' },
{ name: 'Health monitor', url: '/guides/observability/automate-with-agents/health' },
{ name: 'Security monitor', url: '/guides/observability/automate-with-agents/security' },
{ name: 'Overview', url: '/guides/observability/automate-with-agents' },
{ name: 'Health', url: '/guides/observability/automate-with-agents/health' },
{ name: 'Security', url: '/guides/observability/automate-with-agents/security' },
{
name: 'Performance monitor',
name: 'Performance',
url: '/guides/observability/automate-with-agents/performance',
},
{ name: 'Resource monitor', url: '/guides/observability/automate-with-agents/usage' },
{ name: 'Resources', url: '/guides/observability/automate-with-agents/usage' },
],
},
{
+4
View File
@@ -23,4 +23,8 @@ See how these tools perform on real Supabase tasks in [Supabase Evals](/evals),
- **Plugin**: a single install that bundles the MCP server and Agent Skills together for a specific agent.
- **Prompts**: static prompt files you copy into your project for agents that don't support MCP, plugins, or skills natively.
## Local projects for parallel agents
Agents that work in separate git worktrees or branches each need their own database. Otherwise migrations and seed data from one task affect another. With the experimental `supabase stack` commands turned on, `supabase start` gives each worktree or branch its own local Supabase project with its own ports and data. Agents that share one checkout and branch share a local project, so give each agent its own worktree. The commands also run without Docker on Linux and on macOS on Apple silicon, which covers agent sandboxes that have no container engine. See [Running multiple local projects](/docs/guides/local-development/running-multiple-local-projects) and [Docker and native runtimes](/docs/guides/local-development/docker-and-native-runtimes).
<ContentListings id="ai-tools-building-into-app" />
+88 -8
View File
@@ -70,6 +70,7 @@ The Supabase MCP server provides tools organized into feature groups. All groups
- `list_edge_functions` - List all Edge Functions
- `get_edge_function` - Get a specific Edge Function
- `deploy_edge_function` - Deploy an Edge Function
- `create_edge_function_secret` - When available, request secret entry in the Supabase Dashboard. See [Edge Function secrets](#edge-function-secrets) for requirements.
### Account management
@@ -82,7 +83,7 @@ Disabled when using project-scoped mode (`project_ref` parameter).
- `list_projects` / `get_project` - List or get project details
- `create_project` / `pause_project` / `restore_project` - Manage projects
- `list_organizations` / `get_organization` - Organization management
- `get_cost` / `confirm_cost` - Cost information
- `get_cost` / `confirm_cost` - Cost information for tools that use the [legacy cost confirmation workflow](#cost-confirmation)
### Docs
@@ -104,24 +105,102 @@ Requires a paid plan.
- `list_storage_buckets` - List storage buckets
- `get_storage_config` / `update_storage_config` - Storage configuration
## Elicitations
[Elicitation](https://modelcontextprotocol.io/specification/2026-07-28/client/elicitation) lets an MCP server pause a tool call to ask you for input or confirmation. These flows require both server availability and an MCP client that supports the appropriate elicitation mode: forms for [cost confirmation](#cost-confirmation) and [SQL confirmations](#destructive-sql-confirmations), or URLs for [secret entry](#edge-function-secrets).
Server elicitation is separate from your MCP client's own approval of tool calls. It does not replace manual approval or the [security recommendations](#recommendations).
Treat elicitations as a guardrail, not a guarantee of human review or approval. Some clients support hooks or rules that answer elicitations automatically.
### Client support
Elicitations require your MCP client to support the corresponding mode for the request.
| Mode | Supabase use |
| ---- | ----------------------------------------------------------------------------------------------------------- |
| Form | [Cost confirmation](#cost-confirmation) and [destructive SQL confirmations](#destructive-sql-confirmations) |
| URL | [Edge Function secret entry](#edge-function-secrets) in the Supabase Dashboard |
Support for one mode does not imply support for the other. Check your client's documentation for the modes it supports. The MCP specification revision alone does not determine which flows your connection uses.
### Cost confirmation
When your connection uses form-based cost confirmation, your MCP client shows the expected cost before your agent takes an action that incurs additional charges. The action proceeds only on explicit approval.
Cost confirmation applies to `create_project` and `create_branch`. When your agent calls one of these tools, the tool call pauses and your client displays the resource, the standard rate, and the billing interval, along with controls to accept or decline. The exact labels vary by client.
- Accepting resumes the same tool call to create the resource.
- Declining or dismissing the confirmation creates nothing, and the agent receives a result that it can relay to you.
- Confirmations are valid for a short time. If one expires, nothing is created. The agent can call the tool again to request a fresh confirmation.
- The cost is checked again immediately before creation. If it has changed to a different nonzero amount since you confirmed, you receive a fresh confirmation that shows the updated cost.
Project creation with a zero-cost quote does not trigger a cost confirmation prompt. When branch creation uses form-based cost confirmation, it asks you to confirm the standard rate before any allowances or exemptions are applied.
If form-based cost confirmation is unavailable, unsupported by your client, or [skipped for the tool](#skip-form-confirmations), the legacy cost confirmation workflow applies: the agent quotes the cost in chat, uses `get_cost` and `confirm_cost`, and passes the returned `confirm_cost_id` to `create_project` or `create_branch`. These helpers are account tools and are not exposed in project-scoped connections. Skipping the form does not approve the cost or avoid charges.
To check which flow a creation tool uses, ask your client to list the available Supabase tools and the resource types accepted by `get_cost` and `confirm_cost`. The resource types accepted by these helpers use the legacy cost confirmation workflow; another resource type can use form-based cost confirmation on the same connection. Their absence does not prove that form-based cost confirmation is active: project scoping, account feature settings, or server availability can also hide them.
Your MCP client controls how the confirmation is collected. Some clients support hooks or rules that answer elicitations automatically, so treat cost confirmation as a guardrail rather than proof that a person approved each action.
Troubleshooting: If you expect a dialog and don't see one, see [Cost confirmations do not appear in your MCP client](/docs/guides/troubleshooting/cost-confirmations-do-not-appear-in-your-mcp-client-mVq3Lp).
### Destructive SQL confirmations
When SQL elicitation is available and your client supports forms, `execute_sql` and `apply_migration` ask for confirmation when they detect destructive SQL. The `execute_sql` confirmation applies only outside read-only mode. SQL not classified as destructive proceeds without this additional warning. Treat this elicitation as a guardrail - it may not catch every destructive operation and is not a security guarantee.
If SQL elicitation is unavailable, unsupported by your client, or skipped with [`skip_elicitations`](#skip-form-confirmations), SQL follows the existing execution path without an additional MCP confirmation. Read-only restrictions and permissions still apply. Keep the [security recommendations](#recommendations) in place.
Troubleshooting: If you expect a dialog and don't see one, see [SQL confirmations do not appear in your MCP client](/docs/guides/troubleshooting/sql-confirmations-do-not-appear-in-your-mcp-client-sQf7Kp).
### Edge Function secrets
`create_edge_function_secret` is available only when the server offers it, your client supports URL elicitation, Edge Functions tools are enabled, and the connection is not read-only. You also need permission to read and write Edge Function secrets for the project.
Enter secret values only in the Supabase Dashboard, never in chat, model input, or MCP tool arguments. The tool requests the secret name and project, not the secret value.
1. Ask your AI assistant to add an Edge Function secret, specifying the project and secret name without providing the value.
2. Open the Supabase Dashboard URL presented by your MCP client.
3. Enter and save the secret value in the Dashboard.
4. Return to your MCP client and confirm that you saved it.
Save the secret in the Dashboard before confirming in your MCP client. If you cancel the request, this does not undo a secret you already saved in the Dashboard.
Troubleshooting: If the tool is unavailable or the Dashboard flow is incomplete, see [Edge Function secret collection is unavailable or incomplete](/docs/guides/troubleshooting/edge-function-secret-collection-is-unavailable-or-incomplete-eFs4Nx).
## Configuration options
The [configuration panel above](#configure-your-ai-tool) can set these options for you. If you prefer to configure manually, the following URL query parameters are available:
The [configuration panel](#configure-your-ai-tool) can set project scope, read-only mode, and feature groups. For hosted connections, you can also set `skip_elicitations` in the panel or edit your MCP server URL manually. The following URL query parameters are available:
| Parameter | Description | Example |
| ------------------- | ---------------------------------------------------- | ------------------------- |
| `read_only=true` | Execute all queries as a read-only Postgres user | `?read_only=true` |
| `project_ref=<id>` | Scope to a specific project (disables account tools) | `?project_ref=abc123` |
| `features=<groups>` | Enable only specific tool groups (comma-separated) | `?features=database,docs` |
| Parameter | Description | Example |
| --------------------------- | ----------------------------------------------------------------------------------------------------- | ------------------------------------------------ |
| `read_only=true` | Execute all queries as a read-only Postgres user | `?read_only=true` |
| `project_ref=<id>` | Scope to a specific project (disables account tools) | `?project_ref=abc123` |
| `features=<groups>` | Enable only specific tool groups (comma-separated) | `?features=database,docs` |
| `skip_elicitations=<tools>` | Skip form confirmations for the named tools. See [Skip form confirmations](#skip-form-confirmations). | `?skip_elicitations=execute_sql,apply_migration` |
Parameters can be combined: <code><CustomContent data="mcp:servers">remote</CustomContent>?project_ref=abc123&read_only=true</code>
<Admonition type="note">
When using [Supabase CLI](/docs/guides/local-development) for local development, the MCP server is available at <code><CustomContent data="mcp:servers">local</CustomContent></code>.
When using [Supabase CLI](/docs/guides/local-development) for local development, the MCP server is available at <code><CustomContent data="mcp:servers">local</CustomContent></code>. With the experimental `[experimental] stack` setting on, local projects use assigned ports instead, so read the MCP URL from the output of `supabase status`. See [Running multiple local projects](/docs/guides/local-development/running-multiple-local-projects).
</Admonition>
### Skip form confirmations
Use one `skip_elicitations` parameter with a comma-separated list of tool names: `create_project`, `create_branch`, `execute_sql`, or `apply_migration`.
Names are case-sensitive. Omit the parameter or leave it empty to skip none; this does not enable confirmations that the server does not offer. Invalid names are rejected. There is no boolean or `all` switch, and `create_edge_function_secret` is not supported by this parameter.
In the panel, skip choices are hidden for read-only and local connections. Cost confirmation choices require a connection not scoped to a project with account helpers enabled; `create_branch` also requires branching. SQL-confirmation choices require the database feature group. These limits apply to the panel, not to manually supplied URL parameters.
Skip cost confirmation: <code><CustomContent data="mcp:servers">remote</CustomContent>?skip_elicitations=create_project,create_branch</code>
Skip SQL confirmations: <code><CustomContent data="mcp:servers">remote</CustomContent>?skip_elicitations=execute_sql,apply_migration</code>
Skipping cost confirmations retains the [legacy cost confirmation workflow](#cost-confirmation). Skipping SQL confirmations uses the existing SQL execution path without an additional MCP confirmation. Neither option bypasses authentication, permissions, read-only restrictions, or charges.
## Manual authentication
By default the hosted Supabase MCP server uses [dynamic client registration](https://modelcontextprotocol.io/specification/2025-06-18/basic/authorization#dynamic-client-registration) to authenticate with your Supabase org. This means that you don't need to manually create a personal access token (PAT) or OAuth app to use the server.
@@ -193,6 +272,7 @@ We recommend the following best practices to mitigate security risks when using
- **Read-only mode**: Set unattended monitoring and diagnostic routines to [read-only](#configuration-options) mode, which executes SQL queries as a read-only Postgres user.
- **Project scoping**: Scope your MCP server to a [specific project](#configuration-options), 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.
- **Cost confirmation**: When your connection uses cost confirmation dialogs, the server asks for [confirmation through your client](#cost-confirmation) before it creates resources that incur charges. This is a guardrail on top of authentication and authorization, not a replacement for them.
- **Feature groups**: Restrict which [tool groups](#available-tools) are available using the `features` [configuration option](#configuration-options). This helps reduce the attack surface and limits the actions that LLMs can perform to only those that you need.
## On GitHub
@@ -12,9 +12,22 @@ You should only enable retries if your requests fail with network errors (e.g. 5
## Built-in retries for PostgREST queries
Starting with `supabase-js` v2.102.0, PostgREST queries (`.from()`, `.rpc()`) include built-in automatic retries for transient errors. Retries are **enabled by default** and use exponential backoff with jitter.
Starting with `supabase-js` v2.102.0, PostgREST queries (`.from()`, `.rpc()`) include built-in automatic retries for transient errors. Retries are **enabled by default** and use exponential backoff.
Retryable errors include HTTP status codes 408 (Request Timeout), 409 (Conflict), 503 (Service Unavailable), and 504 (Gateway Timeout), as well as network failures. Only idempotent HTTP methods (GET, HEAD, OPTIONS) and POST requests (used by PostgREST) are retried.
`supabase-js` retries only idempotent HTTP methods: GET, HEAD, and OPTIONS. The built-in retry policy doesn't retry POST, PATCH, PUT, or DELETE. For the methods it does retry, it retries on HTTP status codes 503 Service Unavailable and 520 Unknown Error, and on network failures.
Because `.rpc()` sends POST by default, RPC calls aren't retried. Pass `{ get: true }` or `{ head: true }` to send the call as GET or HEAD, which the built-in retries cover.
### Set a request timeout
Retries only run after a request fails. A request that hangs never fails, so it never reaches the retry logic. This can happen during a slow upstream timeout. Pass an `AbortSignal` to cap how long a request can take:
```javascript
const { data, error } = await supabase
.from('your_table')
.select('*')
.abortSignal(AbortSignal.timeout(10_000))
```
### Disable built-in retries
@@ -18,7 +18,7 @@ Insert-only tables don't require a primary key or replica identity. Updates and
## Prepare DuckLake resources [#understand-the-ducklake-components]
Prepare a Postgres catalog, object storage, and a compatible query engine:
DuckLake stores metadata in a Postgres catalog and data in object storage. With **Select Supabase projects**, choose a project for the catalog, then a project and bucket for storage. The catalog and storage can use the same project. Pipelines creates the connection credentials. With **Enter connection details**, you provide the catalog URL and storage credentials. You also need a compatible query engine to read the replicated data.
| Component | Purpose |
| ---------------- | ----------------------------------------------------------------------------------------------------------------------------------- |
@@ -30,56 +30,54 @@ You can query replicated tables, but treat them and their underlying catalog and
## Configure DuckLake as a destination [#choose-a-configuration-mode]
Choose a mode below for its resource requirements and configuration steps.
Follow [Set up Pipelines](/docs/guides/database/replication/pipelines#setup-overview). In **Add pipeline**, select **DuckLake**, enter a **Pipeline name**, choose a **Publication**, and set **Initial sync**. Then choose how to configure the catalog and storage.
Follow [Set up Pipelines](/docs/guides/database/replication/pipelines#setup-overview), select **DuckLake**, then choose **Use Supabase** or **Custom parameters** for the catalog and storage.
### Select Supabase projects [#use-supabase]
### Use Supabase
Use this mode to back the DuckLake with Supabase projects. Pipelines provisions its own catalog and object-storage credentials when you create the destination.
Choose Supabase projects for the Postgres catalog and Storage bucket. Pipelines creates the connection credentials when you create the destination.
Before you begin:
- Choose active, healthy, non-branch projects from the same organization for the catalog and storage. You can use the same project for both.
- Make sure your organization role can administer SQL in the catalog project and Storage in the storage project.
- Create a private standard Storage bucket, or create one from the destination form.
- Choose a metadata schema unique to this DuckLake. Use only letters, numbers, and underscores.
- Choose a Files bucket for DuckLake data. You can create a private bucket from the destination form.
- Choose an unused metadata schema name for this DuckLake. Pipelines creates the schema and its tables. Use only letters, numbers, and underscores. If the catalog project already uses the default `ducklake` schema for Warehouse, enter a different name.
- Keep catalog and storage in the same region when possible, near the [managed pipeline region](/docs/guides/database/replication/pipelines#region).
To configure the destination:
1. Select **Use Supabase**.
2. Choose the **Catalog project**, **Pool size**, and **Metadata schema**. Pool size allows `1` to `6` concurrent DuckDB connections; the default is `4`.
3. Choose the **Storage project** and private **Bucket**.
4. Click **Create and start pipeline** and complete the validation and cost confirmations.
1. Select **Select Supabase projects** under **Configuration method**.
2. Choose the **Catalog project** and **Metadata schema**.
3. Choose the **Storage project** and **Bucket**. Select **New bucket** at the bottom of the bucket list if you haven't created one yet.
4. Optionally adjust **Pool size** under **Advanced settings**. It allows 1 to 6 concurrent DuckDB connections; the default is 4.
5. Click **Start pipeline**. Review any validation warnings, then confirm the estimated cost with **Create and start pipeline**.
Credential-provisioning warnings are expected before creation: catalog and Storage credentials are provisioned when you save the destination. Review the selected resources before proceeding.
### Custom parameters
### Enter connection details [#custom-parameters]
Use this mode with a Postgres catalog and S3-compatible object storage that you control.
Use this mode to connect an existing Postgres database and S3-compatible object storage. You provide their connection details and credentials.
Prepare the following resources:
1. A Postgres database reachable from managed Pipelines. Create a dedicated user that can create and modify the DuckLake metadata schema and its tables.
1. A Postgres database reachable from managed Pipelines. Create a dedicated user with permission to create the DuckLake metadata schema and its tables.
2. An S3-compatible bucket and a dedicated prefix for this DuckLake.
3. Object-storage credentials that can list, read, write, and delete objects under that prefix. Delete access is required for managed file cleanup.
Use a new catalog metadata schema and data prefix for each destination. Reusing an existing schema or prefix can mix the metadata or files of different DuckLakes.
Choose an unused metadata schema name and a new data prefix for each destination. Pipelines creates the schema and its tables. Initialising a DuckLake catalog in that schema beforehand triggers a validation warning; reusing its schema or prefix can mix metadata or files from different DuckLakes.
Configure these fields in the destination form:
Select **Enter connection details** under **Configuration method**, then configure these fields:
- **Catalog URL**: A `postgres://` or `postgresql://` connection URL, including credentials and an `sslmode` appropriate for your provider
- **Catalog URL**: A `postgres://` or `postgresql://` URL for your existing database, including credentials. If your provider requires TLS, append `?sslmode=require` after the database name, or `&sslmode=require` if the URL already has query parameters. Keep the database name from your provider's URL; it doesn't need to be `ducklake_catalog`.
- **Data path**: An `s3://<bucket>/<prefix>` URL
- **Pool size**: From `1` to `6`; the default is `4`
- **S3 access key ID** and **S3 secret access key**: A credential pair for the data path
- **S3 endpoint**: A publicly reachable provider endpoint without `http://` or `https://`
- **S3 region**: The storage provider's region
- **S3 endpoint**: The provider endpoint without `http://` or `https://`
- **S3 URL style**: `path` for Supabase Storage and many S3-compatible providers, or `vhost` for virtual-host-style addressing
- **Use SSL**: Keep enabled for production endpoints
- **S3 URL style**: Path style if the bucket is in the URL path, or virtual-host style if the bucket is in the hostname
- **Use SSL**: Choose **On** for HTTPS, or **Off** if your provider requires HTTP
- **Metadata schema**: A unique Postgres schema for DuckLake metadata, using only letters, numbers, and underscores
Click **Create and start pipeline** and complete the validation and cost confirmations.
Optionally adjust **Pool size** under **Advanced settings**. It allows 1 to 6 concurrent DuckDB connections; the default is 4. Click **Start pipeline**, review the validation results, then confirm the estimated cost with **Create and start pipeline**.
The catalog URL and storage credentials are stored as secrets and aren't returned after creation. When editing the destination, leave a secret field empty to keep its stored value, or enter a new value to replace it.
@@ -95,7 +93,7 @@ A source `TRUNCATE` truncates the DuckLake table. A [table restart](/docs/guides
Connect DuckDB with its `ducklake` extension, or another compatible engine, to the same catalog and storage path. Query through the catalog; reading raw Parquet files can miss inlined changes, delete files, and the current snapshot.
For **Use Supabase** mode, create separate read credentials for the selected catalog and Storage projects. The writer credentials generated for Pipelines aren't exposed. For **Custom parameters**, use separate read-only credentials when your catalog and storage provider support them.
If you selected Supabase projects, create separate read credentials for the catalog and Storage projects. The writer credentials generated for Pipelines aren't exposed. If you entered connection details, use separate read-only credentials when your catalog and storage provider support them.
After attaching the catalog under an alias such as `my_ducklake`, source schemas and tables are available as qualified DuckLake tables:
@@ -163,14 +161,14 @@ For type changes, unsupported changes, and interrupted schema changes, see the s
## Troubleshooting
| Issue | Resolution |
| ---------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Credential-provisioning warnings | Expected in [Use Supabase](#use-supabase) mode before saving. Review the selected resources. |
| Catalog or storage validation fails | Check [custom parameters](#custom-parameters), credentials, connectivity, and permissions for the configured prefix. Local `file://` paths are unsupported. |
| Metadata schema exists | Choose a new schema, unless intentionally reusing the same DuckLake and its corresponding data path. |
| Inserts work but updates or deletes fail | Check [replica identity and published columns](#source-table-requirements). |
| Queries omit changes or deleted rows | [Query through the catalog](#query-the-destination) with credentials for both catalog and storage. |
| A schema change fails | Review [supported changes](#schema-change-support). Don't modify catalog tables or files manually. |
| Issue | Resolution |
| ---------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Credential-provisioning warnings | Expected when you [select Supabase projects](#use-supabase) before saving. Review the selected resources. |
| Catalog or storage validation fails | Check the [connection details](#custom-parameters), credentials, connectivity, and permissions for the configured prefix. Local `file://` paths are unsupported. |
| Metadata schema exists | Choose a new schema, unless intentionally reusing the same DuckLake and its corresponding data path. |
| Inserts work but updates or deletes fail | Check [replica identity and published columns](#source-table-requirements). |
| Queries omit changes or deleted rows | [Query through the catalog](#query-the-destination) with credentials for both catalog and storage. |
| A schema change fails | Review [supported changes](#schema-change-support). Don't modify catalog tables or files manually. |
Use [pipeline monitoring](/docs/guides/database/replication/pipelines-monitoring) to inspect errors. For unresolved failures, [contact support](/dashboard/support/new) with the pipeline ID and error details.
@@ -22,7 +22,7 @@ height={933}
## Set up a local environment
The first step is to set up your local repository with the Supabase CLI:
The first step is to set up your local repository with the Supabase CLI. Each developer, and each feature branch or git worktree, can have its own local database. To run several local projects at the same time on one machine, see [Running multiple local projects](/docs/guides/local-development/running-multiple-local-projects).
```bash
supabase init
@@ -7,13 +7,15 @@ To develop your applications using the locally running Supabase stack, you'll ne
<Admonition type="note">
A container manager compatible with Docker APIs is a prerequisite:
A container manager compatible with Docker APIs is a prerequisite for `supabase start`:
- [OrbStack](https://orbstack.dev/) (macOS) - recommended on macOS
- [Docker Desktop](https://docs.docker.com/desktop/) (macOS, Windows, Linux) - recommended on Windows and Linux
- [Rancher Desktop](https://rancherdesktop.io/) (macOS, Windows, Linux)
- [Podman](https://podman.io/) (macOS, Windows, Linux)
The experimental `supabase stack` commands can also run a local project as processes on your machine without Docker on Linux and on macOS on Apple silicon. See [Docker and native runtimes](/docs/guides/local-development/docker-and-native-runtimes).
</Admonition>
## Quickstart
@@ -145,6 +147,8 @@ Local development with Supabase allows you to work on your projects in a self-co
Once set up, you can initialize a new Supabase project, start the local stack, and begin developing your application using local Supabase services. This includes access to a local Postgres database, Auth, Storage, and other Supabase features.
With the default ports, `supabase start` runs one local project per machine. To run a local project for each app or git worktree at the same time, use the experimental `supabase stack` commands described in [Running multiple local projects](/docs/guides/local-development/running-multiple-local-projects).
## CLI
The Supabase CLI is a tool that enables developers to run Supabase services locally and manage hosted projects directly from the terminal. It provides a suite of commands for various tasks, including:
@@ -16,6 +16,8 @@ There are two starting points, both leading to the same place: database schema a
You need the Supabase CLI installed and a Docker-compatible runtime running. If you haven't set these up yet, see [Install and run the CLI](/docs/guides/local-development/cli/getting-started) for installation across macOS, Windows, and Linux, and for the details of what `supabase start` brings up and how to access each service.
This guide assumes one running local project. If you work on several apps or git worktrees at once and need a local project for each, see [Running multiple local projects](/docs/guides/local-development/running-multiple-local-projects).
Keep in mind that **the local stack is for development only**. It is not hardened for production use and must never be exposed to external traffic. It has no TLS, no rate limiting, and default credentials. Use it to develop and test, then deploy to the [Supabase Platform](https://supabase.com) or a proper self-hosted setup for anything beyond that.
<Admonition type="note" title="`supabase` vs. `npx supabase`">
@@ -515,3 +517,7 @@ supabase start
```
If problems persist, `supabase stop --no-backup` for a clean restart (this removes local database data).
**`supabase start` fails because a port is already allocated**
Another local Supabase project is using the default ports. Stop it with `supabase stop` from its directory. To run both projects at the same time, turn on the experimental `supabase stack` commands and remove the fixed ports from each project's `config.toml`. See [Running multiple local projects](/docs/guides/local-development/running-multiple-local-projects).
@@ -405,6 +405,8 @@ When you are finished working on your Supabase project, you can stop the stack (
supabase stop
```
With the default ports, `supabase start` runs one local project per machine, because every project's `config.toml` uses the same ports. To run several local projects or git worktrees at the same time, see [Running multiple local projects](/docs/guides/local-development/running-multiple-local-projects).
## Telemetry
The Supabase CLI collects telemetry data about general usage. Participating in this program is optional, and you can opt out at any time.
@@ -0,0 +1,112 @@
---
id: 'docker-and-native-runtimes'
title: 'Docker and native runtimes'
description: 'How the experimental supabase stack commands run a local Supabase project in Docker or as native processes, how the CLI picks a runtime, and what the native runtime needs.'
subtitle: 'Run a local Supabase project in containers or as processes on your machine without Docker'
---
This guide explains the two runtimes the experimental `supabase stack` commands use, how the CLI picks one, and which to choose. The Docker runtime runs each service in a container, with Docker or Podman. The native runtime runs each service as a process on your machine, with no container engine, on Linux and on macOS on Apple silicon.
Both runtimes run the same local Supabase project with the same services. The default `supabase start` command always uses Docker. The runtime choice exists only after you turn on the `[experimental] stack` setting. See [Running multiple local projects](/docs/guides/local-development/running-multiple-local-projects#turn-on-the-stack-commands) for how to turn it on.
<Admonition type="caution" title="Experimental">
The `supabase stack` commands are experimental. Their flags and output can change between releases, and the CLI compatibility promise doesn't cover them.
</Admonition>
## Which runtime to choose
Use Docker when Docker or Podman is available. Containers give each local project its own network namespace and file system. That isolation matters most when you run several local projects on one machine.
Use the native runtime where no container engine is available, such as coding agent sandboxes and CI runners. The native runtime is intended for one local project per environment. Native processes share the host's process table and file locks. Several native local projects on one machine are less isolated from each other than containers are.
## How the two runtimes differ
The following table compares how each runtime runs services, what it needs, and where it stores data.
| | Docker runtime | Native runtime |
| -------------------------------- | --------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------- |
| Flag | `--runtime docker` or `--runtime podman` | `--runtime native` |
| How services run | One container per service, from images the CLI pulls | One process per service, from archives the CLI downloads and verifies |
| Requires | A running Docker or Podman engine | A supported platform, `tar` on the host, and, on first start, network access to the archive download hosts |
| Database data | A Docker volume when the Docker client and daemon are both version 26 or later, or a host directory otherwise | `~/.supabase/stacks/<id>/` in your home directory |
| Downloaded artifacts | The engine's image cache | `~/.supabase/cache/stack` in your home directory, shared by all local projects |
| Isolation between local projects | Separate network namespaces and file systems | Shared host process table and file locks |
Both runtimes keep each local project's saved settings under `~/.supabase/stacks/<id>/`. The native runtime keeps its database data there too. If you set `SUPABASE_HOME`, the CLI uses that directory instead of `~/.supabase`.
In both runtimes, Postgres starts right away and other services start on their first request. By default, services that started this way stop again after 60 seconds idle, and Studio after 5 minutes. Functions doesn't stop on its own. A service also stays up while a running service depends on it, such as pgmeta while Studio runs. Pass `--eager` to start every enabled service before the command returns and turn off idle stops.
Starting a service on demand doesn't mean downloading it on demand. By default, the first `supabase start` pulls or downloads every enabled service before it returns. Pass `--preparation on-demand` to defer each download until the service's first request.
## How the CLI picks a runtime
When you don't pass `--runtime`, a new local project uses the first of these that responds:
1. Docker, when the Docker daemon answers `docker version`
2. Podman, when the Podman engine answers `podman info`
3. Native, on Linux `amd64` and `arm64` and on macOS on Apple silicon
Each check waits up to 10 seconds. Having the `docker` command installed isn't enough. If the Docker daemon is stopped, the CLI moves on to Podman or native. It prints a notice that it skipped Docker, with the steps to switch. With no responding engine on a platform without native support, the start fails and asks you to start Docker or Podman.
Passing `--runtime docker`, `--runtime podman`, or `--runtime native` requires that runtime, and the CLI never falls back to another one. If Docker isn't reachable with `--runtime docker`, the start fails and suggests starting Docker, or `--runtime native` for a new local project on a supported platform. With `--runtime native` on an unsupported platform, the start fails before the CLI creates the local project.
The CLI records the runtime when it creates a local project and reuses it on every later start, without checking again. Passing a different `--runtime` to a local project that you already created fails. To move an app to another runtime, start a new named local project, or destroy the local project and start it again. The CLI doesn't convert data between runtimes.
Different apps on one machine can use different runtimes.
## Native runtime requirements
The native runtime runs on these platforms:
| Platform | Support |
| --------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------- |
| Linux on `amd64` or `arm64` | Supported on Ubuntu 22.04 or later, or another distribution with glibc 2.35 or later. Some services use the host's glibc. |
| macOS on Apple silicon | Supported on macOS 14 or later. |
| Windows | Not supported. Use Docker. |
| macOS on Intel | Not supported. Use Docker. |
The native runtime doesn't run as `root`, because Postgres `initdb` can't. To run it as `root`, such as in a CI container, set `SUPABASE_NATIVE_POSTGRES_USER` to a non-root account. Create the account first if it doesn't exist. The CLI then runs Postgres as that user. In some supported coding agent sandboxes, the CLI finds a suitable non-root account without the variable.
On first start, the CLI downloads service archives from the [`supabase/slim-services` GitHub releases](https://github.com/supabase/slim-services/releases), with `supabase-cli-artifacts.s3.us-east-1.amazonaws.com` as a fallback. It verifies their checksums and extracts them with the system `tar`, so install `tar` in minimal sandbox images. In a sandbox with an allowlist, allow `github.com` or that S3 host. Allowing only `ghcr.io` isn't enough.
To download the archives ahead of time without starting any services, such as when you build a sandbox image, run:
```bash
supabase stack prepare --runtime native
```
If the local project doesn't exist, `prepare` creates it, so it appears in `supabase stack list` and you can remove it with `supabase stack destroy`.
The cache in `~/.supabase/cache/stack` is shared by every local project on the machine, and `supabase stack destroy` leaves it in place. Delete that directory to reclaim its disk space and force a fresh download.
## Start with a specific runtime
Start a local project in the native runtime:
```bash
supabase start --runtime native
```
Require Docker for a new local project:
```bash
supabase start --runtime docker
```
Check which runtime a local project uses:
```bash
supabase stack status --output-format json
```
The `runtime` field is `docker`, `podman`, or `native`. The text output of `supabase status` and `supabase stack list` shows the runtime too.
## Limitations
- A local project keeps the runtime it was created with, and the CLI doesn't move data between runtimes. See [How the CLI picks a runtime](#how-the-cli-picks-a-runtime).
- Running several native local projects on one machine works, but they share host resources without the isolation containers provide. Use Docker for that case.
- If Postgres takes too long to become ready on a cold start, raise `health_timeout` under `[db]` in `config.toml`. Both runtimes use that setting.
<ContentListings id="local-development-runtimes-learn-more" />
@@ -0,0 +1,283 @@
---
id: 'running-multiple-local-projects'
title: 'Running multiple local projects'
description: 'Run more than one local Supabase project on the same machine, one per app, git worktree, or named environment, with the experimental supabase stack commands.'
subtitle: 'Run a local Supabase project for every app, git worktree, or environment on one machine'
---
This guide explains how to run more than one local Supabase project on one machine with the experimental `supabase stack` commands. Use it when you switch between apps, work in several git worktrees, run coding agents in parallel, or want separate `dev` and `test` environments for one app.
In this guide, a local project is the set of Supabase services running on your machine for one app, the same thing `supabase start` gives you. Your app is your own code, in the directory where you ran `supabase init`.
With the default ports, `supabase start` runs one local project per machine. Every app's generated `config.toml` uses the same ports, so a second local project fails with a port conflict. The `supabase stack` commands assign each local project its own ports and keep its data separate.
This guide covers:
- [How local projects stay isolated](#how-local-projects-stay-isolated) explains which directories, branches, and names get their own local project. Read it first if you use a monorepo or run several agents.
- [Turn on the stack commands](#turn-on-the-stack-commands) and [Remove fixed ports from config.toml](#remove-fixed-ports-from-configtoml) are one-time setup for each app.
- [Start a local project in each app directory or worktree](#start-a-local-project-in-each-app-directory-or-worktree) through [Stop and destroy local projects](#stop-and-destroy-local-projects) cover day-to-day use.
- [Differences from the default `supabase start`](#differences-from-the-default-supabase-start) and [Limitations](#limitations) list what changes when you switch.
<Admonition type="caution" title="Experimental">
The `supabase stack` commands are experimental. Their flags and output can change between releases, and the CLI compatibility promise doesn't cover them. Local projects started with these commands keep their own data, separate from any local project started with the default `supabase start`.
</Admonition>
## How local projects stay isolated
Every local project has an identity made from three parts:
- **Project root**: the nearest directory, starting from your current directory and walking up, that contains `supabase/config.toml`. Without a config file, the project root is the current directory.
- **Git branch**: if the project root is in a git repository, the current branch is part of the identity. The CLI reads it from the `.git` metadata, so git doesn't need to be installed.
- **Name**: an optional name that you pass with `--stack`.
Two local projects with different identities never share containers, processes, ports, database data, or Storage data. You get a separate local project for each:
- App with its own `supabase/` directory
- Git worktree, because each worktree is a different directory
- Git branch, when you check out a different branch in the same directory
- Name you pass with `--stack`, so one app can run `dev` and `test` side by side
Everything else shares one local project. Packages in a monorepo get separate local projects only if each one has its own `supabase/` directory. Two packages under one `supabase/` directory share a local project. So do two coding agents working in the same checkout on the same branch. To isolate them, use separate git worktrees or different `--stack` names.
Local projects for the same project root also share the project files in its `supabase/` directory. For example, Studio saves SQL snippets to `supabase/snippets`, so the `dev` and `test` local projects of one app see the same snippets.
The CLI assigns ports for each local project from the range 20000 to 32767 and keeps them stable across restarts. Ports written in `config.toml` are used as written, so see [Remove fixed ports from config.toml](#remove-fixed-ports-from-configtoml) first.
## Before you begin
You need:
- Supabase CLI v2.119.0 or later. See [Install and run the CLI](/docs/guides/local-development/cli/getting-started).
- A container engine or a supported platform. The Docker runtime needs a running Docker or Podman engine, and is the recommended runtime for several local projects at the same time. The native runtime needs no container engine but runs only on macOS on Apple silicon and on Linux for `amd64` and `arm64`. See [Choose a runtime](#choose-a-runtime).
- An app directory initialized with `supabase init`, or a directory without a `supabase/config.toml`. Without a config file, the local project starts with default settings and doesn't create a config file.
## Turn on the stack commands
The `supabase stack` commands exist only when the `[experimental] stack` setting is on. Without it, `supabase stack` commands fail with `Unknown subcommand "stack"`.
1. Add this to `supabase/config.toml` in your app directory:
```toml
[experimental]
stack = true
```
In a directory without a `config.toml`, set the environment variable instead:
```bash
export SUPABASE_EXPERIMENTAL_STACK=1
```
2. Confirm that the commands are available:
```bash
supabase stack --help
```
The help starts with `Manage an experimental, unstable local Supabase stack`. If you see the general `Supabase CLI` help instead, the setting isn't on.
With the setting on, `supabase start`, `supabase status`, and `supabase stop` run the stack commands. The rest of this guide uses those top-level commands. `supabase stack start`, `supabase stack status`, and `supabase stack stop` do the same thing. Commands that only exist under `supabase stack`, such as `list` and `destroy`, keep the prefix.
The setting also routes the local targets of the `db`, `migration`, `test`, `gen`, `inspect`, `pull`, `storage`, `seed`, and `services` commands, and `functions serve`, to the stack.
The environment variable takes precedence over the config file. Set `SUPABASE_EXPERIMENTAL_STACK=0` to use the default commands for one session without editing the file.
## Remove fixed ports from config.toml
`supabase init` writes fixed ports into `config.toml`. The CLI treats a port in the file as an exact request. Two local projects that both request port `54321` can't run at the same time.
For a new app, run `init` with the setting turned on. The CLI writes `[experimental] stack = true` and leaves the port keys out:
```bash
SUPABASE_EXPERIMENTAL_STACK=1 supabase init
```
For an app that already has a `config.toml`, remove the port lines from each app that you want to run in parallel:
1. Open `supabase/config.toml` in your app directory.
2. Delete or comment out these keys:
- `port` under `[api]`, `[db]`, `[db.pooler]`, `[studio]`, `[local_smtp]`, and `[analytics]`
- `shadow_port` under `[db]`
- `inspector_port` under `[edge_runtime]`
- `smtp_port` and `pop3_port` under `[local_smtp]`, and `vector_port` under `[analytics]`, if you set them. `supabase init` writes the SMTP ports commented out.
3. Save the file and commit it, so everyone on the team gets the same behavior.
Remove the ports before the app's first `supabase start` with the setting on. A local project keeps the ports it was created with. If you already started one with the fixed ports, removing them afterward makes `supabase start` fail with `cannot change on the saved stack`. Start a new local project with a different `--stack` name, or run `supabase stack destroy` and start again. Destroying deletes the local project's data.
The CLI also treats ports you set with an environment variable, such as `SUPABASE_API_PORT` or `SUPABASE_DB_PORT`, as exact requests. If another process already listens on that port, the start fails. When the port belongs to another local project, the error names that local project.
## Start a local project in each app directory or worktree
The following steps start two independent local projects from two app directories. The same steps work for git worktrees of one repository.
1. Open a terminal in the first app directory and start its local project:
```bash
supabase start
```
When the local project is ready, the command prints `Stack is ready.` and a connection summary. The summary lists the API, REST, Functions, Studio, MCP, Mailpit, and database URLs, the publishable and secret keys, the state of each service, and the runtime.
2. Open a second terminal in the other app directory or worktree and run the same command:
```bash
supabase start
```
The second local project gets its own ports and its own database. Both keep running after the commands return.
3. Point each app at the Project URL of its own local project. The ports stay the same the next time you start it. To write the URLs and keys to a dotenv file, see [Find a local project's endpoints and keys](#find-a-local-projects-endpoints-and-keys).
The first start in each runtime takes longer. By default, the command downloads or pulls every enabled service before it returns. Later starts reuse the cache. To defer each download until the service's first request, pass `--preparation on-demand`.
Postgres starts right away. Other services start on their first request. By default, they stop again after 60 seconds idle, and Studio after 5 minutes. Functions doesn't stop on its own. A service also stays up while a running service depends on it, such as pgmeta while Studio runs. To start every enabled service before the command returns and turn off idle stops, pass `--eager`.
To leave services out, pass `--exclude` with one or more of `rest`, `auth`, `realtime`, `storage`, `functions`, `studio`, `mail`, `analytics`, and `pooler`:
```bash
supabase start --exclude studio,mail
```
The database can't be excluded. Studio needs the REST API, so to exclude `rest`, exclude `studio` too. Excluding `studio` also removes the MCP server, because Studio serves `/mcp`.
Running `supabase start` again on a running local project keeps its current settings and prints `Stack is already running with its current services`. To apply a change to `config.toml`, `--exclude`, or `--eager`, stop the local project and start it again. Starting without `--exclude` restores the services enabled in `config.toml`.
Stopping and starting doesn't apply changes to ports or to the Postgres major version. For those, start a new local project with a different `--stack` name, or run `supabase stack destroy` and start again.
## Run named local projects for one app
One app can run several local projects by name. This keeps a destructive test run away from your development data.
1. Start a `dev` local project:
```bash
supabase start --stack dev
```
2. Start a `test` local project in the same directory:
```bash
supabase start --stack test
```
3. Pass the same `--stack` name to `supabase status`, `supabase stop`, and `supabase stack destroy` to target that local project later.
A local project started without `--stack` is the app directory's default local project. Named local projects and the default one are independent of each other.
The `db`, `migration`, and other database commands use the default local project for the current branch. They can't target a named local project. See [Limitations](#limitations).
## Find a local project's endpoints and keys
Ports differ between local projects, so don't assume the defaults from the CLI documentation. To see the URLs, keys, and service states of a local project, run this in its app directory:
```bash
supabase status
```
Pass `--stack <name>` for a named local project. The local database password is `postgres`, so the database URL in the output works with `psql` and `--db-url`.
To export the connection details as environment variables, pass `--env`:
```bash
supabase status --env --output-format text > .env.local
```
The file includes `API_URL`, `DB_URL`, `PUBLISHABLE_KEY`, `SECRET_KEY`, `ANON_KEY`, and `SERVICE_ROLE_KEY`, plus the URLs of the other available services. To match the variable names your framework expects, pass `--override-name`:
```bash
supabase status --env --override-name API_URL=NEXT_PUBLIC_SUPABASE_URL,ANON_KEY=NEXT_PUBLIC_SUPABASE_ANON_KEY
```
For scripts and agents, request JSON from `supabase start` or `supabase status`:
```bash
supabase start --output-format json
```
The `start` JSON includes the local project's `id`, its `runtime`, `lazy_services` that start on their first request, and the same `env` map that `--env` exports. Its `endpoints` object is keyed by service and endpoint, such as `database.sql`, and each entry has a `protocol`, `address`, `port`, and `url`.
To list every local project on the machine, across all apps, run:
```bash
supabase stack list
```
The list shows each local project's name, project root, branch, runtime, and a short ID. Pass `--output-format json` to get the full ID that `--stack-id` accepts.
To stream live logs from a local project, run `supabase stack logs`. Pass `--service database` to limit the output to one service.
## Choose a runtime
A local project runs in the Docker runtime or the native runtime. The Docker runtime runs each service in a container, with Docker or Podman. The native runtime runs each service as a process on your machine, with no container engine. It runs on macOS on Apple silicon and on Linux `amd64` and `arm64`. Both runtimes run the same services.
For several local projects on one machine, use Docker. Containers give each local project its own network namespace and file system. Native local projects share the host's process table and file locks. Use the native runtime where no container engine is available, such as coding agent sandboxes and CI runners.
When you don't pass `--runtime`, a new local project uses Docker if the Docker daemon responds, then Podman, then native on supported platforms. Having the `docker` command installed isn't enough. The runtime is fixed for the life of a local project. For details, platform requirements, and what the native runtime downloads, see [Docker and native runtimes](/docs/guides/local-development/docker-and-native-runtimes).
## Stop and destroy local projects
Stopping a local project keeps its data and ports. Destroying it deletes its data.
To stop the app directory's default local project, run this in that directory:
```bash
supabase stop
```
To stop a named local project, pass its name:
```bash
supabase stop --stack test
```
To stop every local project on the machine, across all apps, pass `--all`. If any local project fails to stop, the command exits with an error that lists the failed local projects:
```bash
supabase stack stop --all
```
To permanently delete one local project and its data, run `supabase stack destroy`. The command asks for confirmation. Pass `--yes` to skip the prompt in scripts:
```bash
supabase stack destroy --stack test --yes
```
<Admonition type="danger">
`supabase stack destroy` deletes the local project's database data and frees its ports. There is no backup and no undo. There is also no bulk destroy, so one command removes one local project.
</Admonition>
`destroy` keeps two kinds of data:
- Storage upload files, which live in your app at `supabase/.temp/stack-uploads/<id>/`. Delete that directory to remove them.
- Container engine resources, if Docker or Podman isn't running. If no process for the local project is still running, the command removes the local project's registration. It prints cleanup commands to run after the engine starts again. Otherwise it fails and asks you to start the engine and retry.
The printed cleanup command deletes only this local project's database data, from a Docker volume that other local projects share. Don't delete the volume itself.
## Differences from the default `supabase start`
Keep these differences in mind when you turn the setting on for an app that used the default `supabase start`:
- Local projects started this way keep separate data. Turning on the setting doesn't copy the database from a local project you started with the default `supabase start`. It also doesn't stop that local project.
- `supabase stop --no-backup` and `supabase stop --project-id` aren't available. Use `supabase stack destroy` to remove data and `supabase stack stop --all` to stop every local project.
- The `-o` and `--output` flags aren't available. Use `--output-format` instead, and `--env` in place of `-o env`.
- `functions serve` needs a running local project. Start one with `supabase start` first.
## Limitations
- Some `config.toml` settings aren't supported. `supabase start` fails with a message that names the setting. The unsupported settings are:
- `api.tls`
- The Analytics GCP settings, and Analytics backends other than Postgres
- Custom Auth email templates with `content_path`
- Storage Analytics and Storage vector buckets
- A custom `edge_runtime.deno_version`
- OrioleDB, and `db.major_version` values other than 15 and 17
- Database commands can't target a named local project. `supabase db reset`, `supabase db diff`, `supabase db pull`, `supabase db dump`, and the other local database commands use the app directory's default local project for the current branch, and don't accept `--stack`.
- Switching branches switches local projects. The git branch is part of the identity. After you check out another branch in the same directory, `supabase start` creates or resumes a different local project with its own data. The first branch's local project keeps running until you stop it.
- Moving or renaming an app directory creates a new local project, because the identity includes the project root path. The previous local project's data stays until you destroy it.
- `supabase stack logs` streams new log lines only. It has no history.
<ContentListings id="local-development-parallel-projects-learn-more" />
+3 -3
View File
@@ -1,6 +1,6 @@
---
title: Observability
description: 'Read project data, diagnose issues, and hire an agent to monitor your project'
description: 'Read project data, diagnose issues, and run agent prompts to check your project'
---
Use project data to understand what is happening, investigate issues, and give an agent repeatable checks to run.
@@ -15,9 +15,9 @@ Query logs for events, inspect database statistics, or review advisor findings.
Run [detection checks](/docs/guides/observability/detecting) to identify health, security, performance, or capacity issues. Take the resulting error code, time window, or affected object to the [troubleshooting guides](/docs/guides/troubleshooting), then rerun the check after a fix.
## Hire an agent
## Agent prompts
Give an agent recurring checks to run and findings to report. [Set up an agent](/docs/guides/observability/automate-with-agents) with read-only access to your project.
Give an agent a prompt to check your project and report issues to fix. [See the prompts](/docs/guides/observability/automate-with-agents) and run them with read-only access to your project.
<ContentListings id="telemetry-hire-agent" />
@@ -1,33 +1,33 @@
---
id: 'automate-with-agents'
title: 'Hire an agent'
subtitle: 'Run a read-only monitoring routine in your own agent harness.'
description: 'Choose and set up a Health, Security, Performance, or Resource monitor in Claude, Codex, or Cursor.'
title: 'Agent prompts'
subtitle: 'Give an agent a prompt to check your project and report issues to fix.'
description: 'Run a Health, Security, Performance, or Resource prompt in Claude, Codex, or Cursor.'
---
This guide explains how to run a Supabase monitoring agent in your own harness. Each agent is a prompt plus a schedule. It reads project data and reports findings. It does not change the project.
Each prompt tells an agent to check one area of your project — health, security, performance, or resources — and report issues worth fixing. It reads project data read-only and changes nothing. Run it on demand, or put it on a schedule.
## Choose a routine
## Choose a prompt
Start with one monitor. Add another only when the project needs a different source or cadence.
Start with one prompt. Add another only when you need a different source or cadence.
| Monitor | What it watches | Default cadence | Use it when |
| ---------------------------------------------------------------------------------- | ----------------------------------------------------------------------------- | --------------- | -------------------------------------------------------------- |
| [Health monitor](/docs/guides/observability/automate-with-agents/health) | API and Auth server errors, error-rate spikes, connection pressure | Hourly | You need incident detection and regular feedback loops |
| [Security monitor](/docs/guides/observability/automate-with-agents/security) | Security Advisor findings, authentication and authorization failures | Daily | You need a regular access-control and configuration review |
| [Performance monitor](/docs/guides/observability/automate-with-agents/performance) | Slow queries, lock waits, long-running sessions, Performance Advisor findings | Hourly | You need query and database performance checks |
| [Resource monitor](/docs/guides/observability/automate-with-agents/usage) | Request, error, storage, table, and connection growth | Daily | You need to identify growth before it reaches a resource limit |
| Prompt | What it checks | Suggested cadence | Use it when |
| -------------------------------------------------------------------------- | ----------------------------------------------------------------------------- | ----------------- | -------------------------------------------------------------- |
| [Health](/docs/guides/observability/automate-with-agents/health) | API and Auth server errors, error-rate spikes, connection pressure | Hourly | You need incident detection and regular feedback loops |
| [Security](/docs/guides/observability/automate-with-agents/security) | Security Advisor findings, authentication and authorization failures | Daily | You need a regular access-control and configuration review |
| [Performance](/docs/guides/observability/automate-with-agents/performance) | Slow queries, lock waits, long-running sessions, Performance Advisor findings | Hourly | You need query and database performance checks |
| [Resources](/docs/guides/observability/automate-with-agents/usage) | Request, error, storage, table, and connection growth | Daily | You need to identify growth before it reaches a resource limit |
For a small project, run the most relevant routine daily or weekly and include the other categories in its prompt. Split it into specialized monitors only when you need different owners, schedules, or alert thresholds.
For a small project, run the most relevant prompt daily or weekly and fold the other categories into it. Split into separate prompts only when you need different owners, schedules, or alert thresholds.
## Run the routine
## Run a prompt
1. Connect the [Supabase MCP server](/docs/guides/ai-tools/mcp) with `project_ref` and `read_only=true`.
2. Open the monitor that matches the job from the table above.
2. Open the prompt that matches the job from the table above.
3. Set it up in Claude, Codex, Cursor, or copy the prompt into another harness.
4. Run it on demand first. Then put the same prompt on a schedule.
Each agent page describes what it will output. Send those findings through the connections your harness already has, such as Linear in Codex.
Each prompt page describes what it will output. Send those findings through the connections your harness already has, such as Linear in Codex.
Scheduled tasks start a fresh context on every run, so the prompt is self-contained. Review the first runs before you rely on the schedule.
@@ -1,8 +1,8 @@
---
id: 'automate-with-agents-health'
title: 'Health monitor'
title: 'Health'
subtitle: 'A read-only agent that checks API and Auth errors and Postgres connection pressure once per hour.'
description: 'Hourly monitoring for server errors and connection pressure'
description: 'Hourly checks for server errors and connection pressure'
---
```mermaid
@@ -28,7 +28,7 @@ It uses `query_logs` and read-only `execute_sql` on project-scoped [Supabase MCP
## What it will output
Health monitor reports new or changed problems with the affected service, measured error rate or connection usage, and a next investigation step. See [what triggers a health report](/docs/guides/observability/detecting#health).
This prompt reports new or changed problems with the affected service, measured error rate or connection usage, and a next investigation step. See [what triggers a health report](/docs/guides/observability/detecting#health).
If a check cannot run, the agent tells you what is missing. Clear checks and unchanged findings stay quiet.
@@ -1,8 +1,8 @@
---
id: 'automate-with-agents-performance'
title: 'Performance monitor'
title: 'Performance'
subtitle: 'A read-only agent that inspects query performance, blocking sessions, and Performance Advisor findings once per hour.'
description: 'Hourly monitoring for query regressions, blocking sessions, and performance findings'
description: 'Hourly checks for query regressions, blocking sessions, and performance findings'
---
```mermaid
@@ -29,7 +29,7 @@ It uses `get_advisors` and read-only `execute_sql` on project-scoped [Supabase M
## What it will output
Performance monitor reports new or changed findings with the affected query, session, or object, plus an investigation and verification step. It does not infer a regression without comparable measurements or recommend cancellation based only on query age. See [what triggers a performance report](/docs/guides/observability/detecting#performance).
This prompt reports new or changed findings with the affected query, session, or object, plus an investigation and verification step. It does not infer a regression without comparable measurements or recommend cancellation based only on query age. See [what triggers a performance report](/docs/guides/observability/detecting#performance).
If a check cannot run, the agent tells you what is missing. Clear checks and unchanged findings stay quiet.
@@ -1,6 +1,6 @@
---
id: 'automate-with-agents-security'
title: 'Security monitor'
title: 'Security'
subtitle: 'A read-only agent that reviews Security Advisor findings and authentication and authorization failures each day.'
description: 'Daily review of security findings and access failures'
---
@@ -29,7 +29,7 @@ It uses `get_advisors` and `query_logs` on project-scoped, read-only [Supabase M
## What it will output
Security monitor reports new or changed advisor findings and access-failure spikes, with the affected object or service and a next investigation step. A spike is a review signal, not proof of an attack. See [what triggers a security report](/docs/guides/observability/detecting#security).
This prompt reports new or changed advisor findings and access-failure spikes, with the affected object or service and a next investigation step. A spike is a review signal, not proof of an attack. See [what triggers a security report](/docs/guides/observability/detecting#security).
If a check cannot run, the agent tells you what is missing. Clear checks and unchanged findings stay quiet.
@@ -1,8 +1,8 @@
---
id: 'automate-with-agents-usage'
title: 'Resource monitor'
title: 'Resources'
subtitle: 'A read-only agent that tracks resource and request growth and estimates when a confirmed limit could be reached.'
description: 'Daily monitoring for resource growth and approaching limits'
description: 'Daily checks for resource growth and approaching limits'
---
```mermaid
@@ -30,7 +30,7 @@ It uses read-only `execute_sql` and `query_logs` on project-scoped [Supabase MCP
## What it will output
Resource monitor reports new or changed request-growth signals and resource-limit risks. When saved measurements support a forecast within 14 days, it includes the estimated date, calculation, and scaling guide. If history or a matching limit is missing, it explains what it needs instead of inventing a date. See [what triggers a resource report](/docs/guides/observability/detecting#usage).
This prompt reports new or changed request-growth signals and resource-limit risks. When saved measurements support a forecast within 14 days, it includes the estimated date, calculation, and scaling guide. If history or a matching limit is missing, it explains what it needs instead of inventing a date. See [what triggers a resource report](/docs/guides/observability/detecting#usage).
If a check cannot run, the agent tells you what is missing. Clear checks and unchanged findings stay quiet.
@@ -4,7 +4,7 @@ title: 'Detection checks'
description: 'Repeatable health, security, performance, and capacity checks with explicit inputs and outcomes'
---
Use these checks to identify evidence worth investigating. A finding does not establish a cause. The specialist [monitoring agents](/docs/guides/observability/automate-with-agents) use these same checks.
Use these checks to identify evidence worth investigating. A finding does not establish a cause. The [agent prompts](/docs/guides/observability/automate-with-agents) use these same checks.
## Before running checks
+6 -4
View File
@@ -13,15 +13,15 @@ Self-hosting is a good fit if you need full control over your data, have complia
## How self-hosted Supabase differs
Self-hosted Supabase runs as a single project which means that Studio doesn't support multiple organizations or projects. Most settings are configured through [environment variables](https://github.com/supabase/supabase/blob/master/docker/.env.example).
Self-hosted Supabase runs as a single project, which means that Studio doesn't support multiple organizations or projects. Most settings are configured through [environment variables](https://github.com/supabase/supabase/blob/master/docker/.env.example).
Unlike the managed platform, which is fully hosted and operated by Supabase, branching, advanced metrics beyond logs, managed backups and PITR, analytics and vector buckets, ETL, and the platform management API are **unavailable**.
Some features of the managed platform aren't available when self-hosting: branching, advanced metrics beyond logs, managed backups and PITR, analytics and vector buckets, ETL, and the Management API. To collect logs and metrics yourself, see the community-maintained [observability project](https://github.com/supabase-community/supabase-observability).
### Not the same as local development
[Supabase CLI](/docs/guides/local-development/cli/getting-started) runs a local stack for development and testing. That stack is not a self-hosted deployment: it is not hardened for production and must not be exposed to external traffic.
To self-host, use [Docker](/docs/guides/self-hosting/docker) or one of the community deployment options.
To self-host, use [Docker](/docs/guides/self-hosting/docker) or the community-maintained [Kubernetes project](https://github.com/supabase-community/supabase-kubernetes).
## Your responsibilities when self-hosting
@@ -35,9 +35,11 @@ When you self-host, **you are responsible for**:
- Backups and disaster recovery
- Monitoring and uptime
<ContentListings id="self-hosting-third-party-guides" />
## Telemetry
Self-hosted Supabase (run via Docker Compose) **does not phone home or collect any telemetry**.
Self-hosted Supabase **does not phone home or collect any telemetry**.
The **Supabase CLI**, a separate tool also used for [local development](/docs/guides/local-development/cli/getting-started), collects usage telemetry to help improve the developer experience. See [CLI telemetry](/docs/guides/local-development/cli/getting-started#telemetry) for opt-out methods.
@@ -0,0 +1,38 @@
---
title = "Cost confirmations do not appear in your MCP client"
topics = [ "ai" ]
keywords = [
"mcp",
"elicitation",
"cost",
"confirmation",
"get_cost",
"confirm_cost",
"create_project",
"create_branch",
]
---
When your connection uses cost confirmation dialogs, the Supabase MCP server asks for [confirmation through your MCP client](/docs/guides/ai-tools/mcp#cost-confirmation) before `create_project` or `create_branch` creates a resource that incurs charges. If you expect a confirmation dialog and don't see one, work through these checks.
## Your client uses the `get_cost` and `confirm_cost` flow
Cost confirmation dialogs appear when the server offers form-based cost confirmation for the tool and your client supports form elicitations for the request. The specification revision alone does not determine which flow your connection uses. If form-based cost confirmation is unavailable or unsupported, the tool uses the legacy cost confirmation workflow instead. In that workflow, the agent quotes the cost in chat and asks you before creating the resource. No dialog appears, and this is the expected behavior.
To check which flow a creation tool uses, ask your client to list the available Supabase tools and the resource types accepted by `get_cost` and `confirm_cost`. The resource types accepted by these helpers use the legacy cost confirmation workflow; another resource type can use form-based cost confirmation on the same connection. Their absence does not prove that form-based cost confirmation is active: project scoping, account feature settings, or server availability can also hide them.
## Cost confirmations are skipped for the tool
Check your MCP server URL for `skip_elicitations`. If it includes `create_project` or `create_branch`, that tool uses the legacy cost confirmation workflow instead of a confirmation dialog. Skipping the form does not approve the cost or avoid charges. See [Skip form confirmations](/docs/guides/ai-tools/mcp#skip-form-confirmations).
## The project creation is quoted at zero cost
When a new project creation request uses form-based cost confirmation and is quoted at zero cost, no confirmation is requested and the project is created directly. This exception does not apply to branch creation: when it uses form-based cost confirmation, it asks you to confirm the standard rate before any allowances or exemptions are applied.
## Your client answers elicitations automatically
Some clients support hooks or rules that respond to elicitations without showing a dialog. If resources are created without a visible confirmation on a client that supports the dialog, check your client's elicitation or hook configuration.
## Still stuck?
If none of these explain what you're seeing, open an issue on the [Supabase MCP repository](https://github.com/supabase/mcp) with your client name and version.
@@ -0,0 +1,39 @@
---
title = "Edge Function secret collection is unavailable or incomplete"
topics = [ "ai", "functions" ]
keywords = [
"mcp",
"elicitation",
"secrets",
"create_edge_function_secret",
"Dashboard",
]
---
The `create_edge_function_secret` tool uses a [Dashboard flow to collect an Edge Function secret](/docs/guides/ai-tools/mcp#edge-function-secrets). If the tool is unavailable or the flow is incomplete, work through these checks.
## Your connection does not support URL elicitation
The tool requires the server to offer secret collection and your client to support URL elicitations. Support for form elicitations does not imply support for URL elicitations. Check [Client support](/docs/guides/ai-tools/mcp#client-support) for your connection.
## The tool is unavailable for your project
Check that Edge Functions tools are enabled, your connection is not read-only, and you have read and write permissions for project secrets. These are required for `create_edge_function_secret` to be available.
`skip_elicitations` cannot include `create_edge_function_secret` and does not enable this flow.
## The secret has not been saved in the Dashboard
The tool takes the secret name and project information, not the secret value. Never put the secret value in chat, send it to the model, or include it in tool arguments.
To complete the flow:
1. Open the Dashboard URL provided by the tool.
2. Enter and save the secret value in the Supabase Dashboard.
3. Return to your MCP client and confirm completion.
Confirming in your client does not replace saving the value in the Dashboard.
## You cancel after saving the secret
If you cancel the elicitation, this does not undo a secret already saved in the Dashboard. Check the secret in the Dashboard rather than assuming cancellation removed it.
@@ -44,6 +44,8 @@ Another process may be using the required ports. Check for:
- Docker containers
- Other development servers
If the conflict comes from another Supabase project, run both projects at the same time with the experimental `supabase stack` commands. They assign each local project its own ports. Turn on the commands and remove the fixed ports from each project's `config.toml` first. See [Running multiple local projects](/docs/guides/local-development/running-multiple-local-projects).
### Deno cache issues
Clear the Deno cache if you're experiencing module resolution problems:
@@ -0,0 +1,41 @@
---
title = "SQL confirmations do not appear in your MCP client"
topics = [ "ai", "database" ]
keywords = [
"mcp",
"elicitation",
"confirmation",
"destructive SQL",
"execute_sql",
"apply_migration",
"skip_elicitations",
]
---
The Supabase MCP server can ask for [confirmation before running detected destructive SQL](/docs/guides/ai-tools/mcp#destructive-sql-confirmations) through `execute_sql` or `apply_migration`. If you expect a confirmation dialog and don't see one, work through these checks.
## Form-based SQL confirmation is unavailable or unsupported
SQL confirmation dialogs require your client to support form elicitations for the request. Check your connection against [Client support](/docs/guides/ai-tools/mcp#client-support).
If form-based SQL confirmation is unavailable or unsupported, the tools follow their existing SQL execution behavior without an additional MCP confirmation prompt. Permissions and read-only restrictions still apply. There is no legacy cost-style confirmation workflow for SQL.
## The SQL does not trigger detection
The tools only request confirmation for SQL detected as destructive. Detection does not catch every destructive statement. A missing prompt does not prove that SQL is safe.
Review the SQL itself rather than relying on a confirmation dialog. Do not run destructive SQL to test whether a prompt appears.
## Your connection is read-only
`execute_sql` only requests destructive SQL confirmation outside read-only mode. Keep read-only mode enabled when you do not need write access; do not disable it to get a confirmation prompt.
## SQL confirmations are skipped for the tool
Check your MCP server URL for `skip_elicitations`. If it includes `execute_sql` or `apply_migration`, that tool follows its existing SQL execution behavior without the additional MCP confirmation prompt. Permissions and read-only restrictions still apply.
To receive supported confirmations, remove the affected tool from `skip_elicitations`. See [Skip form confirmations](/docs/guides/ai-tools/mcp#skip-form-confirmations).
## Your client answers elicitations automatically
Some clients support hooks or rules that respond to elicitations without showing a dialog. If your client supports form elicitations but no dialog appears, check its elicitation or hook configuration.
+8
View File
@@ -18,6 +18,10 @@ import {
gettingStartedUseCases,
gettingStartedWebAppDemos,
} from './getting-started.data'
import {
localDevelopmentParallelProjectsLearnMore,
localDevelopmentRuntimesLearnMore,
} from './local-development.data'
import { logDrainsDestinations } from './log-drains.data'
import { realtimeExamples, realtimeGetStarted, realtimeResources } from './realtime.data'
import { resourcesMigrate, resourcesOverview, resourcesPostgres } from './resources.data'
@@ -25,6 +29,7 @@ import {
selfHostingCommunity,
selfHostingGetStarted,
selfHostingSupport,
selfHostingThirdPartyGuides,
} from './self-hosting.data'
import { storageExamples, storageGetStarted, storageResources } from './storage.data'
import {
@@ -54,6 +59,8 @@ const ALL_GROUPS: readonly ContentListingGroup[] = [
gettingStartedFrameworkQuickstarts,
gettingStartedWebAppDemos,
gettingStartedMobileTutorials,
localDevelopmentParallelProjectsLearnMore,
localDevelopmentRuntimesLearnMore,
logDrainsDestinations,
realtimeGetStarted,
realtimeExamples,
@@ -63,6 +70,7 @@ const ALL_GROUPS: readonly ContentListingGroup[] = [
resourcesPostgres,
selfHostingGetStarted,
selfHostingCommunity,
selfHostingThirdPartyGuides,
selfHostingSupport,
storageGetStarted,
storageExamples,
@@ -0,0 +1,62 @@
import type { ContentListingGroup } from '~/lib/content-listings.schema'
export const localDevelopmentParallelProjectsLearnMore: ContentListingGroup = {
id: 'local-development-parallel-projects-learn-more',
heading: 'Learn more',
type: 'grid',
columns: 2,
items: [
{
title: 'Install and run the CLI',
href: '/guides/local-development/cli/getting-started',
description: 'Install the CLI and start your first local project.',
},
{
title: 'Local development workflow',
href: '/guides/local-development/cli-workflows',
description: 'Day-to-day commands for one local project, from migrations to troubleshooting.',
},
{
title: 'Docker and native runtimes',
href: '/guides/local-development/docker-and-native-runtimes',
description:
'How the CLI picks a runtime, what the native runtime needs, and where it stores data.',
},
{
title: 'Managing config and secrets',
href: '/guides/local-development/managing-config',
description:
'Keep configuration and secrets consistent across local, staging, and production.',
},
{
title: 'CLI configuration',
href: '/guides/local-development/cli/config',
description: 'Every key in config.toml, including the experimental stack setting.',
},
],
}
export const localDevelopmentRuntimesLearnMore: ContentListingGroup = {
id: 'local-development-runtimes-learn-more',
heading: 'Learn more',
type: 'grid',
columns: 3,
items: [
{
title: 'Running multiple local projects',
href: '/guides/local-development/running-multiple-local-projects',
description:
'Run a local project for every app, git worktree, or environment on one machine.',
},
{
title: 'Install and run the CLI',
href: '/guides/local-development/cli/getting-started',
description: 'Install the CLI and start your first local project.',
},
{
title: 'CLI configuration',
href: '/guides/local-development/cli/config',
description: 'Every key in config.toml, including the experimental stack setting.',
},
],
}
@@ -20,26 +20,46 @@ export const selfHostingGetStarted: ContentListingGroup = {
export const selfHostingCommunity: ContentListingGroup = {
id: 'self-hosting-community',
heading: 'Community-driven projects',
heading: 'Community projects',
headingLevel: 'h2',
type: 'grid',
columns: 2,
description:
"There are several other options to deploy Supabase. If you're interested in helping these projects, visit our [Community page](https://supabase.com/contribute).",
'These projects are maintained by the Supabase community, not by Supabase. To get involved, see the [Community page](https://supabase.com/contribute).',
items: [
{
title: 'Kubernetes',
href: 'https://github.com/supabase-community/supabase-kubernetes',
icon: '/docs/img/icons/kubernetes-icon',
hasLightIcon: false,
description: 'Helm charts to deploy a Supabase on Kubernetes.',
description: 'Run Supabase on Kubernetes with the Supabase Operator or a Helm chart.',
},
{
title: 'Traefik',
href: 'https://github.com/supabase-community/supabase-traefik',
icon: '/docs/img/icons/traefik-icon',
hasLightIcon: false,
description: 'A self-hosted Supabase setup with Traefik as a reverse proxy.',
title: 'Observability',
href: 'https://github.com/supabase-community/supabase-observability',
icon: { kind: 'grafana', color: '#F05A28', bg: 'rgba(240,90,40,0.1)' },
description:
'Collect logs, metrics, and traces from self-hosted Supabase with open-source tools.',
},
],
}
export const selfHostingThirdPartyGuides: ContentListingGroup = {
id: 'self-hosting-third-party-guides',
heading: 'Third-party guides',
headingLevel: 'h2',
type: 'grid',
columns: 2,
description:
"Guides written by other projects and companies. Supabase doesn't maintain them, so check that they match your version of self-hosted Supabase.",
items: [
{
title: 'Secure self-hosted Supabase with NetBird',
href: 'https://netbird.io/knowledge-hub/supabase-self-hosted-netbird',
icon: { kind: 'server', color: '#64748B', bg: 'rgba(100,116,139,0.1)' },
subtitle: 'By NetBird',
description:
'Keep Studio and Postgres off the public internet with NetBird network access controls.',
},
],
}
+4 -4
View File
@@ -3,7 +3,7 @@ import type { AiPromptId } from './ai-prompts.data'
export const monitoringAgents = {
health: {
id: 'health',
name: 'Health monitor',
name: 'Health',
promptId: 'monitoring-agent-health' as AiPromptId,
schedule: {
cadence: 'once per hour',
@@ -15,7 +15,7 @@ export const monitoringAgents = {
},
security: {
id: 'security',
name: 'Security monitor',
name: 'Security',
promptId: 'monitoring-agent-security' as AiPromptId,
schedule: {
cadence: 'once per day',
@@ -26,7 +26,7 @@ export const monitoringAgents = {
},
performance: {
id: 'performance',
name: 'Performance monitor',
name: 'Performance',
promptId: 'monitoring-agent-performance' as AiPromptId,
schedule: {
cadence: 'once per hour',
@@ -37,7 +37,7 @@ export const monitoringAgents = {
},
usage: {
id: 'usage',
name: 'Resource monitor',
name: 'Resources',
promptId: 'monitoring-agent-usage' as AiPromptId,
schedule: {
cadence: 'once each morning',
@@ -10,7 +10,7 @@ import {
describe('getMonitoringAgent', () => {
it('returns a registered agent', () => {
expect(getMonitoringAgent('health').name).toBe('Health monitor')
expect(getMonitoringAgent('health').name).toBe('Health')
})
it('fails clearly for an unknown id', () => {
+7 -7
View File
@@ -98,14 +98,14 @@ export function getMonitoringAgentHarnesses(agent: MonitoringAgent): MonitoringA
? 'https://code.claude.com/docs/en/desktop-scheduled-tasks'
: 'https://code.claude.com/docs/en/routines',
intro: isSubHourly
? `Create a Claude Desktop scheduled task that runs ${agent.name} ${cadence}.`
: `Create a Claude routine that runs ${agent.name} ${cadence}.`,
? `Create a Claude Desktop scheduled task that runs the ${agent.name} prompt ${cadence}.`
: `Create a Claude routine that runs the ${agent.name} prompt ${cadence}.`,
steps: [
MCP_STEP,
isSubHourly
? 'In the Claude Code Desktop app, open **Routines**, click **New routine**, and choose **Local**.'
: 'Open [Claude routines](https://claude.ai/code/routines) or run `/schedule` in Claude Code.',
`Name it ${agent.name}. Paste the [prompt](#${AGENT_PROMPT_ANCHOR}). Set the schedule to ${cadence}.`,
`Name it ${agent.name} checks. Paste the [prompt](#${AGENT_PROMPT_ANCHOR}). Set the schedule to ${cadence}.`,
],
note: isSubHourly
? 'Cloud routines have a 1-hour minimum. Use a [Desktop scheduled task](https://code.claude.com/docs/en/desktop-scheduled-tasks) for this cadence.'
@@ -117,11 +117,11 @@ export function getMonitoringAgentHarnesses(agent: MonitoringAgent): MonitoringA
icon: 'openai',
hasDistinctDarkIcon: true,
docsUrl: 'https://developers.openai.com/codex/app/automations',
intro: `Create a Codex scheduled task that runs ${agent.name} ${cadence}.`,
intro: `Create a Codex scheduled task that runs the ${agent.name} prompt ${cadence}.`,
steps: [
MCP_STEP,
'Open **Scheduled** in the ChatGPT desktop app, or ask Codex to create a standalone scheduled task.',
`Name it ${agent.name}. Paste the [prompt](#${AGENT_PROMPT_ANCHOR}). Set the schedule to ${cadence}. Each run should start a new chat.`,
`Name it ${agent.name} checks. Paste the [prompt](#${AGENT_PROMPT_ANCHOR}). Set the schedule to ${cadence}. Each run should start a new chat.`,
],
},
{
@@ -130,11 +130,11 @@ export function getMonitoringAgentHarnesses(agent: MonitoringAgent): MonitoringA
icon: 'cursor',
hasDistinctDarkIcon: true,
docsUrl: 'https://cursor.com/docs/cloud-agent/automations',
intro: `Create a Cursor automation that runs ${agent.name} ${cadence}.`,
intro: `Create a Cursor automation that runs the ${agent.name} prompt ${cadence}.`,
steps: [
MCP_STEP,
'Create an automation in the Agents Window, at [cursor.com/automations](https://cursor.com/automations), or with the `/automate` skill.',
`Name it ${agent.name}. Use a scheduled trigger (${cadence}, cron \`${cron}\`). Paste the [prompt](#${AGENT_PROMPT_ANCHOR}). Keep the agent read-only, with no repository.`,
`Name it ${agent.name} checks. Use a scheduled trigger (${cadence}, cron \`${cron}\`). Paste the [prompt](#${AGENT_PROMPT_ANCHOR}). Keep the agent read-only, with no repository.`,
],
},
]
+1
View File
@@ -33,6 +33,7 @@
"last-changed": "tsx scripts/last-changed.ts",
"last-changed:reset": "pnpm run last-changed -- --reset",
"lint": "eslint .",
"lint:ratchet": "tsx node_modules/eslint-config-supabase/ratchet-eslint-rules.ts --rules-file node_modules/eslint-config-supabase/ratchet-rules.json",
"postbuild": "pnpm run build:sitemap && ./../../scripts/upload-static-assets.sh",
"prebuild": "pnpm run codegen:graphql && pnpm run codegen:references && pnpm run codegen:examples && pnpm build:federated-content && pnpm run build:markdown && pnpm run build:gz-archive",
"predev": "pnpm run codegen:graphql && pnpm run codegen:references && pnpm run codegen:examples",
+15
View File
@@ -63,6 +63,8 @@ parameters:
description: |
A string used to distinguish different Supabase projects on the same host. Defaults to the working directory name when running `supabase init`.
Local projects started with the experimental `supabase stack` commands are identified by their project directory, git branch, and `--stack` name instead. See [Running multiple local projects](https://supabase.com/docs/guides/local-development/running-multiple-local-projects).
- id: 'api.enabled'
title: 'api.enabled'
tags: ['api']
@@ -1758,6 +1760,19 @@ parameters:
- name: Self-hosted Logflare Configuration
link: https://supabase.com/docs/reference/self-hosting-analytics/list-endpoints#getting-started
- id: 'experimental.stack'
title: 'experimental.stack'
tags: ['experimental']
required: false
default: 'false'
description: |
Turns on the experimental `supabase stack` commands, and makes the top-level `supabase start`, `supabase status`, and `supabase stop` commands run `supabase stack start`, `supabase stack status`, and `supabase stack stop`. The local targets of the `db`, `migration`, `test`, `gen`, `inspect`, `pull`, `storage`, `seed`, and `services` commands, and `functions serve`, also use the stack. With this setting, several local projects can run at the same time on one machine. On supported platforms a local project can also run without Docker.
Local projects started this way keep their own data, separate from local projects started with the default `supabase start`. The environment variable `SUPABASE_EXPERIMENTAL_STACK=1` or `0` overrides this value.
Note: This is an experimental feature and may change in future releases.
links:
- name: 'Running multiple local projects'
link: 'https://supabase.com/docs/guides/local-development/running-multiple-local-projects'
- id: 'experimental.webhooks.enabled'
title: 'experimental.webhooks.enabled'
tags: ['experimental']
+4
View File
@@ -62,3 +62,7 @@ If you add a new content collection or top-level route, add a matching redirect
(`/kb/<section>/:path+.md` → `/kb/markdown/<section>/:path+.md`), and check whether `generate-markdown.mjs`
needs updating too — the content export falls out of its generic `src/content/**` walk automatically, but
per-topic-style listing pages don't.
## Topic-specific guidance
Articles tagged with the `Comparison` topic are primarily oriented towards LLM crawlers (and not human readers). Because of this, these articles are hidden from the main site navigation.
+2 -2
View File
@@ -11,7 +11,7 @@ import {
import { TOPICS, topicToSlug } from '../lib/topics'
const topics = TOPICS.map((topic) => ({
const visibleTopics = TOPICS.filter((topic) => topic.visible).map((topic) => ({
label: topic.name,
href: `${import.meta.env.BASE_URL}/topics/${topicToSlug(topic.name)}`,
}))
@@ -26,7 +26,7 @@ const resources = [
]
const menus = [
{ label: 'Topics', items: topics },
{ label: 'Topics', items: visibleTopics },
{ label: 'Resources', items: resources },
]
+41 -5
View File
@@ -5,41 +5,77 @@
// `pinned` topics surface as cards on the homepage (see src/pages/index.astro)
// — keep this to a handful so that section stays a highlights row, not a
// second copy of the full topic list.
//
// `visible` controls whether the topic appears in nav and homepage sections.
// Topics with `visible: false` still have a working listing page at /topics/[slug].
// Some topics might not need to be visible to end users since their primary audience
// is LLM crawlers.
export const TOPICS = [
{
name: 'Migration',
description: 'Moving data, schemas, or projects onto Supabase.',
pinned: false,
visible: true,
},
{
name: 'Comparison',
description: 'How Supabase compares to other databases and platforms.',
pinned: false,
visible: false,
},
{
name: 'Troubleshooting',
description: 'Common errors and how to resolve them.',
pinned: false,
visible: true,
},
{ name: 'Troubleshooting', description: 'Common errors and how to resolve them.', pinned: false },
{
name: 'Tutorial',
description: 'Step-by-step walkthroughs for building with Supabase.',
pinned: true,
visible: true,
},
{
name: 'Storage',
description: 'Uploading, managing, and serving files.',
pinned: false,
visible: true,
},
{ name: 'Storage', description: 'Uploading, managing, and serving files.', pinned: false },
{
name: 'Auth',
description: 'Authentication, authorization, and user management.',
pinned: true,
visible: true,
},
{
name: 'Database',
description: 'Postgres schemas, queries, and performance.',
pinned: true,
visible: true,
},
{ name: 'Database', description: 'Postgres schemas, queries, and performance.', pinned: true },
{
name: 'Edge Functions',
description: 'Deploying and running serverless functions.',
pinned: false,
visible: true,
},
{
name: 'Queues',
description: 'Background jobs and message processing.',
pinned: false,
visible: true,
},
{
name: 'Realtime',
description: 'Broadcast, presence, and database changes.',
pinned: false,
visible: true,
},
{ name: 'Queues', description: 'Background jobs and message processing.', pinned: false },
{ name: 'Realtime', description: 'Broadcast, presence, and database changes.', pinned: false },
{
name: 'Supabase Platform',
description: 'Project settings, billing, and infrastructure.',
pinned: false,
visible: true,
},
] as const
+3 -2
View File
@@ -7,7 +7,8 @@ import Layout from '../layouts/Layout.astro'
import { TOPICS, topicToSlug } from '../lib/topics'
const guides = await getCollection('guides')
const pinnedTopics = TOPICS.filter((topic) => topic.pinned)
const visibleTopics = TOPICS.filter((topic) => topic.visible)
const pinnedTopics = visibleTopics.filter((topic) => topic.pinned)
const featuredGuides = guides.filter((guide) => guide.data.pinned)
---
@@ -63,7 +64,7 @@ const featuredGuides = guides.filter((guide) => guide.data.pinned)
<section>
<h2 class="mb-6 text-2xl font-medium text-foreground">All Topics</h2>
<ul class="flex flex-col gap-2">
{TOPICS.map((topic) => (
{visibleTopics.map((topic) => (
<li>
<a
href={`${import.meta.env.BASE_URL}/topics/${topicToSlug(topic.name)}`}
+25
View File
@@ -0,0 +1,25 @@
{
"rules": {
"shadcn/no-arbitrary-values": 7,
"shadcn/no-unknown-classes": 6,
"shadcn/no-raw-colors": 7
},
"ruleFiles": {
"shadcn/no-arbitrary-values": {
"components/command-menu.tsx": 2,
"components/command.tsx": 2,
"components/course-hero.tsx": 1,
"components/mdx-components.tsx": 2
},
"shadcn/no-unknown-classes": {
"components/mdx-components.tsx": 5,
"components/side-navigation-item.tsx": 1
},
"shadcn/no-raw-colors": {
"components/chapter-completion.tsx": 1,
"components/course-hero.tsx": 1,
"components/mdx-components.tsx": 2,
"components/side-navigation.tsx": 3
}
}
}
+1
View File
@@ -13,6 +13,7 @@
"internal:sync": "tsx ./scripts/sync-internal-content.mts",
"start": "next start",
"lint": "eslint .",
"lint:ratchet": "tsx node_modules/eslint-config-supabase/ratchet-eslint-rules.ts --rules-file node_modules/eslint-config-supabase/ratchet-rules.json",
"clean": "rimraf .next .turbo tsconfig.tsbuildinfo .contentlayer .velite tsconfig.tsbuildinfo",
"typecheck": "pnpm build:content && tsc --noEmit"
},
+260 -1
View File
@@ -11,7 +11,10 @@
"jsx-a11y/no-redundant-roles": 2,
"jsx-a11y/anchor-is-valid": 6,
"jsx-a11y/heading-has-content": 2,
"valtio/state-snapshot-rule": 1
"valtio/state-snapshot-rule": 1,
"shadcn/no-arbitrary-values": 212,
"shadcn/no-unknown-classes": 151,
"shadcn/no-raw-colors": 64
},
"ruleFiles": {
"react-hooks/exhaustive-deps": {
@@ -1000,6 +1003,262 @@
},
"valtio/state-snapshot-rule": {
"state/storage-explorer.tsx": 1
},
"shadcn/no-arbitrary-values": {
"components/grid/components/editor/DateTimeEditor.tsx": 2,
"components/grid/components/editor/JsonEditor.tsx": 4,
"components/grid/components/editor/TextEditor.tsx": 4,
"components/grid/components/editor/TruncatedWarningOverlay.tsx": 1,
"components/grid/components/header/ExportDialog.tsx": 2,
"components/interfaces/Account/AccessTokens/Classic/ExperimentalTokenDropdown.tsx": 2,
"components/interfaces/Account/AccessTokens/Scoped/Form/RiskMarker.tsx": 2,
"components/interfaces/Account/AuditLogs/AuditLogOverview.tsx": 2,
"components/interfaces/Account/Preferences/HotkeyToggle.tsx": 1,
"components/interfaces/Auth/EmailTemplates/CustomEmailTemplateRestrictionAdmonition.tsx": 2,
"components/interfaces/Compute/RuntimeBadge.tsx": 1,
"components/interfaces/ConfigDrift/ConfigurationDriftPage.tsx": 1,
"components/interfaces/Database/Policies/PolicyEditorPanel/LockedQuerySection.tsx": 10,
"components/interfaces/Database/Policies/PolicyEditorPanel/index.tsx": 6,
"components/interfaces/Database/Privileges/PrivilegesHead.tsx": 1,
"components/interfaces/Database/Replication/ReplicationPipelineStatus/ReplicationPipelineStatus.tsx": 2,
"components/interfaces/Database/Roles/RoleRow.tsx": 1,
"components/interfaces/Database/Schemas/DefaultEdge.tsx": 2,
"components/interfaces/Database/Schemas/SchemaGraph.tsx": 2,
"components/interfaces/Database/Schemas/SchemaTableNode.tsx": 10,
"components/interfaces/Database/Tables/ConstraintToken.tsx": 4,
"components/interfaces/DiskManagement/DiskManagementReviewAndSubmitDialog/DiskManagementReviewAndSubmitDialog.tsx": 2,
"components/interfaces/DiskManagement/fields/StorageTypeField.tsx": 1,
"components/interfaces/DiskManagement/ui/DiskSpaceBar.tsx": 1,
"components/interfaces/Explorer/ExplorerOnboardingWireframes.tsx": 5,
"components/interfaces/Explorer/QueryEditor/QueryRunButton.tsx": 2,
"components/interfaces/Functions/EdgeFunctionOverview/EdgeFunctionInvocationsChart.tsx": 1,
"components/interfaces/Home/ProjectList/ProjectCard.tsx": 2,
"components/interfaces/Integrations/Integration/IntegrationOverviewTabV2/InstallIntegrationSheet/InstallationOverview.tsx": 1,
"components/interfaces/Integrations/Marketplace/MarketplaceFeaturedHeroGrid.tsx": 1,
"components/interfaces/Integrations/Marketplace/MarketplaceSidebar.tsx": 1,
"components/interfaces/Integrations/Queues/QueueTab.tsx": 1,
"components/interfaces/Integrations/VercelGithub/ProjectLinker.tsx": 2,
"components/interfaces/Integrations/Warehouse/WarehouseTableStatusList.tsx": 1,
"components/interfaces/Integrations/Wrappers/WrapperRow.tsx": 2,
"components/interfaces/LogDrains/OrgAuditLogDrains.tsx": 2,
"components/interfaces/Organization/BillingSettings/Subscription/PaymentMethodSelection.tsx": 1,
"components/interfaces/Organization/BillingSettings/Subscription/PlanCard.tsx": 11,
"components/interfaces/Organization/CloudMarketplace/AwsMarketplaceOnboarding.components.tsx": 2,
"components/interfaces/Organization/Documents/ISO27001.tsx": 1,
"components/interfaces/Organization/Documents/SOC2.tsx": 1,
"components/interfaces/Organization/HeaderBanner.tsx": 2,
"components/interfaces/Organization/OAuthApps/AuthorizeRequesterDetails.tsx": 1,
"components/interfaces/ProjectCreation/RegionSelector.tsx": 1,
"components/interfaces/ProjectHome/HighAvailabilityBadge.tsx": 5,
"components/interfaces/ProjectHome/PlanUsageCard.tsx": 1,
"components/interfaces/ProjectHome/ServerLightGrid.tsx": 1,
"components/interfaces/QueryInsights/QueryInsightsChart/QueryInsightsChart.tsx": 2,
"components/interfaces/QueryInsights/hooks/useQueryInsightsTableColumns.tsx": 2,
"components/interfaces/Realtime/Inspector/RealtimeFilterPopover/index.tsx": 1,
"components/interfaces/Reports/ReportBlock/ChartBlock.tsx": 2,
"components/interfaces/Reports/ReportFilterBar.tsx": 3,
"components/interfaces/SQLEditor/DownloadSnippetModal.tsx": 1,
"components/interfaces/Settings/General/Infrastructure/RestartServerButton.tsx": 2,
"components/interfaces/Settings/General/TransferProjectPanel/TransferProjectButton.tsx": 1,
"components/interfaces/Settings/Infrastructure/InfrastructureConfiguration/MapView.tsx": 2,
"components/interfaces/Settings/Integrations/VercelIntegration/VercelSection.tsx": 1,
"components/interfaces/Settings/Logs/PreviewFilterPanel.tsx": 1,
"components/interfaces/Sidebar.tsx": 1,
"components/interfaces/SignIn/SignInMfaForm.tsx": 1,
"components/interfaces/Storage/AnalyticsBuckets/AnalyticsBucketDetails/CreateTable/CreateTableInstructionsDialog.tsx": 2,
"components/interfaces/Storage/AnalyticsBuckets/AnalyticsBucketDetails/NamespaceWithTables/TableRowComponent.tsx": 2,
"components/interfaces/Storage/BucketFilePickerDialog/BucketFilePickerHeader.tsx": 2,
"components/interfaces/Storage/StorageExplorer/FileExplorerRow.tsx": 2,
"components/interfaces/Storage/StorageExplorer/MoveItemsFolderPickerRow.tsx": 2,
"components/interfaces/Storage/StoragePolicies/StoragePoliciesEditor.tsx": 1,
"components/interfaces/Support/AttachmentUpload.tsx": 1,
"components/interfaces/TableGridEditor/GridHeaderActions.tsx": 1,
"components/interfaces/TableGridEditor/SidePanelEditor/ForeignKeySelector/ForeignKeySelector.tsx": 4,
"components/interfaces/TableGridEditor/SidePanelEditor/RowEditor/JsonEditor/index.tsx": 1,
"components/interfaces/TableGridEditor/SidePanelEditor/RowEditor/TextEditor.tsx": 1,
"components/interfaces/TableGridEditor/SidePanelEditor/TableEditor/Column.tsx": 1,
"components/interfaces/TableGridEditor/SidePanelEditor/TableEditor/ForeignKeysManagement/ForeignKeyRow.tsx": 1,
"components/interfaces/UnifiedLogs/ServiceFlow/components/shared/DetailRow.tsx": 1,
"components/interfaces/UnifiedLogs/UnifiedLogs.fields.tsx": 5,
"components/interfaces/UnifiedLogs/components/Columns.tsx": 3,
"components/interfaces/UnifiedLogs/components/LogsList.tsx": 1,
"components/interfaces/UnifiedLogs/components/UserLogFilterControl.tsx": 2,
"components/layouts/Navigation/FloatingMobileToolbar/FloatingMobileToolbar.tsx": 2,
"components/layouts/Navigation/NavigationBar/MobileNavigationBar.tsx": 1,
"components/layouts/Navigation/NavigationBar/ProjectBranchSelectorSheetTabTrigger.tsx": 2,
"components/layouts/ProjectLayout/LogicalBackupCliInstructions.tsx": 1,
"components/layouts/SignInLayout/SignInLayout.tsx": 1,
"components/ui/AIEditor/AskAIWidget.tsx": 1,
"components/ui/AiAssistantDropdown.tsx": 2,
"components/ui/BannerStack/Banners/BannerExplorer.tsx": 3,
"components/ui/BannerStack/Banners/BannerSelect2026.tsx": 13,
"components/ui/Charts/BarChart.tsx": 1,
"components/ui/Charts/ComposedChart.utils.tsx": 2,
"components/ui/DataTable/DataTableFilters/DataTableFilterCheckbox.tsx": 2,
"components/ui/DataTable/DataTableFilters/DataTableFilterCheckboxAsync.tsx": 1,
"components/ui/DataTable/DataTableFilters/DataTableFilterResetButton.tsx": 1,
"components/ui/DataTable/primitives/InputWithAddons.tsx": 1,
"components/ui/DataTable/primitives/Kbd.tsx": 1,
"components/ui/EntityTypeIcon.tsx": 1,
"components/ui/ErrorCodeTooltip/ErrorCodeTooltip.tsx": 1,
"components/ui/GlobalShortcuts/ShortcutsReferenceSheet.tsx": 1,
"components/ui/ShortcutBadge.tsx": 1,
"components/ui/ShortcutTooltip.tsx": 1,
"pages/project/[ref]/settings/infrastructure/replica/[replicaId].tsx": 1,
"pages/project/[ref]/settings/log-drains.tsx": 2,
"state/shortcuts/useDynamicShortcut.tsx": 1,
"state/shortcuts/useShortcut.tsx": 1
},
"shadcn/no-unknown-classes": {
"components/grid/SupabaseGrid.tsx": 1,
"components/grid/components/editor/JsonEditor.tsx": 1,
"components/grid/components/editor/TextEditor.tsx": 1,
"components/grid/components/grid/ColumnHeader.tsx": 6,
"components/grid/components/grid/ColumnOverlayItem.tsx": 5,
"components/grid/components/grid/Grid.tsx": 1,
"components/grid/components/grid/SelectColumn.tsx": 5,
"components/interfaces/App/FeaturePreview/FeaturePreviewModal.tsx": 1,
"components/interfaces/Auth/AuthProvidersForm/FormField.tsx": 1,
"components/interfaces/Auth/AuthProvidersForm/index.tsx": 1,
"components/interfaces/Auth/CustomAuthProviders/CreateOrUpdateCustomProviderSheet.tsx": 1,
"components/interfaces/Auth/CustomAuthProviders/CustomAuthProvidersList.tsx": 1,
"components/interfaces/Auth/OAuthApps/CreateOrUpdateOAuthAppSheet.tsx": 1,
"components/interfaces/Auth/OAuthApps/OAuthAppsList.tsx": 1,
"components/interfaces/Auth/Overview/OverviewLearnMore.tsx": 1,
"components/interfaces/Auth/Users/UserPanel.tsx": 1,
"components/interfaces/Auth/Users/UsersGridComponents.tsx": 3,
"components/interfaces/Billing/Payment/AddNewPaymentMethodModal.tsx": 1,
"components/interfaces/BranchManagement/ConnectToGitHub.tsx": 1,
"components/interfaces/ConnectSheet/content/steps/direct-connection/IPv4StatusPanel.tsx": 1,
"components/interfaces/Database/EnumeratedTypes/CreateEnumeratedTypeSidePanel.tsx": 1,
"components/interfaces/Database/EnumeratedTypes/EditEnumeratedTypeSidePanel.tsx": 1,
"components/interfaces/Database/Functions/FunctionsList/FunctionList.tsx": 1,
"components/interfaces/Database/Hooks/EditHookPanel.tsx": 1,
"components/interfaces/Database/Policies/PolicyEditorPanel/PolicyEditorPanelHeader.tsx": 1,
"components/interfaces/Database/Policies/PolicyEditorPanel/PolicyTemplates.tsx": 1,
"components/interfaces/Database/Replication/ReplicationDiagram/Edges.tsx": 2,
"components/interfaces/Database/Replication/ReplicationDiagram/index.tsx": 1,
"components/interfaces/Database/Schemas/SchemaGraph.tsx": 1,
"components/interfaces/Database/Schemas/SchemaTableNode.tsx": 2,
"components/interfaces/Database/Triggers/ChooseFunctionForm.tsx": 1,
"components/interfaces/Database/Triggers/EventTriggersList/EventTriggerList.tsx": 1,
"components/interfaces/Database/Triggers/TriggersList/TriggerList.tsx": 1,
"components/interfaces/Docs/DocSection.tsx": 1,
"components/interfaces/Home/ProjectList/ShimmeringCard.tsx": 2,
"components/interfaces/Integrations/CronJobs/CronJobPage.tsx": 1,
"components/interfaces/Integrations/CronJobs/CronJobTableCell.tsx": 1,
"components/interfaces/Integrations/CronJobs/CronJobsTab.DataGrid.tsx": 1,
"components/interfaces/Integrations/CronJobs/PreviousRunsTab.tsx": 2,
"components/interfaces/Integrations/Queues/QueuesTab.tsx": 1,
"components/interfaces/Integrations/Queues/SingleQueue/QueueDataGrid.tsx": 1,
"components/interfaces/Integrations/Vault/Secrets/SecretsManagement.tsx": 1,
"components/interfaces/JwtSecrets/jwt-secret-keys-table/action-panel.tsx": 1,
"components/interfaces/Organization/BillingSettings/Subscription/UpgradeModal.tsx": 1,
"components/interfaces/Organization/ProjectClaim/confirm.tsx": 1,
"components/interfaces/Platform/Webhooks/PlatformWebhooksEndpointDetails.tsx": 1,
"components/interfaces/ProjectAPIDocs/ContentSnippet.tsx": 4,
"components/interfaces/ProjectAPIDocs/ResourceContent.tsx": 4,
"components/interfaces/QueryPerformance/IndexAdvisor/IndexSuggestionIcon.tsx": 1,
"components/interfaces/QueryPerformance/QueryIndexes.tsx": 2,
"components/interfaces/Realtime/Inspector/MessagesTable.tsx": 2,
"components/interfaces/Realtime/Inspector/SelectedRealtimeMessagePanel.tsx": 1,
"components/interfaces/Reports/ReportBlock/ChartBlock.tsx": 1,
"components/interfaces/Reports/ReportBlock/ReportBlockContainer.tsx": 1,
"components/interfaces/Reports/ReportPadding.tsx": 1,
"components/interfaces/Reports/renderers/ApiRenderers.tsx": 1,
"components/interfaces/Settings/API/HardenAPIModal.tsx": 4,
"components/interfaces/Settings/Addons/Addons.tsx": 2,
"components/interfaces/Settings/Database/DatabaseReadOnlyAlert.tsx": 1,
"components/interfaces/Settings/General/CustomDomainConfig/CustomDomainsShimmerLoader.tsx": 2,
"components/interfaces/Settings/Infrastructure/InfrastructureConfiguration/DiagramFlow.tsx": 1,
"components/interfaces/Settings/Infrastructure/InfrastructureConfiguration/Edge.tsx": 2,
"components/interfaces/Settings/Infrastructure/InfrastructureConfiguration/HaInstanceConfiguration.tsx": 1,
"components/interfaces/Settings/Infrastructure/InfrastructureConfiguration/InstanceConfiguration.tsx": 1,
"components/interfaces/Settings/Infrastructure/InfrastructureConfiguration/MapView.tsx": 1,
"components/interfaces/Settings/Integrations/VercelIntegration/VercelSection.tsx": 1,
"components/interfaces/Settings/Logs/LogTable.tsx": 3,
"components/interfaces/Settings/Logs/LogsFormatters.tsx": 4,
"components/interfaces/Storage/BucketFilePickerDialog/BucketFilePickerExplorer.tsx": 1,
"components/interfaces/Storage/BucketFilePickerDialog/BucketFilePickerRow.tsx": 1,
"components/interfaces/Storage/StorageBucketsError.tsx": 1,
"components/interfaces/Storage/StorageExplorer/FileExplorer.tsx": 1,
"components/interfaces/Storage/StorageExplorer/FileExplorerRow.tsx": 2,
"components/interfaces/Storage/StorageExplorer/FileExplorerRowEditing.tsx": 2,
"components/interfaces/TableGridEditor/SidePanelEditor/RowEditor/ForeignRowSelector/SelectorGrid.tsx": 2,
"components/interfaces/TableGridEditor/SidePanelEditor/RowEditor/TextEditor.tsx": 1,
"components/interfaces/TableGridEditor/SidePanelEditor/TableEditor/Column.tsx": 1,
"components/interfaces/TableGridEditor/SidePanelEditor/TableEditor/ColumnManagement.tsx": 1,
"components/interfaces/TableGridEditor/SidePanelEditor/TableEditor/ForeignKeysManagement/ForeignKeyRow.tsx": 1,
"components/interfaces/UnifiedLogs/components/LogsListPanel.tsx": 2,
"components/layouts/Navigation/FloatingMobileToolbar/FloatingMobileToolbar.tsx": 1,
"components/layouts/Navigation/NavigationBar/MobileNavigationBar.tsx": 1,
"components/layouts/SQLEditorLayout/SQLEditorNavV2/SQLEditorNav.tsx": 1,
"components/layouts/WizardLayout.tsx": 2,
"components/ui/AIAssistantPanel/CollapsibleCodeBlock.tsx": 1,
"components/ui/AIAssistantPanel/MessageMarkdown.tsx": 1,
"components/ui/AIAssistantPanel/SupportAssistantSuccessCardContent.tsx": 1,
"components/ui/AIAssistantPanel/elements/Tool.tsx": 1,
"components/ui/AutoEnableRLSNotice.tsx": 1,
"components/ui/CodeEditor/CodeEditor.tsx": 2,
"components/ui/DataGridResults/CellDetailPanel.tsx": 1,
"components/ui/DataTable/DataTableInfinite.tsx": 1,
"components/ui/EdgeFunctionBlock/EdgeFunctionBlock.tsx": 1,
"components/ui/EditorPanel/EditorPanel.tsx": 1,
"components/ui/FileExplorerAndEditor/index.tsx": 2,
"components/ui/Forms/FormSection.tsx": 2,
"components/ui/QueryBlock/QueryBlock.tsx": 3,
"components/ui/ShimmerLine.tsx": 1,
"components/ui/Shimmers.tsx": 3,
"pages/project/[ref]/functions/[functionSlug]/code.tsx": 1,
"pages/project/[ref]/storage/vectors/buckets/[bucketId].tsx": 1
},
"shadcn/no-raw-colors": {
"components/interfaces/Advisors/AdvisorRuleItem.tsx": 1,
"components/interfaces/Auth/EmailTemplates/TemplateEditor.tsx": 1,
"components/interfaces/BranchManagement/BranchPanels.tsx": 1,
"components/interfaces/BranchManagement/CreateBranchModal.tsx": 6,
"components/interfaces/BranchManagement/DeleteBranchModal.tsx": 1,
"components/interfaces/ConnectSheet/content/steps/direct-connection/IPv4StatusPanel.tsx": 1,
"components/interfaces/Database/Functions/FunctionsList/FunctionsList.tsx": 2,
"components/interfaces/Database/Hooks/DeleteHookModal.tsx": 2,
"components/interfaces/Database/Migrations/Migrations.tsx": 1,
"components/interfaces/Database/Triggers/DeleteEventTrigger.tsx": 1,
"components/interfaces/Database/Triggers/TriggersList/TriggersList.tsx": 2,
"components/interfaces/DiskManagement/ui/DiskSpaceBar.tsx": 1,
"components/interfaces/Functions/FunctionsEmptyState.tsx": 1,
"components/interfaces/Integrations/CronJobs/DeleteCronJob.tsx": 1,
"components/interfaces/Integrations/Marketplace/MarketplaceFeaturedHeroGrid.tsx": 2,
"components/interfaces/Integrations/Queues/SingleQueue/DeleteQueue.tsx": 1,
"components/interfaces/Integrations/Queues/SingleQueue/PurgeQueue.tsx": 1,
"components/interfaces/Integrations/VercelGithub/IntegrationPanels.tsx": 1,
"components/interfaces/JwtSecrets/jwt-secret-keys-table/signing-key-row.tsx": 1,
"components/interfaces/Organization/BillingSettings/BillingBreakdown/BillingMetric.tsx": 2,
"components/interfaces/Organization/BillingSettings/Subscription/ExitSurveyModal.tsx": 1,
"components/interfaces/Organization/BillingSettings/Subscription/UpgradeModal.tsx": 1,
"components/interfaces/Organization/IntegrationSettings/SidePanelVercelProjectLinker.tsx": 1,
"components/interfaces/Organization/TeamSettings/MembersView.tsx": 1,
"components/interfaces/ProjectCreation/ProjectCreationForm.tsx": 1,
"components/interfaces/Reports/renderers/ApiRenderers.tsx": 1,
"components/interfaces/SQLEditor/UtilityPanel/UtilityActions.tsx": 4,
"components/interfaces/Settings/Integrations/IntegrationsSettings.tsx": 1,
"components/interfaces/Settings/Logs/Logs.DatePickers.tsx": 1,
"components/interfaces/SignIn/ForgotPasswordWizard.tsx": 2,
"components/interfaces/Storage/StoragePolicies/PolicyTemplates/TemplatesList.tsx": 1,
"components/interfaces/TableGridEditor/SidePanelEditor/OperationQueueSidePanel/DeleteRowOperationItem.tsx": 1,
"components/interfaces/TableGridEditor/SidePanelEditor/RowEditor/InputField.tsx": 1,
"components/interfaces/TableGridEditor/SidePanelEditor/RowEditor/JsonEditor/DrilldownViewer/DrilldownPane.tsx": 2,
"components/interfaces/TableGridEditor/SidePanelEditor/SpreadsheetImport/SpreadSheetFileUpload.tsx": 2,
"components/interfaces/UnifiedLogs/UnifiedLogs.utils.ts": 1,
"components/layouts/AppLayout/EnableBranchingButton/BranchingPITRNotice.tsx": 1,
"components/layouts/SQLEditorLayout/SQLEditorNavV2/SQLEditorTreeViewItem.tsx": 2,
"components/ui/AIAssistantPanel/Confirm.tsx": 1,
"components/ui/AIAssistantPanel/Message.Actions.tsx": 1,
"components/ui/AIAssistantPanel/MessagePartQueryLogs.tsx": 1,
"components/ui/DatePicker/TimeSplitInput.tsx": 1,
"components/ui/EdgeFunctionBlock/EdgeFunctionBlock.tsx": 2,
"components/ui/FileExplorerAndEditor/index.tsx": 1,
"components/ui/ProjectSettings/ToggleLegacyApiKeys.tsx": 1,
"components/ui/SqlWarningAdmonition.tsx": 1
}
}
}
@@ -23,6 +23,7 @@ import { DestinationType } from '../DestinationPanel.types'
import { TableOptions } from './BigQuery/TableOptions'
import {
DEFAULT_CONNECTION_POOL_SIZE,
DEFAULT_DUCKLAKE_POOL_SIZE,
DEFAULT_MAX_COPY_CONNECTIONS_PER_TABLE,
DEFAULT_MAX_FILL_MS,
DEFAULT_MAX_TABLE_SYNC_WORKERS,
@@ -55,11 +56,39 @@ export const AdvancedSettings = ({
<div className="flex flex-col items-start gap-0.5">
<span className="text-sm font-medium">Advanced settings</span>
<span className="text-sm text-foreground-lighter font-normal">
Customize how the pipeline syncs and replicates data.
{type === 'DuckLake'
? 'Adjust catalog connections and replication settings.'
: 'Customize how the pipeline syncs and replicates data.'}
</span>
</div>
</AccordionTrigger>
<AccordionContent className="pb-0! pt-3 [&>div]:flex [&>div]:flex-col [&>div]:gap-y-4 [&>div]:px-5">
{type === 'DuckLake' && (
<FormField
control={form.control}
name="ducklakePoolSize"
render={({ field }) => (
<FormItemLayout
layout="horizontal"
label="Pool size"
description="Maximum concurrent connections this pipeline opens to the catalog. Choose 1 to 6."
>
<FormControl>
<Input
{...field}
type="number"
min={1}
max={6}
value={field.value ?? ''}
onChange={handleNumberChange(field)}
placeholder={String(DEFAULT_DUCKLAKE_POOL_SIZE)}
/>
</FormControl>
</FormItemLayout>
)}
/>
)}
<FormField
control={form.control}
name="maxFillMs"
@@ -107,7 +136,7 @@ export const AdvancedSettings = ({
step={1}
value={field.value ?? ''}
onChange={handleNumberChange(field)}
placeholder={`Default: ${DEFAULT_MAX_TABLE_SYNC_WORKERS}`}
placeholder={String(DEFAULT_MAX_TABLE_SYNC_WORKERS)}
/>
<InputGroupAddon align="inline-end">
<InputGroupText>workers</InputGroupText>
@@ -136,7 +165,7 @@ export const AdvancedSettings = ({
step={1}
value={field.value ?? ''}
onChange={handleNumberChange(field)}
placeholder={`Default: ${DEFAULT_MAX_COPY_CONNECTIONS_PER_TABLE}`}
placeholder={String(DEFAULT_MAX_COPY_CONNECTIONS_PER_TABLE)}
/>
<InputGroupAddon align="inline-end">
<InputGroupText>connections</InputGroupText>
@@ -201,7 +230,7 @@ export const AdvancedSettings = ({
step={1}
value={field.value ?? ''}
onChange={handleNumberChange(field)}
placeholder={`Default: ${DEFAULT_CONNECTION_POOL_SIZE}`}
placeholder={String(DEFAULT_CONNECTION_POOL_SIZE)}
/>
<InputGroupAddon align="inline-end">
<InputGroupText>connections</InputGroupText>
@@ -311,6 +311,16 @@ const baseClickHouseFormData = {
}
describe('DestinationForm.utils DuckLake', () => {
it('uses the default pool size when the advanced field is cleared', () => {
const config = buildDestinationConfigForValidation({
projectRef: 'project-ref',
selectedType: 'DuckLake',
data: { ...baseDucklakeFormData, ducklakePoolSize: '' },
})
expect(config).toMatchObject({ ducklake: { poolSize: undefined } })
})
it('builds DuckLake validation config with required fields trimmed and blank optionals removed', () => {
const config = buildDestinationConfigForValidation({
projectRef: 'project-ref',
@@ -332,7 +332,7 @@ const buildDucklakeConfig = (
catalogProjectRef: normalizeRequiredString(data.ducklakeCatalogProjectRef),
storageProjectRef: normalizeRequiredString(data.ducklakeStorageProjectRef),
bucket: normalizeRequiredString(data.ducklakeStorageBucket),
poolSize: data.ducklakePoolSize,
poolSize: data.ducklakePoolSize === '' ? undefined : data.ducklakePoolSize,
metadataSchema: normalizeOptionalString(data.ducklakeMetadataSchema),
}
return supabaseConfig
@@ -341,7 +341,7 @@ const buildDucklakeConfig = (
const manualConfig: DucklakeManualDestinationConfig = {
catalogUrl: data.ducklakeCatalogUrl ?? '',
dataPath: data.ducklakeDataPath ?? '',
poolSize: data.ducklakePoolSize,
poolSize: data.ducklakePoolSize === '' ? undefined : data.ducklakePoolSize,
s3AccessKeyId: normalizeRequiredString(data.ducklakeS3AccessKeyId),
s3SecretAccessKey: normalizeRequiredString(data.ducklakeS3SecretAccessKey),
s3Region: normalizeRequiredString(data.ducklakeS3Region),
@@ -25,29 +25,29 @@ export const ANALYTICS_BUCKET_NAMESPACE_FIELD_COPY = {
export const DUCKLAKE_CATALOG_PROJECT_FIELD_COPY = {
label: 'Catalog project',
description:
"Pipelines connects to this project's Postgres instance to store the DuckLake catalog.",
description: 'Postgres project that stores this DuckLake’s metadata.',
} as const
export const DUCKLAKE_STORAGE_PROJECT_FIELD_COPY = {
label: 'Storage project',
description: 'The project whose object storage holds the DuckLake data files.',
description: 'Supabase project that stores the DuckLake data files.',
} as const
export const DUCKLAKE_BUCKET_FIELD_COPY = {
label: 'Bucket',
description: 'The bucket in which DuckLake data files will be stored.',
description: 'Files bucket for DuckLake data.',
} as const
export const DUCKLAKE_CATALOG_URL_FIELD_COPY = {
label: 'Catalog URL',
createDescription: 'A Postgres connection string for the DuckLake catalog.',
createDescription:
'Postgres URL for an existing database. Add TLS settings to the URL if required.',
editDescription: 'Stored catalog URL is hidden. Enter a new URL to replace it.',
} as const
export const DUCKLAKE_DATA_PATH_FIELD_COPY = {
label: 'Data path',
description: 'An S3 path where DuckLake data files will be written.',
description: 'S3 path for DuckLake data files.',
} as const
export const SNOWFLAKE_ACCOUNT_ID_FIELD_COPY = {
@@ -12,10 +12,14 @@ export const DuckLakeFormSchema = z.object({
ducklakeCatalogUrl: z.string().optional(),
ducklakeDataPath: z.string().optional(),
ducklakePoolSize: z
.number()
.int()
.min(1, 'Pool size must be greater than 0.')
.max(6, 'Pool size must be 6 or less.')
.union([
z.literal(''),
z
.number()
.int()
.min(1, 'Pool size must be greater than 0.')
.max(6, 'Pool size must be 6 or less.'),
])
.optional(),
ducklakeS3AccessKeyId: z.string().optional(),
ducklakeS3SecretAccessKey: z.string().optional(),
@@ -1,12 +1,21 @@
import { Check, Database, Eye, EyeOff, Plus, SlidersHorizontal } from 'lucide-react'
import { Check, Eye, EyeOff, Plus } from 'lucide-react'
import { useMemo, useState } from 'react'
import { useWatch, type UseFormReturn } from 'react-hook-form'
import { toast } from 'sonner'
import {
Button,
cn,
ComboboxTrigger,
Command,
CommandEmpty,
CommandGroup,
CommandInput,
CommandItem,
CommandList,
CommandSeparator,
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogSection,
@@ -15,6 +24,12 @@ import {
FormControl,
FormField,
Input,
Popover,
PopoverContent,
PopoverTrigger,
RadioGroupStacked,
RadioGroupStackedItem,
ScrollArea,
Select,
SelectContent,
SelectGroup,
@@ -26,7 +41,7 @@ import { Input as PasswordInput } from 'ui-patterns/DataInputs/Input'
import { FormItemLayout } from 'ui-patterns/form/FormItemLayout/FormItemLayout'
import { SelectionListState } from 'ui-patterns/SelectionListState'
import { DEFAULT_DUCKLAKE_POOL_SIZE, STORED_SECRET_PLACEHOLDER } from '../DestinationForm.constants'
import { STORED_SECRET_PLACEHOLDER } from '../DestinationForm.constants'
import type { DestinationPanelSchemaType } from '../DestinationForm.schema'
import {
DUCKLAKE_BUCKET_FIELD_COPY,
@@ -55,16 +70,15 @@ import { PROJECT_STATUS } from '@/lib/constants'
const DUCKLAKE_MODE_OPTIONS = [
{
value: DUCKLAKE_MODE_SUPABASE,
icon: Database,
label: 'Use Supabase',
label: 'Select Supabase projects',
description:
'Create or use a DuckLake backed by your Supabase projects. Catalog and storage are managed for you.',
'Choose projects for the Postgres catalog and Storage bucket. They can be the same project; Pipelines creates credentials.',
},
{
value: DUCKLAKE_MODE_CUSTOM,
icon: SlidersHorizontal,
label: 'Custom parameters',
description: 'Bring your own Postgres catalog and S3-compatible object storage credentials.',
label: 'Enter connection details',
description:
'Provide a Postgres catalog URL and S3-compatible storage details and credentials.',
},
] as const
@@ -76,44 +90,20 @@ const DuckLakeModeSelector = ({
onChange: (value: DucklakeMode) => void
}) => {
return (
<div
role="radiogroup"
aria-label="DuckLake configuration mode"
className="grid grid-cols-2 gap-3"
<RadioGroupStacked
aria-label="Configuration method"
value={value}
onValueChange={(nextValue) => onChange(nextValue as DucklakeMode)}
>
{DUCKLAKE_MODE_OPTIONS.map((option) => {
const Icon = option.icon
const selected = value === option.value
return (
<button
key={option.value}
type="button"
tabIndex={0}
role="radio"
aria-checked={selected}
onClick={() => onChange(option.value)}
className={cn(
'relative flex flex-col gap-y-3 rounded-md border p-4 text-left transition',
'hover:border-control-hover',
selected ? 'border-control-hover bg-surface-300' : 'border-default bg-surface-100'
)}
>
<div className="flex items-start justify-between">
<Icon size={18} strokeWidth={1.5} className="text-foreground-light" />
{selected ? (
<Check size={16} className="text-primary" />
) : (
<span className="h-4 w-4 rounded-full border border-strong" />
)}
</div>
<div className="flex flex-col gap-y-1">
<span className="text-sm text-foreground">{option.label}</span>
<span className="text-xs text-foreground-light">{option.description}</span>
</div>
</button>
)
})}
</div>
{DUCKLAKE_MODE_OPTIONS.map((option) => (
<RadioGroupStackedItem
key={option.value}
value={option.value}
label={option.label}
description={option.description}
/>
))}
</RadioGroupStacked>
)
}
@@ -146,6 +136,11 @@ const DuckLakeSupabaseFields = ({ form }: { form: UseFormReturn<DestinationPanel
() => new Map(projects.map((project) => [project.ref, project])),
[projects]
)
const storageProjectName =
projectsByRef.get(ducklakeStorageProjectRef ?? '')?.name ??
(ducklakeStorageProjectRef === sourceProject?.ref
? sourceProject?.name
: ducklakeStorageProjectRef)
const regionForRef = (ref?: string) => {
if (!ref) return undefined
@@ -194,7 +189,7 @@ const DuckLakeSupabaseFields = ({ form }: { form: UseFormReturn<DestinationPanel
<div className="flex flex-col gap-y-1">
<p className="text-sm font-medium text-foreground">Catalog</p>
<p className="text-sm text-foreground-light">
The selected project's Postgres database is used as the DuckLake catalog.
DuckLake metadata is stored in the selected project’s Postgres database.
</p>
</div>
@@ -223,31 +218,6 @@ const DuckLakeSupabaseFields = ({ form }: { form: UseFormReturn<DestinationPanel
)}
/>
<FormField
control={form.control}
name="ducklakePoolSize"
render={({ field }) => (
<FormItemLayout
layout="horizontal"
label="Pool size"
description="Number of concurrent DuckDB connections to the catalog."
>
<FormControl>
<Input
type="number"
min={1}
max={6}
value={field.value ?? ''}
placeholder={`Default: ${DEFAULT_DUCKLAKE_POOL_SIZE}`}
onChange={(event) =>
field.onChange(event.target.value === '' ? undefined : Number(event.target.value))
}
/>
</FormControl>
</FormItemLayout>
)}
/>
<FormField
control={form.control}
name="ducklakeMetadataSchema"
@@ -255,7 +225,7 @@ const DuckLakeSupabaseFields = ({ form }: { form: UseFormReturn<DestinationPanel
<FormItemLayout
layout="horizontal"
label="Metadata schema"
description="Schema used for DuckLake metadata tables in the catalog's Postgres."
description="New schema where DuckLake metadata will be stored."
>
<FormControl>
<Input {...field} placeholder="ducklake" value={field.value ?? ''} />
@@ -309,29 +279,25 @@ const DuckLakeSupabaseFields = ({ form }: { form: UseFormReturn<DestinationPanel
label={DUCKLAKE_BUCKET_FIELD_COPY.label}
description={DUCKLAKE_BUCKET_FIELD_COPY.description}
>
<div className="flex items-center gap-x-2">
<div className="grow">
<FormControl>
<BucketSelection form={form} value={field.value} onChange={field.onChange} />
</FormControl>
</div>
<Button
type="button"
icon={<Plus />}
disabled={!ducklakeStorageProjectRef}
onClick={() => setShowNewBucketDialog(true)}
>
New bucket
</Button>
</div>
<FormControl>
<BucketSelection
form={form}
value={field.value}
onChange={field.onChange}
onCreateBucket={() => setShowNewBucketDialog(true)}
/>
</FormControl>
</FormItemLayout>
)}
/>
<Dialog open={showNewBucketDialog} onOpenChange={setShowNewBucketDialog}>
<DialogContent>
<DialogContent size="small">
<DialogHeader>
<DialogTitle>Create a new file bucket</DialogTitle>
<DialogTitle>New bucket</DialogTitle>
<DialogDescription>
Creates a private bucket in the selected Storage project ({storageProjectName}).
</DialogDescription>
</DialogHeader>
<DialogSectionSeparator />
<DialogSection className="flex flex-col gap-y-2">
@@ -409,7 +375,7 @@ const DuckLakeCustomFields = ({
placeholder={
editMode
? STORED_SECRET_PLACEHOLDER
: 'postgres://user:pass@host:5432/ducklake_catalog'
: 'postgresql://user:password@host:5432/database'
}
onChange={(event) => field.onChange(event.target.value)}
actions={
@@ -445,39 +411,12 @@ const DuckLakeCustomFields = ({
</FormItemLayout>
)}
/>
<FormField
control={form.control}
name="ducklakePoolSize"
render={({ field }) => (
<FormItemLayout
layout="horizontal"
label="Pool size"
description="Number of concurrent DuckDB connections to use."
>
<FormControl>
<Input
type="number"
min={1}
max={6}
value={field.value ?? ''}
placeholder={`Default: ${DEFAULT_DUCKLAKE_POOL_SIZE}`}
onChange={(event) =>
field.onChange(
event.target.value === '' ? undefined : Number(event.target.value)
)
}
/>
</FormControl>
</FormItemLayout>
)}
/>
</div>
<div className="flex flex-col gap-y-1">
<p className="text-sm font-medium text-foreground">Object storage</p>
<p className="text-sm text-foreground-light">
Optional credentials and endpoint settings for S3-compatible storage providers.
Connection settings and credentials for your S3-compatible object storage.
</p>
</div>
@@ -498,8 +437,13 @@ const DuckLakeCustomFields = ({
<FormControl>
<Input
{...field}
placeholder={editMode ? STORED_SECRET_PLACEHOLDER : 'my-access-key'}
placeholder={editMode ? STORED_SECRET_PLACEHOLDER : undefined}
value={field.value ?? ''}
autoComplete="off"
data-1p-ignore
data-lpignore="true"
data-form-type="other"
data-bwignore
/>
</FormControl>
</FormItemLayout>
@@ -518,26 +462,50 @@ const DuckLakeCustomFields = ({
? 'Stored secret access key is hidden. Enter a new secret to replace it.'
: 'Required secret access key for the object storage provider.'
}
className="relative"
>
<FormControl>
<PasswordInput
{...field}
type={showSecretAccessKey && !editMode ? 'text' : 'password'}
placeholder={editMode ? STORED_SECRET_PLACEHOLDER : undefined}
value={field.value ?? ''}
autoComplete="off"
actions={
!editMode && (
<Button
className="w-7"
aria-label={
showSecretAccessKey
? 'Hide S3 secret access key'
: 'Show S3 secret access key'
}
icon={showSecretAccessKey ? <Eye /> : <EyeOff />}
onClick={() => setShowSecretAccessKey(!showSecretAccessKey)}
/>
)
}
/>
</FormControl>
</FormItemLayout>
)}
/>
<FormField
control={form.control}
name="ducklakeS3Endpoint"
render={({ field }) => (
<FormItemLayout
layout="horizontal"
label="S3 endpoint"
description="Public address of your storage provider, without the HTTP or HTTPS prefix."
>
<FormControl>
<Input
{...field}
type={showSecretAccessKey && !editMode ? 'text' : 'password'}
placeholder={editMode ? STORED_SECRET_PLACEHOLDER : 'my-secret-key'}
placeholder="s3.us-east-1.amazonaws.com"
value={field.value ?? ''}
/>
</FormControl>
{!editMode && (
<Button
aria-label={
showSecretAccessKey ? 'Hide secret access key' : 'Show secret access key'
}
icon={showSecretAccessKey ? <Eye /> : <EyeOff />}
className="w-7 absolute right-6 top-[4px]"
onClick={() => setShowSecretAccessKey(!showSecretAccessKey)}
/>
)}
</FormItemLayout>
)}
/>
@@ -558,22 +526,6 @@ const DuckLakeCustomFields = ({
)}
/>
<FormField
control={form.control}
name="ducklakeS3Endpoint"
render={({ field }) => (
<FormItemLayout
layout="horizontal"
label="S3 endpoint"
description="Required endpoint without the protocol scheme, for example `127.0.0.1:5000/s3`."
>
<FormControl>
<Input {...field} placeholder="127.0.0.1:5000/s3" value={field.value ?? ''} />
</FormControl>
</FormItemLayout>
)}
/>
<FormField
control={form.control}
name="ducklakeS3UrlStyle"
@@ -581,14 +533,26 @@ const DuckLakeCustomFields = ({
<FormItemLayout
layout="horizontal"
label="S3 URL style"
description="Choose `path` for MinIO/Supabase-style endpoints or `vhost` for AWS-style virtual host addressing."
description="Controls where the bucket name appears in requests to your storage provider."
>
<FormControl>
<Select value={field.value ?? 'path'} onValueChange={field.onChange}>
<SelectTrigger>{field.value ?? 'path'}</SelectTrigger>
<SelectTrigger>
{field.value === 'vhost' ? 'Virtual-host style' : 'Path style'}
</SelectTrigger>
<SelectContent>
<SelectItem value="path">path</SelectItem>
<SelectItem value="vhost">vhost</SelectItem>
<SelectItem value="path" className="[&>span]:top-2.5">
<p>Path style</p>
<p className="text-foreground-lighter">
Bucket name appears in the URL path.
</p>
</SelectItem>
<SelectItem value="vhost" className="[&>span]:top-2.5">
<p>Virtual-host style</p>
<p className="text-foreground-lighter">
Bucket name appears in the hostname.
</p>
</SelectItem>
</SelectContent>
</Select>
</FormControl>
@@ -603,17 +567,25 @@ const DuckLakeCustomFields = ({
<FormItemLayout
layout="horizontal"
label="Use SSL"
description="Whether to use SSL when connecting to the S3-compatible endpoint."
description="Controls whether connections to your storage provider use HTTPS."
>
<FormControl>
<Select
value={field.value === false ? 'false' : 'true'}
onValueChange={(value) => field.onChange(value === 'true')}
>
<SelectTrigger>{field.value === false ? 'false' : 'true'}</SelectTrigger>
<SelectTrigger>{field.value === false ? 'Off' : 'On'}</SelectTrigger>
<SelectContent>
<SelectItem value="true">true</SelectItem>
<SelectItem value="false">false</SelectItem>
<SelectItem value="true" className="[&>span]:top-2.5">
<p>On</p>
<p className="text-foreground-lighter">Encrypts the connection with HTTPS.</p>
</SelectItem>
<SelectItem value="false" className="[&>span]:top-2.5">
<p>Off</p>
<p className="text-foreground-lighter">
Uses HTTP if your provider requires it.
</p>
</SelectItem>
</SelectContent>
</Select>
</FormControl>
@@ -637,7 +609,11 @@ const DuckLakeCustomFields = ({
<FormItemLayout
layout="horizontal"
label="Metadata schema"
description="Schema used for DuckLake metadata tables in Postgres."
description={
editMode
? 'Schema containing this destination’s DuckLake metadata tables.'
: 'New schema where DuckLake metadata will be stored.'
}
>
<FormControl>
<Input {...field} placeholder="ducklake" value={field.value ?? ''} />
@@ -666,20 +642,19 @@ export const DuckLakeFields = ({
return (
<div className="flex flex-col gap-y-6 p-5">
<p className="text-sm font-medium text-foreground">DuckLake settings</p>
{!editMode && (
<div className="flex flex-col gap-y-3">
<p className="text-xs uppercase tracking-wider text-foreground-lighter">
How should this DuckLake be configured?
</p>
<DuckLakeModeSelector
value={effectiveMode}
onChange={(value) =>
form.setValue('ducklakeMode', value, { shouldValidate: true, shouldDirty: true })
}
/>
</div>
{editMode ? (
<p className="text-sm font-medium text-foreground">DuckLake settings</p>
) : (
<FormItemLayout layout="horizontal" label="Configuration method">
<FormControl>
<DuckLakeModeSelector
value={effectiveMode}
onChange={(value) =>
form.setValue('ducklakeMode', value, { shouldValidate: true, shouldDirty: true })
}
/>
</FormControl>
</FormItemLayout>
)}
{effectiveMode === DUCKLAKE_MODE_SUPABASE ? (
@@ -776,11 +751,15 @@ const BucketSelection = ({
form,
value,
onChange,
onCreateBucket,
}: {
form: UseFormReturn<DestinationPanelSchemaType>
value: string | undefined
onChange: (value: string) => void
onCreateBucket: () => void
}) => {
const [searchTerm, setSearchTerm] = useState('')
const [isDropdownOpen, setIsDropdownOpen] = useState(false)
const ducklakeStorageProjectRef = useWatch({
control: form.control,
name: 'ducklakeStorageProjectRef',
@@ -805,6 +784,10 @@ const BucketSelection = ({
[bucketsData]
)
const isBucketsErrorVisible = isMetadataListErrorVisible(isErrorBuckets, buckets.length)
const isBucketsLoading = isMetadataListLoading(
isPendingBuckets || isFetchingBuckets,
buckets.length
)
const { handleOpenChange: handleRefreshBucketsOnOpen } = useRefreshOnOpen({
isEnabled: !!ducklakeStorageProjectRef,
refetch: refetchBuckets,
@@ -812,41 +795,94 @@ const BucketSelection = ({
if (!ducklakeStorageProjectRef) {
return (
<Select disabled>
<SelectTrigger>Select a storage project first</SelectTrigger>
</Select>
<ComboboxTrigger size="small" disabled>
Select a storage project first
</ComboboxTrigger>
)
}
return (
<Select
value={value || ''}
onOpenChange={handleRefreshBucketsOnOpen}
onValueChange={(e) => {
if (e) onChange(e)
<Popover
modal={false}
open={isDropdownOpen}
onOpenChange={(open) => {
setIsDropdownOpen(open)
handleRefreshBucketsOnOpen(open)
if (!open) setSearchTerm('')
}}
>
<SelectTrigger>{value || 'Select a bucket'}</SelectTrigger>
<SelectContent>
<SelectGroup>
<SelectionListState
isLoading={isMetadataListLoading(isPendingBuckets || isFetchingBuckets, buckets.length)}
isError={isBucketsErrorVisible}
isEmpty={
!isMetadataListLoading(isPendingBuckets || isFetchingBuckets, buckets.length) &&
!isBucketsErrorVisible &&
buckets.length === 0
}
emptyLabel="No buckets available"
errorLabel="Unable to load buckets"
<PopoverTrigger asChild>
<ComboboxTrigger
size="small"
aria-expanded={isDropdownOpen}
data-state={isDropdownOpen ? 'open' : 'closed'}
className={cn(!value && 'text-foreground-muted')}
>
{value || 'Select a bucket'}
</ComboboxTrigger>
</PopoverTrigger>
<PopoverContent sameWidthAsTrigger className="p-0" align="start" side="bottom">
<Command>
<CommandInput
placeholder="Find bucket..."
className="text-xs"
value={searchTerm}
onValueChange={setSearchTerm}
/>
{buckets.map((bucket) => (
<SelectItem key={bucket.id} value={bucket.id}>
{bucket.name}
</SelectItem>
))}
</SelectGroup>
</SelectContent>
</Select>
<CommandList>
{!isBucketsLoading && !isBucketsErrorVisible && buckets.length > 0 && (
<CommandEmpty>No buckets found</CommandEmpty>
)}
<SelectionListState
isLoading={isBucketsLoading}
isError={isBucketsErrorVisible}
isEmpty={!isBucketsLoading && !isBucketsErrorVisible && buckets.length === 0}
emptyLabel="No buckets available"
errorLabel="Unable to load buckets"
skeletonVariant="command"
/>
{buckets.length > 0 && (
<CommandGroup>
<ScrollArea
className={buckets.length > 7 ? 'h-[210px]' : ''}
onWheel={(event) => event.stopPropagation()}
>
{buckets.map((bucket) => (
<CommandItem
key={bucket.id}
value={bucket.name}
className="cursor-pointer flex items-center justify-between gap-x-2 w-full"
onSelect={() => {
onChange(bucket.id)
setIsDropdownOpen(false)
}}
>
<span>{bucket.name}</span>
{value === bucket.id && (
<Check className="text-primary" strokeWidth={2} size={13} />
)}
</CommandItem>
))}
</ScrollArea>
</CommandGroup>
)}
<CommandSeparator />
<CommandGroup forceMount>
<CommandItem
forceMount
className="cursor-pointer w-full"
onSelect={() => {
setIsDropdownOpen(false)
onCreateBucket()
}}
>
<Plus size={14} strokeWidth={1.5} className="mr-2" />
New bucket
</CommandItem>
</CommandGroup>
</CommandList>
</Command>
</PopoverContent>
</Popover>
)
}
@@ -30,6 +30,7 @@ import {
useIsAwsK8sCloudProvider,
useIsHighAvailability,
useIsOrioleDb,
useOrioleDbReleaseStage,
useSelectedProjectQuery,
} from '@/hooks/misc/useSelectedProject'
import { DOCS_URL, PROJECT_STATUS } from '@/lib/constants'
@@ -41,6 +42,7 @@ export const RestoreToNewProject = () => {
const { hasAccess: hasAccessToRestoreToNewProject, isLoading: isLoadingEntitlement } =
useCheckEntitlements('backup.restore_to_new_project')
const isOrioleDb = useIsOrioleDb()
const orioleDbReleaseStage = useOrioleDbReleaseStage()
const isAwsK8s = useIsAwsK8sCloudProvider()
const isHighAvailability = useIsHighAvailability()
@@ -130,7 +132,7 @@ export const RestoreToNewProject = () => {
<Admonition
type="default"
title="Restoring to new projects are not available for OrioleDB"
description="OrioleDB is currently in public beta and restoring to a new project is unavailable on OrioleDB projects"
description={`OrioleDB is currently in public ${orioleDbReleaseStage} and restoring to a new project is unavailable on OrioleDB projects`}
/>
)
}
+3 -2
View File
@@ -3,7 +3,7 @@ import { useQuery } from '@tanstack/react-query'
import { databaseKeys } from './keys'
import type { components } from '@/data/api'
import { get, handleError } from '@/data/fetchers'
import { useIsOrioleDbInAws } from '@/hooks/misc/useSelectedProject'
import { useIsOrioleDbInAws, useOrioleDbReleaseStage } from '@/hooks/misc/useSelectedProject'
import { PROJECT_STATUS } from '@/lib/constants'
import type { ResponseError, UseCustomQueryOptions } from '@/types'
@@ -35,13 +35,14 @@ export const useBackupsQuery = <TData = BackupsData>(
) => {
// [Joshen] Check for specifically false to account for project not loaded yet
const isOrioleDbInAws = useIsOrioleDbInAws()
const orioleDbReleaseStage = useOrioleDbReleaseStage()
return useQuery<BackupsData, BackupsError, TData>({
queryKey: databaseKeys.backups(projectRef),
queryFn: ({ signal }) => getBackups({ projectRef }, signal),
enabled:
enabled &&
!isOrioleDbInAws &&
!(isOrioleDbInAws && orioleDbReleaseStage === 'alpha') &&
typeof projectRef !== 'undefined' &&
projectStatus !== PROJECT_STATUS.COMING_UP &&
projectStatus !== PROJECT_STATUS.UNKNOWN,
@@ -1,5 +1,10 @@
import { useParams } from 'common'
import {
isOrioleDbVersionAtLeast,
ORIOLEDB_PUBLIC_BETA_VERSION,
type OrioleDbReleaseStage,
} from './useSelectedProject.utils'
import { useProjectDetailQuery } from '@/data/projects/project-detail-query'
import { PROJECT_STATUS, PROVIDERS } from '@/lib/constants'
@@ -54,6 +59,16 @@ export const useIsOrioleDbInAws = () => {
return isOrioleDbInAws
}
// OrioleDB moved from Public Alpha to Public Beta at engine version 17.11.0.001.
// PITR remains unsupported for every OrioleDB version regardless of stage.
export const useOrioleDbReleaseStage = (): OrioleDbReleaseStage | undefined => {
const { data: project } = useSelectedProjectQuery()
if (!project?.dbVersion?.endsWith('orioledb')) return undefined
return isOrioleDbVersionAtLeast(project.dbVersion, ORIOLEDB_PUBLIC_BETA_VERSION)
? 'beta'
: 'alpha'
}
// [Joshen TODO] There's a duplicate method `resolveHighAvailability` in `useHighAvailability.constants`
export const useIsHighAvailability = () => {
const { data: project } = useSelectedProjectQuery()
@@ -0,0 +1,151 @@
import { describe, expect, it } from 'vitest'
import {
appVersionAtLeast,
compareAppVersions,
isOrioleDbVersionAtLeast,
parseAppVersionKey,
} from './useSelectedProject.utils'
describe('parseAppVersionKey', () => {
it('parses a dbVersion string into a version key', () => {
expect(parseAppVersionKey('supabase-postgres-17.9.9.999-orioledb')).toEqual([17, 9, 9, 999])
})
it('parses dbVersion strings with an arch prefix', () => {
expect(parseAppVersionKey('supabase-postgres-arm64-17.9.0.019')).toEqual([17, 9, 0, 19])
expect(parseAppVersionKey('supabase-postgres-x86_64-17.9.0.019')).toEqual([17, 9, 0, 19])
})
it('returns null for non-conforming strings', () => {
expect(parseAppVersionKey('CUSTOM_AMI_SENTINEL')).toBeNull()
expect(parseAppVersionKey('not-a-version')).toBeNull()
})
it('returns null for null or undefined input', () => {
expect(parseAppVersionKey(null)).toBeNull()
expect(parseAppVersionKey(undefined)).toBeNull()
})
})
describe('compareAppVersions', () => {
it('returns a negative number when a is older than b', () => {
expect(
compareAppVersions('supabase-postgres-17.8.9.999', 'supabase-postgres-17.9.0.019')
).toBeLessThan(0)
})
it('returns zero when versions are equal', () => {
expect(compareAppVersions('supabase-postgres-17.9.0.019', 'supabase-postgres-17.9.0.019')).toBe(
0
)
})
it('returns a positive number when a is newer than b', () => {
expect(
compareAppVersions('supabase-postgres-17.11.0.001', 'supabase-postgres-17.9.0.019')
).toBeGreaterThan(0)
})
it('compares numerically rather than by segment digit-width', () => {
// Regression case for the digit-concatenation bug in lib/helpers.ts'
// getSemanticVersion, where '15.1.1.2' (-> '15112') compares as less than
// '14.1.0.44' (-> '141044') despite being the newer version.
expect(
compareAppVersions('supabase-postgres-15.1.1.2', 'supabase-postgres-14.1.0.44')
).toBeGreaterThan(0)
})
it('throws on unparseable input', () => {
expect(() => compareAppVersions('not-a-version', 'supabase-postgres-17.9.0.019')).toThrow()
expect(() => compareAppVersions('supabase-postgres-17.9.0.019', 'not-a-version')).toThrow()
})
})
describe('appVersionAtLeast', () => {
it('returns true when the version is above or equal to the threshold', () => {
expect(appVersionAtLeast('supabase-postgres-17.9.9.999', 'supabase-postgres-17.9.0.019')).toBe(
true
)
expect(appVersionAtLeast('supabase-postgres-17.9.0.019', 'supabase-postgres-17.9.0.019')).toBe(
true
)
})
it('returns false when the version is below the threshold', () => {
expect(appVersionAtLeast('supabase-postgres-17.8.9.999', 'supabase-postgres-17.9.0.019')).toBe(
false
)
})
it('returns false for an unparseable version instead of throwing', () => {
expect(appVersionAtLeast('not-a-version', 'supabase-postgres-17.9.0.019')).toBe(false)
})
})
describe('isOrioleDbVersionAtLeast', () => {
it('returns true when the engine version is above the threshold', () => {
expect(
isOrioleDbVersionAtLeast(
'supabase-postgres-17.11.0.002-orioledb',
'supabase-postgres-17.11.0.001-orioledb'
)
).toBe(true)
expect(
isOrioleDbVersionAtLeast(
'supabase-postgres-17.12.0.001-orioledb',
'supabase-postgres-17.11.0.001-orioledb'
)
).toBe(true)
})
it('returns true when the engine version equals the threshold', () => {
expect(
isOrioleDbVersionAtLeast(
'supabase-postgres-17.11.0.001-orioledb',
'supabase-postgres-17.11.0.001-orioledb'
)
).toBe(true)
})
it('returns false when the engine version is below the threshold', () => {
expect(
isOrioleDbVersionAtLeast(
'supabase-postgres-17.10.9.999-orioledb',
'supabase-postgres-17.11.0.001-orioledb'
)
).toBe(false)
// wal-g (scheduled backup) support landed at 17.9.0.019, which is still
// below the Public Beta cutoff — these projects remain alpha.
expect(
isOrioleDbVersionAtLeast(
'supabase-postgres-17.9.0.019-orioledb',
'supabase-postgres-17.11.0.001-orioledb'
)
).toBe(false)
})
it('returns false for a non-OrioleDB dbVersion', () => {
expect(
isOrioleDbVersionAtLeast(
'supabase-postgres-17.11.0.001',
'supabase-postgres-17.11.0.001-orioledb'
)
).toBe(false)
})
it('returns false for undefined dbVersion', () => {
expect(isOrioleDbVersionAtLeast(undefined, 'supabase-postgres-17.11.0.001-orioledb')).toBe(
false
)
})
it('returns false for a non-OrioleDB threshold', () => {
expect(
isOrioleDbVersionAtLeast(
'supabase-postgres-17.11.0.001-orioledb',
'supabase-postgres-17.11.0.001'
)
).toBe(false)
})
})
@@ -0,0 +1,42 @@
export type OrioleDbReleaseStage = 'alpha' | 'beta'
// OrioleDB engine versions ship as 4-part strings (e.g. `17.11.0.001`), which
// `lib/semver` doesn't parse (it's strict 3-part semver), so compare them separately here.
export const ORIOLEDB_PUBLIC_BETA_VERSION = 'supabase-postgres-17.11.0.001-orioledb'
// Sortable [major, minor, patch, build] key parsed from a dbVersion string like
// `supabase-postgres-17.11.0.001-orioledb` or `supabase-postgres-arm64-17.11.0.001`.
const APP_VERSION_KEY_RE = /^supabase-postgres-(?:arm64-|x86_64-)?(\d+)\.(\d+)\.(\d+)\.(\d+)/
export type AppVersionKey = [number, number, number, number]
// Returns null for non-conforming strings — callers should treat null as "not orderable".
export function parseAppVersionKey(version: string | null | undefined): AppVersionKey | null {
if (!version) return null
const match = APP_VERSION_KEY_RE.exec(version)
if (!match) return null
return [Number(match[1]), Number(match[2]), Number(match[3]), Number(match[4])]
}
// Negative when `a` is older, zero when equal, positive when newer.
// Throws on unparseable input — null-tolerant callers parse first.
export function compareAppVersions(a: string, b: string): number {
const keyA = parseAppVersionKey(a)
const keyB = parseAppVersionKey(b)
if (!keyA) throw new Error(`unparseable app_version: ${a}`)
if (!keyB) throw new Error(`unparseable app_version: ${b}`)
return keyA[0] - keyB[0] || keyA[1] - keyB[1] || keyA[2] - keyB[2] || keyA[3] - keyB[3]
}
export function appVersionAtLeast(version: string, threshold: string): boolean {
if (!parseAppVersionKey(version)) return false
return compareAppVersions(version, threshold) >= 0
}
export const isOrioleDbVersionAtLeast = (dbVersion: string | undefined, threshold: string) => {
if (!dbVersion?.endsWith('orioledb') || !threshold.endsWith('orioledb')) return false
return appVersionAtLeast(dbVersion, threshold)
}
+2 -2
View File
@@ -16,7 +16,7 @@
"build:tanstack": "vite build --mode ${MODE:-production}",
"start:tanstack": "node --env-file-if-exists=.env --env-file-if-exists=.env.local --env-file-if-exists=.env.${MODE:-production} --env-file-if-exists=.env.${MODE:-production}.local .output/server/index.mjs",
"lint": "eslint .",
"lint:ratchet": "tsx scripts/ratchet-eslint-rules.ts --rules-file scripts/ratchet-rules.json",
"lint:ratchet": "tsx node_modules/eslint-config-supabase/ratchet-eslint-rules.ts --rules-file scripts/ratchet-rules.json --rules-file node_modules/eslint-config-supabase/ratchet-rules.json",
"clean": "rimraf .next .turbo tsconfig.tsbuildinfo",
"test": "vitest --run --coverage",
"test:watch": "vitest watch",
@@ -87,7 +87,7 @@
"@tanstack/react-query-devtools": "~5.90.2",
"@tanstack/react-router": "catalog:",
"@tanstack/react-router-devtools": "^1.166.13",
"@tanstack/react-router-ssr-query": "^1.166.12",
"@tanstack/react-router-ssr-query": "^1.167.3",
"@tanstack/react-start": "catalog:",
"@tanstack/react-table": "catalog:",
"@tanstack/react-virtual": "^3.13.24",
@@ -28,7 +28,11 @@ import { useBackupsQuery } from '@/data/database/backups-query'
import { useCheckEntitlements } from '@/hooks/misc/useCheckEntitlements'
import { useAsyncCheckPermissions } from '@/hooks/misc/useCheckPermissions'
import { useHighAvailability } from '@/hooks/misc/useHighAvailability'
import { useIsOrioleDbInAws, useSelectedProjectQuery } from '@/hooks/misc/useSelectedProject'
import {
useIsOrioleDbInAws,
useOrioleDbReleaseStage,
useSelectedProjectQuery,
} from '@/hooks/misc/useSelectedProject'
import { DOCS_URL, PROJECT_STATUS } from '@/lib/constants'
import type { NextPageWithLayout } from '@/types'
@@ -71,6 +75,7 @@ const PITR = () => {
const { hasAccess: hasAccessToPitr, isLoading: isLoadingEntitlements } =
useCheckEntitlements('pitr.available_variants')
const isOrioleDbInAws = useIsOrioleDbInAws()
const orioleDbReleaseStage = useOrioleDbReleaseStage()
const {
data: backups,
error,
@@ -96,8 +101,8 @@ const PITR = () => {
return (
<Admonition
type="default"
title="Database backups are not available for OrioleDB"
description="OrioleDB is currently in public beta and Point-in-Time Recovery is unavailable on OrioleDB projects"
title="Point-in-Time Recovery is not available for OrioleDB"
description={`OrioleDB is currently in public ${orioleDbReleaseStage} and Point-in-Time Recovery is unavailable on OrioleDB projects`}
>
<DocsButton abbrev={false} className="mt-2" href={DOCS_URL} />
</Admonition>
@@ -1,6 +1,7 @@
import { PermissionAction } from '@supabase/shared-types/out/constants'
import { useParams } from 'common'
import { DatabaseBackup, Info } from 'lucide-react'
import { Admonition } from 'ui-patterns/Admonition'
import { PageContainer } from 'ui-patterns/PageContainer'
import {
PageHeader,
@@ -17,12 +18,17 @@ import DatabaseBackupsNav from '@/components/interfaces/Database/Backups/Databas
import { DatabaseLayout } from '@/components/layouts/DatabaseLayout/DatabaseLayout'
import { DefaultLayout } from '@/components/layouts/DefaultLayout'
import { AlertError } from '@/components/ui/AlertError'
import { DocsButton } from '@/components/ui/DocsButton'
import { HighAvailabilityDisabledEmptyState } from '@/components/ui/HighAvailability/HighAvailabilityDisabledEmptyState'
import InformationBox from '@/components/ui/InformationBox'
import { NoPermission } from '@/components/ui/NoPermission'
import { useBackupsQuery } from '@/data/database/backups-query'
import { useAsyncCheckPermissions } from '@/hooks/misc/useCheckPermissions'
import { useIsHighAvailability } from '@/hooks/misc/useSelectedProject'
import {
useIsHighAvailability,
useIsOrioleDbInAws,
useOrioleDbReleaseStage,
} from '@/hooks/misc/useSelectedProject'
import { DOCS_URL } from '@/lib/constants'
import type { NextPageWithLayout } from '@/types'
@@ -62,6 +68,8 @@ const ScheduledBackups = () => {
} = useBackupsQuery({ projectRef })
const isHighAvailability = useIsHighAvailability()
const isOrioleDbInAws = useIsOrioleDbInAws()
const orioleDbReleaseStage = useOrioleDbReleaseStage()
const isPitrEnabled = backups?.pitr_enabled
const { can: canReadScheduledBackups, isSuccess: isPermissionsLoaded } = useAsyncCheckPermissions(
@@ -69,6 +77,18 @@ const ScheduledBackups = () => {
'back_ups'
)
if (isOrioleDbInAws && orioleDbReleaseStage === 'alpha') {
return (
<Admonition
type="default"
title="Database backups are not available for OrioleDB"
description={`OrioleDB is currently in public ${orioleDbReleaseStage} and projects created are strictly ephemeral with no database backups`}
>
<DocsButton abbrev={false} className="mt-2" href={DOCS_URL} />
</Admonition>
)
}
if (isHighAvailability) {
return (
<HighAvailabilityDisabledEmptyState
+4019 -4019
View File
File diff suppressed because it is too large. Load diff
+3 -2
View File
@@ -31,6 +31,7 @@ import {
redirect,
Scripts,
type AnyRouter,
type ErrorComponentProps,
} from '@tanstack/react-router'
import { TanStackRouterDevtoolsPanel } from '@tanstack/react-router-devtools'
import {
@@ -308,7 +309,7 @@ function NotFound() {
return <Error404 />
}
function ErrorBoundaryRoute({ error }: { error: Error }) {
function ErrorBoundaryRoute({ error }: ErrorComponentProps) {
// Mirrors `errorBoundaryHandler` above (used by the in-tree
// `react-error-boundary`) — TanStack's `errorComponent` covers
// errors thrown during route load/render before the in-tree
@@ -321,7 +322,7 @@ function ErrorBoundaryRoute({ error }: { error: Error }) {
;(error as Error & { sentryId?: string }).sentryId = eventId
}
})
console.error(error.stack)
console.error(error instanceof Error ? error.stack : error)
}, [error])
return <Error500 />
+37
View File
@@ -0,0 +1,37 @@
{
"rules": {
"shadcn/no-arbitrary-values": 36,
"shadcn/no-unknown-classes": 11,
"shadcn/no-raw-colors": 2
},
"ruleFiles": {
"shadcn/no-arbitrary-values": {
"app/example/headless-app/layout.tsx": 3,
"app/example/oauth-consent/layout.tsx": 3,
"app/example/password-based-auth/layout.tsx": 3,
"app/example/social-auth/layout.tsx": 3,
"components/catalog-preview.tsx": 5,
"components/command-menu.tsx": 2,
"components/command.tsx": 2,
"components/component-preview.tsx": 7,
"components/library-overview.tsx": 3,
"components/mdx-components.tsx": 2,
"components/tanstack-db-generator/index.tsx": 1,
"registry/default/blocks/realtime-flow/components/realtime-flow-overlay.tsx": 1,
"registry/default/platform/platform-kit-nextjs/components/supabase-manager/index.tsx": 1
},
"shadcn/no-unknown-classes": {
"app/(app)/docs/[[...slug]]/page.tsx": 2,
"components/block-overview-tabs.tsx": 2,
"components/block-preview.tsx": 1,
"components/component-preview.tsx": 2,
"components/mdx-components.tsx": 1,
"registry/default/components/ui/popover.tsx": 1,
"registry/default/components/ui/select.tsx": 1,
"registry/default/components/ui/tooltip.tsx": 1
},
"shadcn/no-raw-colors": {
"components/mdx-components.tsx": 2
}
}
}
+1
View File
@@ -18,6 +18,7 @@
"test:headless-tools": "tsx scripts/test-headless-tools.mts",
"start": "next start",
"lint": "eslint .",
"lint:ratchet": "tsx node_modules/eslint-config-supabase/ratchet-eslint-rules.ts --rules-file node_modules/eslint-config-supabase/ratchet-rules.json",
"clean": "rimraf .next .turbo tsconfig.tsbuildinfo .contentlayer .velite",
"typecheck": "run-p build:content build:markdown && next typegen && tsc --noEmit -p tsconfig.json"
},
+277
View File
@@ -0,0 +1,277 @@
{
"rules": {
"shadcn/no-arbitrary-values": 527,
"shadcn/no-unknown-classes": 112,
"shadcn/no-raw-colors": 212
},
"ruleFiles": {
"shadcn/no-arbitrary-values": {
"app/(home)/_components/BuiltWithSupabaseSection.tsx": 2,
"app/(products)/auth/_components/FeaturesSection.tsx": 40,
"app/(products)/database/_components/FeaturesSection.tsx": 15,
"app/(products)/database/_components/Illustrations/SpreadsheetIllustration.tsx": 9,
"app/(products)/database/_components/VisualDemo.tsx": 5,
"app/(products)/edge-functions/_components/Highlights.tsx": 1,
"app/(products)/realtime/_components/FeaturesSection.tsx": 5,
"app/(products)/storage/_components/FeaturesSection.tsx": 16,
"app/contribute/about/page.tsx": 1,
"app/partners/PartnersContent.tsx": 2,
"app/state-of-startups/components/StateOfStartupsAuroraHeader.tsx": 2,
"app/state-of-startups/components/StateOfStartupsHeader.tsx": 2,
"app/state-of-startups/register/RegisterContent.tsx": 12,
"components/Blog/BlogGridItem.tsx": 1,
"components/BuiltWithSupabaseSection.tsx": 2,
"components/Changelog/ChangelogInlineMarkdown.tsx": 1,
"components/Changelog/ChangelogLlmMarkdownButton.tsx": 2,
"components/Changelog/ChangelogTimelineList.tsx": 2,
"components/Contribute/Hero.tsx": 1,
"components/Contribute/Icons.tsx": 2,
"components/Contribute/UnansweredThreadsTable.tsx": 2,
"components/Contribute/UserProfile.tsx": 2,
"components/CustomerStories/CutomsersSliderDesktop.tsx": 1,
"components/Error404.tsx": 3,
"components/Events/new/EventTimeline.tsx": 1,
"components/FeaturesMatrix.tsx": 1,
"components/Footer/index.tsx": 1,
"components/Go/TweetsSection.tsx": 2,
"components/LaunchWeek/11/HackathonCallout.tsx": 1,
"components/LaunchWeek/11/LW11Day1.tsx": 1,
"components/LaunchWeek/11/LW11Meetups.tsx": 1,
"components/LaunchWeek/11/LW11Summary.tsx": 1,
"components/LaunchWeek/11/LaunchWeekLogoHeader.tsx": 4,
"components/LaunchWeek/11/LaunchWeekPrizeSection.tsx": 1,
"components/LaunchWeek/11/Releases/BuildStage.tsx": 1,
"components/LaunchWeek/11/Releases/components/AdventCard.tsx": 2,
"components/LaunchWeek/11/Releases/components/DaySection.tsx": 6,
"components/LaunchWeek/12/HackathonCallout.tsx": 1,
"components/LaunchWeek/12/LWMeetups.tsx": 1,
"components/LaunchWeek/12/LaunchWeekLogoHeader.tsx": 4,
"components/LaunchWeek/12/LaunchWeekPrizeSection.tsx": 1,
"components/LaunchWeek/12/Releases/BuildStage.tsx": 1,
"components/LaunchWeek/12/Releases/Day1.tsx": 1,
"components/LaunchWeek/12/Releases/LWHeader.tsx": 1,
"components/LaunchWeek/12/Releases/components/BuildCard.tsx": 2,
"components/LaunchWeek/12/Releases/components/DaySection.tsx": 2,
"components/LaunchWeek/13/Releases/Day1.tsx": 1,
"components/LaunchWeek/13/Releases/components/BuildCard.tsx": 2,
"components/LaunchWeek/13/Releases/components/DaySection.tsx": 2,
"components/LaunchWeek/14/ActionButton.tsx": 6,
"components/LaunchWeek/14/Header.tsx": 4,
"components/LaunchWeek/14/Releases/components/BuildCard.tsx": 2,
"components/LaunchWeek/14/Releases/components/DaySection.tsx": 2,
"components/LaunchWeek/15/ActionButton.tsx": 6,
"components/LaunchWeek/15/LW15MainStage.tsx": 3,
"components/LaunchWeek/7/BlogLinks.tsx": 2,
"components/LaunchWeek/7/LabelBadge.tsx": 3,
"components/LaunchWeek/7/LaunchSection/LaunchWeekLogoHeader.tsx": 3,
"components/LaunchWeek/7/LaunchWeekPrizeCard.tsx": 1,
"components/LaunchWeek/7/LaunchWeekPrizeSection.tsx": 3,
"components/LaunchWeek/7/Releases/components/index.tsx": 22,
"components/LaunchWeek/7/Releases/index.tsx": 6,
"components/LaunchWeek/8/LWArchive.tsx": 3,
"components/LaunchWeek/8/LaunchWeekLogoHeader.tsx": 4,
"components/LaunchWeek/8/LaunchWeekPrizeCard.tsx": 1,
"components/LaunchWeek/8/Releases/LWAnnouncement.tsx": 1,
"components/LaunchWeek/8/Releases/components/index.tsx": 20,
"components/LaunchWeek/8/Releases/index.tsx": 17,
"components/LaunchWeek/X/Album/Player.tsx": 2,
"components/LaunchWeek/X/LWXMeetups.tsx": 7,
"components/LaunchWeek/X/LWXSummary.tsx": 1,
"components/LaunchWeek/X/LaunchWeekLogoHeader.tsx": 4,
"components/LaunchWeek/X/LaunchWeekPrizeCard.tsx": 2,
"components/LaunchWeek/X/LaunchWeekPrizeSection.tsx": 5,
"components/LaunchWeek/X/Releases/BuildStage.tsx": 1,
"components/LaunchWeek/X/Releases/LWXStickyNav.tsx": 3,
"components/LaunchWeek/X/Releases/MainStage.tsx": 4,
"components/LaunchWeek/X/Releases/components/AdventCard.tsx": 7,
"components/LaunchWeek/X/Releases/components/DaySection.tsx": 9,
"components/Nav/ProductDropdown.tsx": 1,
"components/Nav/index.tsx": 3,
"components/OpenSource/OSSHero.tsx": 1,
"components/OpenSource/Sponsorships.tsx": 2,
"components/Panel/Panel.tsx": 1,
"components/Partners/BackgroundPattern.tsx": 1,
"components/Pricing/ComputePricingCalculator.tsx": 1,
"components/Pricing/PricingAddons.tsx": 4,
"components/Pricing/PricingComparisonTable.tsx": 1,
"components/Pricing/PricingComputeAnimation.tsx": 3,
"components/Pricing/PricingComputeSection.tsx": 4,
"components/Pricing/PricingPlans.tsx": 15,
"components/Products/DataAPIsVisual.tsx": 1,
"components/Products/Functions/CI.tsx": 1,
"components/Products/Functions/RealtimeLogs.tsx": 2,
"components/Products/RealtimeVisual.tsx": 10,
"components/Products/index.tsx": 1,
"components/Regions/RegionTooltip.tsx": 2,
"components/Sections/FeaturesSection.tsx": 1,
"components/Sections/ParagraphSection.tsx": 1,
"components/Sections/ProductHeader2.tsx": 1,
"components/Sections/ProductModulesHeader.tsx": 1,
"components/Sections/TimedTabsSection.tsx": 2,
"components/Sections/TwitterSocialProof.tsx": 2,
"components/Solutions/PostGrid.tsx": 1,
"components/SurveyResults/StateOfStartupsHeader.tsx": 2,
"components/UsersGrowthChart.tsx": 2,
"components/Wrapped/Pages/CustomerStories.tsx": 1,
"components/Wrapped/Pages/Devs.tsx": 1,
"components/Wrapped/Pages/Home.tsx": 1,
"components/Wrapped/Pages/ProductAnnouncements.tsx": 1,
"components/Wrapped/Pages/SupabaseSelect.tsx": 1,
"components/Wrapped/Pages/YearOfAI.tsx": 1,
"components/logos.tsx": 2,
"data/aws-reinvent-2025.tsx": 5,
"data/home/content.tsx": 4,
"data/products/functions/page.tsx": 1,
"data/solutions/agencies.tsx": 9,
"data/solutions/beginners.tsx": 6,
"data/solutions/developers.tsx": 9,
"data/solutions/firebase.tsx": 9,
"data/solutions/hackathon.tsx": 9,
"data/solutions/innovation-teams.tsx": 6,
"data/solutions/neon.tsx": 9,
"data/solutions/no-code.tsx": 5,
"data/solutions/postgres-developers.tsx": 9,
"data/solutions/startups.tsx": 9,
"data/solutions/vibe-coders.tsx": 5,
"pages/careers.tsx": 12,
"pages/changelog.tsx": 1,
"pages/contact-us.tsx": 3,
"pages/features.tsx": 5,
"pages/ga.tsx": 4,
"pages/launch-week/7/index.tsx": 2,
"pages/launch-week/8/index.tsx": 1,
"pages/support.tsx": 2
},
"shadcn/no-unknown-classes": {
"app/partners/PartnersContent.tsx": 1,
"app/state-of-startups/components/ParticipantsCarousel.tsx": 1,
"components/AIDemo/DotGrid.tsx": 1,
"components/AIDemo/Panel.tsx": 1,
"components/Announcement/Badge.tsx": 6,
"components/Avatar.tsx": 1,
"components/Blog/BlogCollapsible.tsx": 2,
"components/Card.tsx": 1,
"components/Changelog/ChangelogTimelineList.tsx": 1,
"components/CodeBlock/CodeBlock.tsx": 1,
"components/CustomerStories/CutomsersSliderDesktop.tsx": 1,
"components/Enterprise/CTAForm.tsx": 1,
"components/Go/TweetsSection.tsx": 2,
"components/LaunchWeek/11/LW11Day1.tsx": 2,
"components/LaunchWeek/11/Releases/BuildStage.tsx": 1,
"components/LaunchWeek/11/Releases/LW11StickyNav.tsx": 1,
"components/LaunchWeek/11/Releases/components/AdventCard.tsx": 1,
"components/LaunchWeek/11/Releases/components/DaySection.tsx": 2,
"components/LaunchWeek/12/Releases/BuildStage.tsx": 1,
"components/LaunchWeek/12/Releases/Day1.tsx": 2,
"components/LaunchWeek/12/Releases/LWStickyNav.tsx": 1,
"components/LaunchWeek/12/Releases/components/BuildCard.tsx": 1,
"components/LaunchWeek/12/Releases/components/DaySection.tsx": 2,
"components/LaunchWeek/13/Releases/BuildStage.tsx": 1,
"components/LaunchWeek/13/Releases/Day1.tsx": 2,
"components/LaunchWeek/13/Releases/LWStickyNav.tsx": 2,
"components/LaunchWeek/13/Releases/components/BuildCard.tsx": 1,
"components/LaunchWeek/13/Releases/components/DaySection.tsx": 2,
"components/LaunchWeek/14/Releases/BuildStage.tsx": 1,
"components/LaunchWeek/14/Releases/LWStickyNav.tsx": 2,
"components/LaunchWeek/14/Releases/components/BuildCard.tsx": 1,
"components/LaunchWeek/14/Releases/components/DaySection.tsx": 2,
"components/LaunchWeek/15/LW15BuildStage.tsx": 1,
"components/LaunchWeek/15/LW15Hackathon.tsx": 1,
"components/LaunchWeek/15/LW15Heading.tsx": 6,
"components/LaunchWeek/7/LaunchSection/LaunchWeekLogoHeader.tsx": 1,
"components/LaunchWeek/7/Releases/Day5.tsx": 2,
"components/LaunchWeek/7/Releases/index.tsx": 1,
"components/LaunchWeek/X/Album/Player.tsx": 1,
"components/LaunchWeek/X/Releases/BuildStage.tsx": 1,
"components/LaunchWeek/X/Releases/LWXStickyNav.tsx": 1,
"components/LaunchWeek/X/Releases/components/DaySection.tsx": 2,
"components/Nav/MobileMenu.tsx": 1,
"components/OpenSource/RepoCard.tsx": 1,
"components/OpenSource/Repos.tsx": 2,
"components/OpenSource/Sponsorships.tsx": 1,
"components/Pricing/CostControlAnimation.tsx": 2,
"components/Pricing/PricingComparisonTable.tsx": 1,
"components/Pricing/PricingTableRow.tsx": 4,
"components/Products/Functions/RealtimeLogs.tsx": 2,
"components/Products/RealtimeVisual.tsx": 1,
"components/Products/StorageVisual.tsx": 1,
"components/Sections/APISection.tsx": 1,
"components/Sections/FunctionsUsecases.tsx": 1,
"components/Sections/HighlightCards.tsx": 1,
"components/Sections/ImageParagraphSection.tsx": 2,
"components/Sections/ProductHeader2.tsx": 2,
"components/Sections/TwitterSocialProof.tsx": 1,
"components/Solutions/CTAForm.tsx": 1,
"components/Solutions/FeaturesGrid.tsx": 2,
"components/Solutions/TwoColumnsSection.tsx": 2,
"components/Tabs/Tabs.tsx": 2,
"components/TabsWithHighlights.tsx": 2,
"components/VideoWithHighlights.tsx": 2,
"components/logos.tsx": 2,
"pages/beta.tsx": 2,
"pages/careers.tsx": 1,
"pages/changelog/[slug].tsx": 1,
"pages/contact/sales.tsx": 1,
"pages/customers/[slug].tsx": 3,
"pages/support.tsx": 2
},
"shadcn/no-raw-colors": {
"app/(home)/_components/logos/FrameworkLogos.tsx": 2,
"app/(home)/_components/logos/PublicityLogos.tsx": 12,
"app/(products)/auth/_components/FeaturesSection.tsx": 9,
"app/(products)/database/_components/FeaturesSection.tsx": 4,
"app/(products)/realtime/_components/FeaturesSection.tsx": 4,
"app/state-of-startups/StateOfStartups2026Content.tsx": 2,
"components/AIDemo/Panel.tsx": 1,
"components/CaseStudies/benchmark.tsx": 1,
"components/Charts/DeveloperGrowthChart.tsx": 2,
"components/ExampleCard.tsx": 3,
"components/LaunchWeek/14/ActionButton.tsx": 12,
"components/LaunchWeek/14/Header.tsx": 6,
"components/LaunchWeek/15/ActionButton.tsx": 12,
"components/LaunchWeek/7/LaunchWeekPrizeSection.tsx": 2,
"components/LaunchWeek/7/Releases/components/index.tsx": 8,
"components/LaunchWeek/7/Releases/index.tsx": 3,
"components/LaunchWeek/8/LWArchive.tsx": 1,
"components/LaunchWeek/8/Releases/components/index.tsx": 11,
"components/LaunchWeek/X/Album/Player.tsx": 1,
"components/LaunchWeek/X/Releases/BuildStage.tsx": 1,
"components/LaunchWeek/X/Releases/data/lwx_advent_days.tsx": 20,
"components/Nav/ProductDropdown.tsx": 1,
"components/OpenSource/RepoCard.tsx": 2,
"components/Panel/Panel.tsx": 1,
"components/Partners/BackgroundPattern.tsx": 1,
"components/Pricing/PricingIcons.tsx": 1,
"components/Pricing/PricingTableRow.tsx": 1,
"components/Products/DatabaseVisual.tsx": 3,
"components/Products/Functions/Metrics.tsx": 2,
"components/Products/VectorAI/OpenAIImage.tsx": 4,
"components/Products/VectorVisual.tsx": 2,
"components/Realtime/sandpack.tsx": 1,
"components/Sections/CustomerQuotesSection.tsx": 1,
"components/SurveyResults/SurveyChart.tsx": 1,
"components/code-display.tsx": 5,
"data/BetaPage.js": 6,
"data/Solutions.tsx": 9,
"data/products/database/pipelines-visual.tsx": 19,
"data/solutions/agencies.tsx": 1,
"data/solutions/ai-builders.tsx": 14,
"data/solutions/beginners.tsx": 1,
"data/solutions/developers.tsx": 1,
"data/solutions/firebase.tsx": 1,
"data/solutions/hackathon.tsx": 1,
"data/solutions/neon.tsx": 1,
"data/solutions/no-code.tsx": 1,
"data/solutions/postgres-developers.tsx": 1,
"data/solutions/startups.tsx": 1,
"data/solutions/vibe-coders.tsx": 1,
"pages/assistant.tsx": 2,
"pages/beta.tsx": 2,
"pages/brand-assets.tsx": 2,
"pages/modules/cron.tsx": 1,
"pages/modules/queues.tsx": 1,
"pages/modules/vector.tsx": 1,
"pages/state-of-startups-2025.tsx": 2
}
}
}
+4
View File
@@ -36,6 +36,8 @@ import amoe from './legal/amoe'
import amoeThankYou from './legal/amoe-thankyou'
import contestRules from './legal/contest-rules'
import byocEarlyAccess from './pre-release/byoc-early-access'
import multigresEarlyAccess from './pre-release/multigres-early-access'
import multigresEarlyAccessThankYou from './pre-release/multigres-early-access-thank-you'
import supabasePipelinesNewDestinations from './pre-release/supabase-pipelines-new-destinations'
import supabasePipelinesNewDestinationsThankYou from './pre-release/supabase-pipelines-new-destinations-thank-you'
import supabaseStripeProjects from './stripe-projects/supabase-stripe-projects'
@@ -47,6 +49,8 @@ const pages: GoPageInput[] = [
byocEarlyAccess, // maintain until PM says to remove
supabasePipelinesNewDestinations, // maintain while Pipelines destination early access is active
supabasePipelinesNewDestinationsThankYou, // maintain while Pipelines destination early access is active
multigresEarlyAccess, // maintain while the Multigres private alpha is active
multigresEarlyAccessThankYou, // maintain while the Multigres private alpha is active
amoe, // maintain forever
amoeThankYou, // maintain forever
contestRules, // maintain forever
@@ -0,0 +1,31 @@
import type { GoPageInput } from 'marketing'
const page: GoPageInput = {
template: 'thank-you',
slug: 'multigres-early-access/thank-you',
metadata: {
title: 'Multigres private alpha request received',
description: 'Thanks for requesting access to the Multigres private alpha.',
ogImage: '/images/blog/multigres/multigres.png',
},
hero: {
title: 'Thanks for your request',
subtitle: 'Private alpha',
description:
"We got your request for the Multigres private alpha. If your organization is selected, we'll reach out with next steps.",
ctas: [
{
label: 'Read the announcement',
href: '/blog/select-2026-scale-without-limits',
variant: 'secondary',
},
{
label: 'Back to Supabase',
href: '/',
variant: 'secondary',
},
],
},
}
export default page
@@ -0,0 +1,101 @@
import type { GoPageInput } from 'marketing'
const page: GoPageInput = {
template: 'lead-gen',
slug: 'multigres-early-access',
metadata: {
title: 'Multigres private alpha - Request access',
description:
'Request access to the Multigres private alpha on Supabase. Get multi-node high availability for Postgres, built by the team behind Vitess.',
ogImage: '/images/blog/multigres/multigres.png',
},
hero: {
title: 'Multigres on Supabase',
subtitle: 'Private alpha',
description:
'Get multi-node high availability for Postgres, built by the team behind Vitess. The private alpha is by invite.',
image: {
src: '/images/blog/multigres/multigres.png',
alt: 'Multigres',
width: 1200,
height: 800,
},
ctas: [
{
label: 'Request access',
href: '#form',
variant: 'primary',
},
{
label: 'Read the announcement',
href: '/blog/select-2026-scale-without-limits',
variant: 'secondary',
},
],
},
sections: [
{
type: 'feature-grid',
className: 'border-y border-muted bg-surface-75 py-16 sm:py-24',
title: 'Multi-node high availability for Postgres',
description: 'It works with Supabase Auth, Storage, and Edge Functions.',
columns: 3,
items: [
{
title: 'Three nodes across zones',
description:
'Each Multigres cluster runs three nodes across availability zones in one region.',
},
{
title: 'Failover in seconds',
description:
'When a node fails, Multigres promotes a replica in seconds. Committed writes survive.',
},
{
title: 'No code changes',
description: "You don't need to change your application code to move to Multigres.",
},
],
},
{
type: 'form',
id: 'form',
className: 'pt-4 pb-16 sm:pt-8 sm:pb-24',
title: 'Request access to the Multigres private alpha',
description:
"This form is for the Supabase-managed Multigres private alpha, not the open-source Multigres project. The alpha is for testing, not production use, and it comes with no guarantees on data, availability, or support. During the alpha, Realtime, point-in-time recovery, and logical replication aren't available, and our team adds read replicas and gateways for you. If your organization is selected, we'll reach out with next steps.",
fields: [
{
type: 'email',
name: 'email',
label: 'Email address',
placeholder: 'Work email',
required: true,
},
{
type: 'text',
name: 'org_slug',
label: 'Supabase organization slug',
placeholder: 'acme-inc',
description: 'Use the slug from your Supabase dashboard URL, for example /org/acme-inc.',
required: true,
},
],
submitLabel: 'Request access',
successRedirect: '/go/multigres-early-access/thank-you',
disclaimer:
'By submitting this form, I confirm that I have read and understood the [Privacy Policy](https://supabase.com/privacy).',
crm: {
notion: {
database_id: 'e345167c699f459bad511549f6cb993a',
columnMap: {
email: 'Email',
org_slug: 'Organization Slug',
},
},
},
},
],
}
export default page
@@ -0,0 +1,24 @@
import SectionContainerWithCn from '~/components/Layouts/SectionContainerWithCn'
import Link from 'next/link'
import { WaitlistForm } from './WaitlistForm'
export function CTASection() {
return (
<SectionContainerWithCn height="none">
<div className="flex flex-col items-center text-center gap-10 py-24 md:py-32">
<div className="flex flex-col items-center gap-4">
<h2 className="text-2xl md:text-4xl text-foreground">
Run your next workload
<span className="block text-foreground-lighter">next to your data</span>
</h2>
<p className="text-foreground-lighter text-sm max-w-md">
Compute is in Private Alpha. Join the waitlist and we&apos;ll reach out as invites roll
out.
</p>
</div>
<WaitlistForm />
</div>
</SectionContainerWithCn>
)
}
@@ -0,0 +1,37 @@
import { ComputeVisual } from './ComputeVisual'
import { CTASection } from './CTASection'
import { DeploySection } from './DeploySection'
import { FAQSection } from './FAQSection'
import { Hero } from './Hero'
import { Highlights } from './Highlights'
import { TrustBoundarySection } from './TrustBoundarySection'
import { WorkloadsSection } from './WorkloadsSection'
export function ComputeContent() {
return (
<div className="overflow-x-clip">
<section className="border-t border-border" aria-label="Hero">
<Hero />
</section>
<section id="workloads" className="border-t border-border" aria-label="Workload shapes">
<WorkloadsSection />
</section>
<section id="highlights" className="space-y-8 md:space-y-16" aria-label="Compute highlights">
<ComputeVisual />
<Highlights />
</section>
<section id="security" className="border-t border-border" aria-label="Security model">
<TrustBoundarySection />
</section>
<section id="deploy" className="border-t border-border" aria-label="Deployment options">
<DeploySection />
</section>
<section id="faq" className="border-t border-border" aria-label="Frequently asked questions">
<FAQSection />
</section>
<section id="waitlist" className="border-t border-border" aria-label="Join the waitlist">
<CTASection />
</section>
</div>
)
}
@@ -0,0 +1,42 @@
interface ComputeLogoProps {
size?: number
className?: string
}
export function ComputeLogo({ size = 18, className }: ComputeLogoProps) {
return (
<svg
width={size}
height={size}
viewBox="0 0 18 18"
fill="none"
xmlns="http://www.w3.org/2000/svg"
className={className}
>
<path
d="M15.75 12V5.99999C15.7497 5.73694 15.6803 5.4786 15.5487 5.25086C15.417 5.02312 15.2278 4.83401 15 4.70249L9.75 1.70249C9.52197 1.57084 9.2633 1.50153 9 1.50153C8.7367 1.50153 8.47803 1.57084 8.25 1.70249L3 4.70249C2.7722 4.83401 2.58299 5.02312 2.45135 5.25086C2.31971 5.4786 2.25027 5.73694 2.25 5.99999V12C2.25027 12.263 2.31971 12.5214 2.45135 12.7491C2.58299 12.9769 2.7722 13.166 3 13.2975L8.25 16.2975C8.47803 16.4291 8.7367 16.4985 9 16.4985C9.2633 16.4985 9.52197 16.4291 9.75 16.2975L15 13.2975C15.2278 13.166 15.417 12.9769 15.5487 12.7491C15.6803 12.5214 15.7497 12.263 15.75 12Z"
stroke="currentColor"
strokeLinecap="round"
strokeLinejoin="round"
/>
<path
d="M13 10.6011V7.4004C12.9998 7.26009 12.9587 7.12227 12.8807 7.00079C12.8027 6.8793 12.6906 6.77842 12.5556 6.70826L9.44444 5.10793C9.30932 5.0377 9.15603 5.00073 9 5.00073C8.84397 5.00073 8.69068 5.0377 8.55556 5.10793L5.44444 6.70826C5.30945 6.77842 5.19732 6.8793 5.11932 7.00079C5.04131 7.12227 5.00016 7.26009 5 7.4004V10.6011C5.00016 10.7414 5.04131 10.8792 5.11932 11.0007C5.19732 11.1222 5.30945 11.223 5.44444 11.2932L8.55556 12.8935C8.69068 12.9638 8.84397 13.0007 9 13.0007C9.15603 13.0007 9.30932 12.9638 9.44444 12.8935L12.5556 11.2932C12.6906 11.223 12.8027 11.1222 12.8807 11.0007C12.9587 10.8792 12.9998 10.7414 13 10.6011Z"
stroke="currentColor"
strokeLinecap="square"
strokeLinejoin="round"
/>
<path
d="M6 7.00073L9 9.00073L12 7.00073"
stroke="currentColor"
strokeLinecap="square"
strokeLinejoin="round"
/>
<path
d="M9 12.5V9.00073"
stroke="currentColor"
strokeLinecap="square"
strokeLinejoin="round"
/>
</svg>
)
}
@@ -0,0 +1,716 @@
'use client'
import { AnimatePresence, motion, useReducedMotion } from 'framer-motion'
import { Database } from 'icons'
import type { LucideIcon } from 'lucide-react'
import { Bot, User } from 'lucide-react'
import { useCallback, useEffect, useId, useRef, useState } from 'react'
import { cn } from 'ui'
import SectionContainerWithCn from '../../../../components/Layouts/SectionContainerWithCn'
import styles from './compute-visual.module.css'
import { ComputeLogo } from './ComputeLogo'
// Ephemeral sandboxes cycle through this lifecycle on a loop: empty (no
// container running) -> deploying -> active -> stopping -> suspending -> empty.
// Durations are compressed relative to a real sandbox's lifetime ("a few
// minutes" of activity becomes tens of seconds, etc.) so the loop stays
// watchable on a marketing page.
type SandboxPhase = 'empty' | 'deploying' | 'active' | 'stopping' | 'suspending'
const PHASE_ORDER: SandboxPhase[] = ['empty', 'deploying', 'active', 'stopping', 'suspending']
const PHASE_DURATION_MS: Record<SandboxPhase, [number, number]> = {
empty: [3000, 6000],
deploying: [2000, 4000],
active: [18000, 32000],
stopping: [800, 1600],
suspending: [3000, 5000],
}
const STATUS_META: Record<
Exclude<SandboxPhase, 'empty'>,
{ label: string; dot: string; text: string }
> = {
deploying: { label: 'Deploying', dot: 'bg-warning animate-pulse', text: 'text-foreground' },
active: { label: 'Active', dot: 'bg-brand', text: 'text-foreground' },
stopping: { label: 'Stopping', dot: 'bg-warning', text: 'text-foreground-muted' },
suspending: { label: 'Suspending', dot: 'bg-border-stronger', text: 'text-foreground-muted' },
}
const SANDBOX_RUNTIMES = ['node', 'deno'] as const
// Names read like something an agent platform could generate per run
const SANDBOX_WORKLOADS = [
'brainstorm',
'plan',
'prototype',
'demo',
'explore',
'research',
'spike',
'draft',
'scaffold',
'smoosh',
'review',
'support',
'tests',
'e2e',
'build',
'scrape',
'crawl',
'migrate',
'seed',
'bench',
'index',
'parse',
'render',
] as const
// The name each slot renders on the server and on first paint. Names are only
// ever re-rolled later, from the lifecycle effect, so hydration stays stable.
const SANDBOX_SLOTS = [
{ name: 'review-4f2a', size: '2 GB · 1 vCPU', delay: 0 },
{ name: 'plan-9be0', size: '2 GB · 1 vCPU', delay: 5500 },
{ name: 'scrape-7c31', size: '2 GB · 1 vCPU', delay: 9000 },
{ name: 'migrate-2d8a', size: '4 GB · 2 vCPU', delay: 3000 },
] as const
const PERSISTENT_SERVICES = [
{ name: 'api', runtime: 'node', size: '2 GB · 1 vCPU', since: '2mo ago' },
{ name: 'embeddings', runtime: 'dockerfile', size: '4 GB · 2 vCPU', since: '3d ago' },
] as const
function randomInRange([min, max]: [number, number]) {
return min + Math.random() * (max - min)
}
function randomItem<T>(items: readonly T[]) {
return items[Math.floor(Math.random() * items.length)]
}
// Two sandboxes can legitimately run the same workload, so the run id — not
// the prefix — is what keeps names distinct on screen.
function randomSandboxName() {
const runId = Math.floor(Math.random() * 0x10000)
.toString(16)
.padStart(4, '0')
return `${randomItem(SANDBOX_WORKLOADS)}-${runId}`
}
function formatElapsed(seconds: number) {
if (seconds < 60) return `${seconds}s`
return `${Math.floor(seconds / 60)}m${seconds % 60}s`
}
function useSandboxLifecycle(
runtimes: readonly string[],
{ name: initialName, delay: initialDelayMs }: (typeof SANDBOX_SLOTS)[number]
) {
const prefersReducedMotion = useReducedMotion()
const [phase, setPhase] = useState<SandboxPhase>('empty')
const [elapsed, setElapsed] = useState(0)
const [runtime, setRuntime] = useState<string>(runtimes[0])
const [name, setName] = useState<string>(initialName)
// Drives the phase machine: schedule the next phase, then reschedule
// itself from there. Each deploy is a different sandbox, so the name and
// runtime are both re-rolled every time a build starts.
useEffect(() => {
if (prefersReducedMotion) return
let timeoutId: ReturnType<typeof setTimeout>
const advance = (current: SandboxPhase) => {
const next = PHASE_ORDER[(PHASE_ORDER.indexOf(current) + 1) % PHASE_ORDER.length]
if (next === 'deploying') {
setName(randomSandboxName())
setRuntime(randomItem(runtimes))
}
setPhase(next)
setElapsed(0)
timeoutId = setTimeout(() => advance(next), randomInRange(PHASE_DURATION_MS[next]))
}
timeoutId = setTimeout(() => advance('empty'), initialDelayMs)
return () => clearTimeout(timeoutId)
}, [runtimes, initialDelayMs, prefersReducedMotion])
// Ticks the elapsed-time counter shown in the card footer while the
// sandbox is doing something worth timing.
useEffect(() => {
if (prefersReducedMotion || phase === 'empty' || phase === 'suspending') return
const interval = setInterval(() => setElapsed((value) => value + 1), 1000)
return () => clearInterval(interval)
}, [phase, prefersReducedMotion])
if (prefersReducedMotion)
return { phase: 'active' as const, elapsed: 96, runtime: runtimes[0], name: initialName }
return { phase, elapsed, runtime, name }
}
function ComputeIcon({ dimmed = false }: { dimmed?: boolean }) {
return (
<div
className={cn(
'hidden sm:flex h-6 w-6 md:h-7 md:w-7 shrink-0 items-center justify-center rounded-md border border-border',
dimmed ? 'text-foreground-muted opacity-30' : 'text-foreground-lighter'
)}
>
<ComputeLogo size={16} />
</div>
)
}
function ComputeCardShell({ children }: { children: React.ReactNode }) {
return (
<div className="flex w-full flex-col rounded-lg border border-border bg-surface-75 overflow-hidden">
{children}
</div>
)
}
function ComputeCardHeader({
name,
runtime,
statusSlot,
}: {
name: string
runtime: string
statusSlot: React.ReactNode
}) {
return (
<div className="flex flex-col sm:flex-row sm:items-center gap-2 px-3 py-2.5">
<div className="flex items-center gap-2">
<ComputeIcon />
<div className="flex min-w-0 flex-col leading-tight">
<span className="truncate text-xs text-foreground">{name}</span>
<span className="truncate font-mono text-[11px] text-foreground-muted">{runtime}</span>
</div>
</div>
<div className="hidden sm:ml-auto sm:flex shrink-0 items-center gap-1.5">{statusSlot}</div>
</div>
)
}
function ComputeCardFooter({
left,
right,
className,
}: {
left: string | React.ReactNode
right: string
className?: string
}) {
return (
<div
className={cn(
'bg-surface-200 flex flex-col sm:flex-row sm:items-center sm:justify-between border-t border-border px-3 py-2 font-mono text-xs text-foreground-muted',
className
)}
>
<span className="flex items-center gap-1.5">{left}</span>
<span>{right}</span>
</div>
)
}
// Always-on production backend service: fixed status, no lifecycle.
function PersistentServiceCard({
name,
runtime,
size,
since,
}: (typeof PERSISTENT_SERVICES)[number]) {
return (
<ComputeCardShell>
<ComputeCardHeader
name={name}
runtime={runtime}
statusSlot={
<>
<span className="h-1.5 w-1.5 rounded-full bg-brand" aria-hidden />
<span className="text-xs text-foreground">Active</span>
</>
}
/>
<ComputeCardFooter className="hidden sm:flex" left={size} right={since} />
<ComputeCardFooter
className="sm:hidden"
left={
<>
<span className="h-1.5 w-1.5 rounded-full bg-brand" aria-hidden />
<span className="text-xs text-foreground">Active</span>
</>
}
right={since}
/>
</ComputeCardShell>
)
}
// Ephemeral sandbox: renders as a dashed empty slot until an agent
// spins it up, then crossfades into a live card that tracks its phase.
// Lifecycle state is lifted to the parent (see useSandboxLifecycle call
// sites in ComputeVisual) so the connector lines can react to it too.
function SandboxCard({
name,
size,
phase,
elapsed,
runtime,
}: {
name: string
size: string
phase: SandboxPhase
elapsed: number
runtime: string
}) {
return (
<AnimatePresence mode="popLayout" initial={false}>
{phase === 'empty' ? (
<motion.div
key="empty"
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
transition={{ duration: 0.3 }}
className="flex h-full items-center justify-center rounded-lg border border-dashed border-border"
>
<ComputeIcon dimmed />
</motion.div>
) : (
<motion.div
key="card"
initial={{ opacity: 0, scale: 0.96 }}
animate={{ opacity: 1, scale: 1 }}
exit={{ opacity: 0, scale: 0.96 }}
transition={{ duration: 0.3 }}
>
<ComputeCardShell>
<ComputeCardHeader
name={name}
runtime={runtime}
statusSlot={
<AnimatePresence mode="wait" initial={false}>
<motion.span
key={phase}
initial={{ opacity: 0, y: -2 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: 2 }}
transition={{ duration: 0.2 }}
className="flex items-center gap-1.5"
>
<span
className={cn('h-1.5 w-1.5 rounded-full', STATUS_META[phase].dot)}
aria-hidden
/>
<span className={cn('text-xs', STATUS_META[phase].text)}>
{STATUS_META[phase].label}
</span>
</motion.span>
</AnimatePresence>
}
/>
<ComputeCardFooter
className="hidden sm:flex"
left={size}
right={phase === 'suspending' ? '–' : formatElapsed(elapsed)}
/>
<ComputeCardFooter
className="sm:hidden"
left={
<AnimatePresence mode="wait" initial={false}>
<motion.span
key={phase}
initial={{ opacity: 0, y: -2 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: 2 }}
transition={{ duration: 0.2 }}
className="flex items-center gap-1.5"
>
<span
className={cn('h-1.5 w-1.5 rounded-full', STATUS_META[phase].dot)}
aria-hidden
/>
<span className={cn('text-xs', STATUS_META[phase].text)}>
{STATUS_META[phase].label}
</span>
</motion.span>
</AnimatePresence>
}
right={phase === 'suspending' ? '–' : formatElapsed(elapsed)}
/>
</ComputeCardShell>
</motion.div>
)}
</AnimatePresence>
)
}
function ApplicationCard() {
return (
<div className="flex items-center gap-2 rounded-lg border border-foreground-muted/30 border-dashed px-4 py-2.5">
<Database className="h-4 w-4 text-foreground-lighter" strokeWidth={1.5} aria-hidden />
<span className="font-mono text-xs uppercase tracking-widest text-foreground-lighter">
Application
</span>
</div>
)
}
function SourceCard({ icon: Icon, label }: { icon: LucideIcon; label: string }) {
return (
<div className="flex flex-col items-center justify-center rounded-lg border border-foreground-muted/30 border-dashed sm:min-w-32 px-4 py-2 sm:py-4 gap-1 sm:gap-2">
<Icon className="h-5 w-5 text-foreground-muted" strokeWidth={1.5} aria-hidden />
<span className="font-mono text-xs uppercase tracking-widest text-foreground-muted">
{label}
</span>
</div>
)
}
/* ------------------------------------------------------------------ *
* Connectors
*
* The dashed lines are drawn in a single SVG overlay instead of being
* baked into the layout, so they stay correct at any width: every card
* is measured, each connector is routed between two measured centers,
* and the whole layer is masked by the card rectangles. Masking is what
* lets a connector be anchored at a card's center — the line is trimmed
* exactly at the card's edge, so it reads as plugging into the card.
* ------------------------------------------------------------------ */
type NodeKey =
| 'app'
| 'api'
| 'embeddings'
| 'sandboxTopMid'
| 'sandboxBottomMid'
| 'sandboxTopRight'
| 'sandboxBottomRight'
| 'human'
| 'agent1'
| 'agent2'
type Box = { x: number; y: number; width: number; height: number }
type Point = { x: number; y: number }
type Geometry = { width: number; height: number; boxes: Partial<Record<NodeKey, Box>> }
type Connector = { id: string; d: string; active: boolean }
const CORNER_RADIUS = 14
const CARD_RADIUS = 8
// Measures every registered node relative to the container and re-measures
// on any resize (viewport, font load, a card changing size).
function useDiagramGeometry() {
const containerRef = useRef<HTMLElement | null>(null)
const nodesRef = useRef(new Map<NodeKey, HTMLElement>())
const [geometry, setGeometry] = useState<Geometry | null>(null)
const registerNode = useCallback(
(key: NodeKey) => (element: HTMLElement | null) => {
if (element) nodesRef.current.set(key, element)
else nodesRef.current.delete(key)
},
[]
)
useEffect(() => {
const container = containerRef.current
if (!container) return
const measure = () => {
const base = container.getBoundingClientRect()
const boxes: Partial<Record<NodeKey, Box>> = {}
nodesRef.current.forEach((element, key) => {
const rect = element.getBoundingClientRect()
boxes[key] = {
x: rect.left - base.left,
y: rect.top - base.top,
width: rect.width,
height: rect.height,
}
})
const next = { width: base.width, height: base.height, boxes }
// Bail out on no-op measurements: ResizeObserver fires once per
// observed element on setup, and each of those would re-render.
setGeometry((current) =>
current && JSON.stringify(current) === JSON.stringify(next) ? current : next
)
}
measure()
const observer = new ResizeObserver(measure)
observer.observe(container)
nodesRef.current.forEach((element) => observer.observe(element))
return () => observer.disconnect()
}, [])
return { containerRef, registerNode, geometry }
}
function distance(a: Point, b: Point) {
return Math.hypot(b.x - a.x, b.y - a.y)
}
function towards(from: Point, to: Point, length: number): Point {
const total = distance(from, to)
if (total === 0) return from
return {
x: from.x + ((to.x - from.x) * length) / total,
y: from.y + ((to.y - from.y) * length) / total,
}
}
// Orthogonal polyline with rounded corners — the classic diagram elbow.
function roundedPath(points: Point[], radius = CORNER_RADIUS) {
const waypoints = points.filter(
(point, index) => index === 0 || distance(points[index - 1], point) > 0.5
)
if (waypoints.length < 2) return ''
let d = `M ${waypoints[0].x} ${waypoints[0].y}`
for (let index = 1; index < waypoints.length - 1; index++) {
const previous = waypoints[index - 1]
const corner = waypoints[index]
const next = waypoints[index + 1]
const cornerRadius = Math.min(
radius,
distance(previous, corner) / 2,
distance(corner, next) / 2
)
if (cornerRadius < 0.5) {
d += ` L ${corner.x} ${corner.y}`
continue
}
const entry = towards(corner, previous, cornerRadius)
const exit = towards(corner, next, cornerRadius)
d += ` L ${entry.x} ${entry.y} Q ${corner.x} ${corner.y} ${exit.x} ${exit.y}`
}
const last = waypoints[waypoints.length - 1]
return `${d} L ${last.x} ${last.y}`
}
// Routes every connector from the measured layout: one line per column from
// its source card up into the bottom card of that column, plus the always-on
// feeding the application. Anchoring at card centers and masking the card
// rectangles is what makes a line read as plugging into the card.
function buildConnectors(geometry: Geometry, live: Record<string, boolean>): Connector[] {
const { boxes } = geometry
const keys: NodeKey[] = [
'app',
'api',
'embeddings',
'sandboxBottomMid',
'sandboxBottomRight',
'human',
'agent1',
'agent2',
]
if (keys.some((key) => !boxes[key])) return []
const box = (key: NodeKey) => boxes[key] as Box
const top = (key: NodeKey) => box(key).y
const bottom = (key: NodeKey) => box(key).y + box(key).height
const cx = (key: NodeKey) => box(key).x + box(key).width / 2
const cy = (key: NodeKey) => box(key).y + box(key).height / 2
const center = (key: NodeKey): Point => ({ x: cx(key), y: cy(key) })
const at = (x: number, y: number): Point => ({ x, y })
// Horizontal bands: the empty strips above and below the card grid, where
// each connector makes its sideways jog.
const sourceBand = (bottom('embeddings') + top('human')) / 2
const appBand = (bottom('app') + top('api')) / 2
const connector = (id: string, points: Point[], active = true): Connector => ({
id,
d: roundedPath(points),
active,
})
return [
// Each source feeds its own column.
connector('human-embeddings', [
center('human'),
at(cx('human'), sourceBand),
at(cx('embeddings'), sourceBand),
center('embeddings'),
]),
connector(
'agent1-sandboxBottomMid',
[
center('agent1'),
at(cx('agent1'), sourceBand),
at(cx('sandboxBottomMid'), sourceBand),
center('sandboxBottomMid'),
],
live.sandboxBottomMid
),
connector(
'agent2-sandboxBottomRight',
[
center('agent2'),
at(cx('agent2'), sourceBand),
at(cx('sandboxBottomRight'), sourceBand),
center('sandboxBottomRight'),
],
live.sandboxBottomRight
),
// The always-on column serves the application.
connector('api-app', [
center('api'),
at(cx('api'), appBand),
at(cx('app'), appBand),
center('app'),
]),
]
}
// Faint dashed base for every connector, plus a brighter overlay that marches
// along the path (see .flow) and fades out while the workload it feeds is idle.
function ConnectorLayer({
geometry,
live,
}: {
geometry: Geometry | null
live: Record<string, boolean>
}) {
const maskId = `compute-connectors-mask-${useId()}`
if (!geometry) return null
const { width, height, boxes } = geometry
const connectors = buildConnectors(geometry, live)
if (!connectors.length) return null
return (
<svg
className="pointer-events-none absolute inset-0 h-full w-full"
viewBox={`0 0 ${width} ${height}`}
fill="none"
aria-hidden="true"
>
<mask id={maskId} maskUnits="userSpaceOnUse" x="0" y="0" width={width} height={height}>
<rect x="0" y="0" width={width} height={height} fill="white" />
{Object.entries(boxes).map(([key, node]) => (
<rect
key={key}
x={node.x}
y={node.y}
width={node.width}
height={node.height}
rx={CARD_RADIUS}
fill="black"
/>
))}
</mask>
<g mask={`url(#${maskId})`}>
{connectors.map((connector) => (
<g key={connector.id}>
<path
d={connector.d}
className="text-border-muted"
stroke="currentColor"
strokeWidth="1"
strokeDasharray="2 4"
/>
<path
d={connector.d}
className={cn(
'text-foreground-muted/50 transition-opacity duration-100',
styles.flow
)}
stroke="currentColor"
strokeWidth="1"
strokeLinecap="round"
style={{ opacity: connector.active ? 1 : 0 }}
/>
</g>
))}
</g>
</svg>
)
}
export function ComputeVisual() {
// Lifecycle state for the 4 sandbox slots, lifted up here so the connector
// lines below can react to whichever sandboxes they feed.
const sandboxTopMid = useSandboxLifecycle(SANDBOX_RUNTIMES, SANDBOX_SLOTS[0])
const sandboxTopRight = useSandboxLifecycle(SANDBOX_RUNTIMES, SANDBOX_SLOTS[1])
const sandboxBottomMid = useSandboxLifecycle(SANDBOX_RUNTIMES, SANDBOX_SLOTS[2])
const sandboxBottomRight = useSandboxLifecycle(SANDBOX_RUNTIMES, SANDBOX_SLOTS[3])
const { containerRef, registerNode, geometry } = useDiagramGeometry()
// A connector only flows while the sandbox it feeds is actually running.
const isLive = (phase: SandboxPhase) => phase !== 'empty' && phase !== 'suspending'
const live = {
sandboxTopMid: isLive(sandboxTopMid.phase),
sandboxBottomMid: isLive(sandboxBottomMid.phase),
sandboxTopRight: isLive(sandboxTopRight.phase),
sandboxBottomRight: isLive(sandboxBottomRight.phase),
}
return (
<SectionContainerWithCn>
<figure ref={containerRef} className="relative w-full max-w-3xl mx-auto">
<span className="sr-only">
A diagram showing humans and agents connecting to Supabase Compute. Two always-on backend
services serve the application; agents spin up short-lived sandboxes on demand, which
build, run, and suspend automatically.
</span>
<ConnectorLayer geometry={geometry} live={live} />
<div className="relative grid grid-cols-3 gap-x-2" aria-hidden="true">
<div className="flex col-span-full justify-center">
<div ref={registerNode('app')}>
<ApplicationCard />
</div>
</div>
<div className="col-span-full h-12" />
<div ref={registerNode('api')} className="sm:pr-2">
<PersistentServiceCard {...PERSISTENT_SERVICES[0]} />
</div>
<div ref={registerNode('sandboxTopMid')} className="h-full">
<SandboxCard size={SANDBOX_SLOTS[0].size} {...sandboxTopMid} />
</div>
<div ref={registerNode('sandboxTopRight')} className="h-full">
<SandboxCard size={SANDBOX_SLOTS[1].size} {...sandboxTopRight} />
</div>
<div className="col-span-full h-2" />
<div ref={registerNode('embeddings')} className="sm:pr-2">
<PersistentServiceCard {...PERSISTENT_SERVICES[1]} />
</div>
<div ref={registerNode('sandboxBottomMid')} className="h-full">
<SandboxCard size={SANDBOX_SLOTS[2].size} {...sandboxBottomMid} />
</div>
<div ref={registerNode('sandboxBottomRight')} className="h-full">
<SandboxCard size={SANDBOX_SLOTS[3].size} {...sandboxBottomRight} />
</div>
<div className="col-span-full h-12" />
{/* Sources */}
<div className="col-span-full flex justify-center gap-2">
<div ref={registerNode('human')}>
<SourceCard icon={User} label="Human" />
</div>
<div ref={registerNode('agent1')}>
<SourceCard icon={Bot} label="Agent 1" />
</div>
<div ref={registerNode('agent2')}>
<SourceCard icon={Bot} label="Agent 2" />
</div>
</div>
</div>
</figure>
</SectionContainerWithCn>
)
}
@@ -0,0 +1,91 @@
'use client'
import SectionContainerWithCn from '~/components/Layouts/SectionContainerWithCn'
import { AnimatePresence, motion, useInView, useReducedMotion } from 'framer-motion'
import Link from 'next/link'
import { useEffect, useRef, useState } from 'react'
import { Button } from 'ui'
const TERMINAL_LINES = [
{ text: '$ supabase compute deploy my-app', delay: 0 },
{ text: 'Building from Dockerfile...', delay: 0.3, dim: true },
{ text: "Provisioning 2 GB / 1 vCPU in your database's region...", delay: 0.6, dim: true },
{ text: 'Issuing scoped project credentials...', delay: 0.9, dim: true },
{ text: '', delay: 1.1 },
{ text: '✓ Service deployed:', delay: 1.2, accent: true },
{ text: ' https://your-project.supabase.co/compute/my-app', delay: 1.3, accent: true },
]
function TerminalAnimation() {
const ref = useRef<HTMLDivElement>(null)
const inView = useInView(ref, { once: true, amount: 0.5 })
const prefersReducedMotion = useReducedMotion()
const [visibleCount, setVisibleCount] = useState(0)
useEffect(() => {
if (!inView) return
// Show the finished output straight away rather than typing it out.
if (prefersReducedMotion) {
setVisibleCount(TERMINAL_LINES.length)
return
}
const timeouts: ReturnType<typeof setTimeout>[] = []
TERMINAL_LINES.forEach((line, i) => {
timeouts.push(setTimeout(() => setVisibleCount(i + 1), line.delay * 1000))
})
return () => timeouts.forEach(clearTimeout)
}, [inView, prefersReducedMotion])
return (
<div ref={ref} className="w-full rounded-lg border border-border bg-surface-75 overflow-hidden">
<div className="flex items-center gap-1.5 px-2 py-2 border-b border-border bg-surface-200">
<span className="ml-2 text-xs text-foreground-muted font-mono uppercase">Terminal</span>
</div>
<div className="px-4 md:px-6 font-mono text-xs md:text-sm leading-relaxed h-[240px] flex flex-col justify-start items-start text-left pt-4">
<AnimatePresence>
{TERMINAL_LINES.slice(0, visibleCount).map((line, i) => (
<motion.div
key={i}
initial={prefersReducedMotion ? false : { opacity: 0, y: 1 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: prefersReducedMotion ? 0 : 0.2 }}
className={
line.accent ? 'text-brand' : line.dim ? 'text-foreground-light' : 'text-foreground'
}
>
{line.text || ' '}
</motion.div>
))}
</AnimatePresence>
</div>
</div>
)
}
export function DeploySection() {
return (
<SectionContainerWithCn spacing="sections">
<div className="grid grid-cols-1 lg:grid-cols-2 gap-8 lg:gap-16 items-start">
<div className="flex flex-col items-start gap-4">
<span className="text-foreground-muted font-mono text-xs uppercase tracking-widest">
Deployment
</span>
<h2 className="text-2xl md:text-4xl text-foreground">
Deploy from
<span className="text-foreground-lighter block">wherever you work</span>
</h2>
<p className="text-foreground-lighter text-sm lg:text-base">
The same deployment surface serves humans and agents: one command in a terminal, one
tool call in an agent session, one request against the API.
</p>
<Button asChild>
<Link href="#waitlist">Join the waitlist</Link>
</Button>
</div>
<TerminalAnimation />
</div>
</SectionContainerWithCn>
)
}
@@ -0,0 +1,70 @@
'use client'
import ReactMarkdown from 'react-markdown'
import { Accordion, AccordionContent, AccordionItem, AccordionTrigger } from 'ui'
const faqs = [
{
question: 'How does Compute relate to Edge Functions?',
answer:
"Compute complements Edge Functions, which remain fully supported and best for short-lived work like webhooks and lightweight APIs. Compute handles what they can't: long-running jobs, background workers, and heavier workloads with no wall-clock limits. If a function outgrows Edge Functions, you can migrate it to Compute without code changes.",
},
{
question: 'Which languages and runtimes are supported?',
answer:
'In Private Alpha, Node, Deno, and any Dockerfile are supported — no wrapper scripts or extra orchestration code required. Bun and Python are on the way; until they land, a Dockerfile covers anything else you need to run.',
},
{
question: 'Can a service run indefinitely?',
answer:
'There are no wall-clock limits, so jobs and pipelines run as long as the work takes. For HTTP services, Compute suspends automatically when idle on network I/O and resumes in under a second, so a service configured as always-on behaves like one without billing you for idle time.',
},
{
question: 'Where does Compute run?',
answer:
"During Private Alpha, Compute is only available in us-west-2, with more regions rolling out over time until every Supabase region is covered. Sandboxes and services deploy to your primary database's region — that's where writes happen, and it keeps behavior predictable. They can still read from replicas via the load-balanced connection endpoint, so replica capacity isn't wasted. Pinning a workload to a specific replica region is on the roadmap.",
},
{
question: 'Can I attach a persistent disk?',
answer:
'Sandboxes and services are designed to scale to zero and restart on demand, so the local filesystem is fast scratch space, not durable storage. For state that needs to persist, use your Postgres database for structured data and Supabase Storage for files — both reachable with near-zero intra-region latency and no data-transfer tax. Mountable persistent disks are on the roadmap.',
},
{
question: 'How will pricing work?',
answer:
'Compute bills per compute-hour based on the instance size you choose, with no per-request metering. Idle sandboxes and services suspend automatically, so you pay nothing while they sleep. Traffic between a workload and its own database is free. There is no separate subscription — usage appears on your existing Supabase invoice with the same spend caps and credits. Final rates will be published before general availability.',
},
{
question: 'How do I get access?',
answer:
'Compute is in Private Alpha. Join the waitlist and we will send invites in waves as capacity grows. Feedback from alpha participants directly shapes the product ahead of public launch.',
},
]
export function FAQSection() {
return (
<div className="mx-auto max-w-5xl">
<div className="sm:py-18 mx-auto px-6 py-16 md:py-24 lg:px-16 lg:py-24 xl:px-20">
<h2 className="h3 text-center">Frequently asked questions</h2>
<div className="my-16">
<Accordion type="multiple" className="text-foreground-light">
{faqs.map((faq, i) => (
<div className="border-b py-2" key={i}>
<AccordionItem value={`faq--${i.toString()}`} className="border-none">
<AccordionTrigger>
<span className="text-foreground text-left">{faq.question}</span>
</AccordionTrigger>
<AccordionContent>
<div className="prose text-foreground-lighter">
<ReactMarkdown>{faq.answer}</ReactMarkdown>
</div>
</AccordionContent>
</AccordionItem>
</div>
))}
</Accordion>
</div>
</div>
</div>
)
}
@@ -0,0 +1,33 @@
import SectionContainerWithCn from '~/components/Layouts/SectionContainerWithCn'
import Link from 'next/link'
import { Badge, Button } from 'ui'
export function Hero() {
return (
<SectionContainerWithCn spacing="sections">
<div className="flex flex-col gap-4">
<div className="flex items-center gap-4 -mt-8">
<Badge variant="warning">Private Alpha</Badge>
</div>
<div className="grid grid-cols-1 lg:grid-cols-2 gap-4 items-end">
<h1 className="text-foreground text-3xl sm:text-5xl sm:leading-none">
<span className="flex items-center gap-3">Compute</span>
<span className="text-foreground-lighter block">for agentic workloads</span>
</h1>
<p className="text-foreground-lighter text-sm lg:text-base text-balance">
Agent sandboxes and always-on backend services running inside your Supabase project —
next to your data, behind your auth.
</p>
</div>
<div className="flex items-center gap-2 mt-2 lg:mt-4">
<Button asChild size="medium">
<Link href="#waitlist">Join the waitlist</Link>
</Button>
{/* <Button asChild size="medium" variant="outline">
<Link href="#waitlist">View announcement</Link>
</Button> */}
</div>
</div>
</SectionContainerWithCn>
)
}
@@ -0,0 +1,41 @@
import { FeatureItem, type Feature } from '~/components/FeatureItem'
import SectionContainerWithCn from '~/components/Layouts/SectionContainerWithCn'
import { Boxes, Database, DollarSign, Terminal } from 'lucide-react'
const highlights: Feature[] = [
{
icon: Boxes,
heading: 'One primitive, every workload',
subheading: 'Ephemeral sandboxes and always-on HTTP services, one primitive.',
},
{
icon: Terminal,
heading: 'Any language',
subheading: 'Node, Deno, or any Dockerfile.',
},
{
icon: Database,
heading: 'Next to your database',
subheading: 'Same network as Postgres — single-digit millisecond queries.',
},
{
icon: DollarSign,
heading: 'Scale to zero',
subheading:
'Services suspend when idle and resume in under a second. You pay nothing while they sleep.',
},
]
export function Highlights() {
return (
<div>
<SectionContainerWithCn height="none" className="mb-8 md:mb-24">
<ul className="grid grid-cols-1 gap-4 gap-y-10 md:grid-cols-2 md:gap-12 xl:grid-cols-4">
{highlights.map((highlight) => (
<FeatureItem feature={highlight} key={highlight.heading} />
))}
</ul>
</SectionContainerWithCn>
</div>
)
}
@@ -0,0 +1,70 @@
import SectionContainerWithCn from '~/components/Layouts/SectionContainerWithCn'
import { EyeOff, KeyRound, Network, Plug, ScanSearch, ShieldCheck } from 'lucide-react'
const features = [
{
title: 'Short-lived credentials',
paragraph: 'Access runs through Supabase Auth with short-lived keys, not standing secrets.',
icon: <KeyRound className="w-5 h-5 stroke-[1.4px]" />,
},
{
title: 'Zero-config data access',
paragraph: 'Reach your database and Storage with the permissions your key already carries.',
icon: <Plug className="w-5 h-5 stroke-[1.4px]" />,
},
{
title: 'Per-workload firewalls',
paragraph: 'Define which external endpoints and ports each workload can reach.',
icon: <Network className="w-5 h-5 stroke-[1.4px]" />,
},
{
title: 'Cloaked secrets',
paragraph: 'Scope secrets per workload, and cloak values so your code never sees them.',
icon: <EyeOff className="w-5 h-5 stroke-[1.4px]" />,
},
{
title: 'Patched automatically',
paragraph: 'Kernel-level security patches roll out automatically. Nothing to do on your part.',
icon: <ShieldCheck className="w-5 h-5 stroke-[1.4px]" />,
},
{
title: 'Dependency scanning',
paragraph: 'Dependencies are scanned at runtime, with alerts as new vulnerabilities land.',
icon: <ScanSearch className="w-5 h-5 stroke-[1.4px]" />,
},
]
export function TrustBoundarySection() {
return (
<SectionContainerWithCn spacing="sections">
<div className="flex flex-col gap-4 max-w-xl">
<span className="text-foreground-muted font-mono text-xs uppercase tracking-widest">
Security
</span>
<h2 className="text-2xl md:text-4xl text-foreground">
One trust boundary,
<span className="text-foreground-lighter block">preconfigured</span>
</h2>
<p className="text-foreground-lighter text-sm lg:text-base">
Every sandbox and service inherits your project's auth, roles, and permissions the moment
it starts, running in its own microVM with dedicated CPU and memory. Agents get a sandbox
that is already scoped to the data it is allowed to touch — nothing more.
</p>
</div>
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-3">
{features.map((feature) => (
<div
key={feature.title}
className="bg-surface-75 border border-border rounded-lg p-6 flex flex-col gap-3"
>
<div className="text-foreground-lighter" aria-hidden>
{feature.icon}
</div>
<h3 className="text-foreground text-base font-medium">{feature.title}</h3>
<p className="text-foreground-lighter text-sm">{feature.paragraph}</p>
</div>
))}
</div>
</SectionContainerWithCn>
)
}
@@ -0,0 +1,263 @@
'use client'
import { CircleAlert } from 'lucide-react'
import Link from 'next/link'
import { useEffect, useState } from 'react'
import {
Button,
Input,
Label,
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
Separator,
TextArea,
} from 'ui'
import { Alert } from 'ui/src/components/shadcn/ui/alert'
interface FormData {
firstName: string
lastName: string
email: string
company: string
useCase: string
details: string
}
const defaultFormValue: FormData = {
firstName: '',
lastName: '',
email: '',
company: '',
useCase: '',
details: '',
}
const isValidEmail = (email: string): boolean => {
const emailPattern = /^[\w-\.+]+@([\w-]+\.)+[\w-]{2,8}$/
return emailPattern.test(email)
}
export function WaitlistForm() {
const [formData, setFormData] = useState<FormData>(defaultFormValue)
const [honeypot, setHoneypot] = useState('')
const [errors, setErrors] = useState<{ [key: string]: string }>({})
const [isSubmitting, setIsSubmitting] = useState(false)
const [success, setSuccess] = useState(false)
const [startTime, setStartTime] = useState(0)
useEffect(() => {
setStartTime(Date.now())
}, [])
const handleChange = (e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>) => {
const { name, value } = e.target
setErrors({})
setFormData((prev) => ({ ...prev, [name]: value }))
}
const validate = (): boolean => {
const newErrors: { [key: string]: string } = {}
if (!formData.firstName) newErrors.firstName = 'This field is required'
if (!formData.lastName) newErrors.lastName = 'This field is required'
if (!formData.email) newErrors.email = 'This field is required'
else if (!isValidEmail(formData.email)) newErrors.email = 'Invalid email address'
setErrors(newErrors)
return Object.keys(newErrors).length === 0
}
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault()
// Spam prevention: reject if submitted implausibly fast.
if ((Date.now() - startTime) / 1000 < 3) {
setErrors({ general: 'Submission too fast. Please try again.' })
return
}
if (!validate()) return
setIsSubmitting(true)
try {
const response = await fetch('/api-v2/submit-form-compute-waitlist', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ ...formData, honeypot }),
})
if (response.ok) {
setSuccess(true)
} else {
const errorData = await response.json()
setErrors({ general: errorData.message ?? 'Submission failed. Please try again.' })
}
} catch {
setErrors({ general: 'An unexpected error occurred. Please try again.' })
} finally {
setIsSubmitting(false)
}
}
return (
<div className="w-full max-w-lg">
{/* Always mounted so assistive tech announces submission results as they change. */}
<div role="status" aria-live="polite" className="sr-only">
{success
? "You're on the list. We'll reach out as invites roll out during the Private Alpha."
: (errors.general ?? '')}
</div>
{success ? (
<div className="border border-border rounded-lg bg-surface-75 p-6 md:p-8 w-full flex flex-col items-center gap-2 text-center">
<p className="text-foreground font-medium">You&apos;re on the list</p>
<p className="text-foreground-lighter text-sm">
We&apos;ll reach out as invites roll out during the Private Alpha.
</p>
</div>
) : (
<form
onSubmit={handleSubmit}
className="border border-border rounded-lg bg-surface-75 p-6 md:p-8 w-full max-w-lg flex flex-col gap-4 text-left"
>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div className="flex flex-col gap-2">
<Label
htmlFor="firstName"
className="text-foreground-light flex justify-between text-left"
>
First name
{errors.firstName && (
<span className="text-foreground-muted text-xs">{errors.firstName}</span>
)}
</Label>
<Input
id="firstName"
name="firstName"
value={formData.firstName}
onChange={handleChange}
placeholder="Jane"
/>
</div>
<div className="flex flex-col gap-2">
<Label
htmlFor="lastName"
className="text-foreground-light flex justify-between text-left"
>
Last name
{errors.lastName && (
<span className="text-foreground-muted text-xs">{errors.lastName}</span>
)}
</Label>
<Input
id="lastName"
name="lastName"
value={formData.lastName}
onChange={handleChange}
placeholder="Doe"
/>
</div>
</div>
<div className="flex flex-col gap-2">
<Label htmlFor="email" className="text-foreground-light flex justify-between text-left">
Work email
{errors.email && (
<span className="text-foreground-muted text-xs">{errors.email}</span>
)}
</Label>
<Input
id="email"
name="email"
type="email"
value={formData.email}
onChange={handleChange}
placeholder="jane@company.com"
/>
</div>
<div className="flex flex-col gap-2">
<Label htmlFor="company" className="text-foreground-light text-left">
Company
</Label>
<Input
id="company"
name="company"
value={formData.company}
onChange={handleChange}
placeholder="Company name"
/>
</div>
<div className="flex flex-col gap-2">
<Label htmlFor="useCase" className="text-foreground-light text-left">
What will you run on Compute?
</Label>
<Select
value={formData.useCase}
onValueChange={(value) => setFormData((prev) => ({ ...prev, useCase: value }))}
>
<SelectTrigger id="useCase">
<SelectValue placeholder="Select an option" />
</SelectTrigger>
<SelectContent>
<SelectItem value="sandboxes">Sandboxes for AI agents</SelectItem>
<SelectItem value="services">Backend services</SelectItem>
<SelectItem value="both">Both</SelectItem>
</SelectContent>
</Select>
</div>
<div className="flex flex-col gap-2">
<Label htmlFor="details" className="text-foreground-light text-left">
Tell us more
</Label>
<TextArea
id="details"
name="details"
value={formData.details}
onChange={handleChange}
placeholder="What are you building, and what would you run on Compute?"
rows={3}
/>
</div>
{/* Honeypot: hidden from real users, catches basic bots. */}
<input
type="text"
name="honeypot"
value={honeypot}
onChange={(e) => setHoneypot(e.target.value)}
tabIndex={-1}
autoComplete="off"
aria-hidden="true"
className="hidden"
/>
<Separator />
<Button type="submit" block size="medium" loading={isSubmitting} disabled={isSubmitting}>
Join the waitlist
</Button>
<p className="text-foreground-lighter text-xs text-left">
By submitting this form, you acknowledge that Supabase Compute is offered as a private
alpha preview, provided on an "as is" and "as available" basis without warranties of any
kind. It is intended for internal evaluation only and should not be used to serve
production workloads or your own end customers. Supabase may modify, suspend, or
discontinue the preview at any time without notice, and there is no guarantee it will
become a generally available product. You confirm you are authorized to submit this
request on behalf of your organization, and that you have read and understood our{' '}
<Link href="/privacy" className="text-foreground hover:underline">
Privacy Policy
</Link>
.
</p>
{errors.general && (
<Alert role="presentation" className="flex gap-2 text-foreground text-sm">
<CircleAlert className="w-3 h-3 shrink-0" /> <span>{errors.general}</span>
</Alert>
)}
</form>
)}
</div>
)
}
@@ -0,0 +1,105 @@
import SectionContainerWithCn from '~/components/Layouts/SectionContainerWithCn'
import type { LucideIcon } from 'lucide-react'
import {
Bot,
Container,
Infinity as InfinityIcon,
Pause,
Server,
Shield,
Terminal,
TrendingUp,
} from 'lucide-react'
type WorkloadFeature = {
icon: LucideIcon
text: string
}
type Workload = {
label: string
title: string
paragraph: string
features: WorkloadFeature[]
}
const workloads: Workload[] = [
{
label: 'Sandboxes',
title: 'Isolated environments for agent code',
paragraph:
'Execute code in a secure, isolated environment — one per task, per session, or per agent.',
features: [
{ icon: Shield, text: 'Per-session environment for executing code' },
{ icon: Bot, text: 'Provision programmatically via the API or MCP server' },
{ icon: Pause, text: 'Suspend and resume with state preserved' },
],
},
{
label: 'Services',
title: 'Always-on backends in any language',
paragraph:
'Transcription pipelines, embedding jobs, agent frameworks, and APIs — running as long as the work takes.',
features: [
{ icon: TrendingUp, text: 'Auto-scaling for traffic surges' },
{ icon: InfinityIcon, text: 'No wall-clock limits on execution' },
{ icon: Container, text: 'Any runtime, static binary, or Dockerfile' },
{ icon: Server, text: 'HTTP services, background jobs, and long-running pipelines' },
],
},
]
export function WorkloadsSection() {
return (
<SectionContainerWithCn spacing="sections">
<div className="flex flex-col gap-4">
<span className="text-foreground-muted font-mono text-xs uppercase tracking-widest">
Workloads
</span>
<div className="flex flex-col md:grid grid-cols-2 items-end gap-2">
<h2 className="text-2xl md:text-4xl text-foreground">
One runtime,
<span className="text-foreground-lighter block">two shapes of work</span>
</h2>
<p className="text-foreground-lighter text-sm lg:text-base text-pretty">
Sandboxes, APIs, and background jobs usually mean three vendors and a database somewhere
else. Compute runs them all in Supabase alongside your application backend.
</p>
</div>
</div>
<div className="grid grid-cols-1 lg:grid-cols-2 gap-3">
{workloads.map((workload) => (
<div
key={workload.label}
className="bg-surface-75 border border-border rounded-lg p-6 md:p-8 flex flex-col gap-4"
>
<span className="text-foreground-lighter font-mono text-xs uppercase tracking-widest">
{workload.label}
</span>
<div className="flex flex-col gap-1">
<h3 className="text-foreground text-2xl">{workload.title}</h3>
<p className="text-foreground-lighter text-sm">{workload.paragraph}</p>
</div>
<ul className="flex flex-col gap-2 mt-2">
{workload.features.map((feature) => {
const Icon = feature.icon
return (
<li
key={feature.text}
className="flex items-start gap-2 text-foreground-light text-sm"
>
<Icon
className="w-4 h-4 text-brand shrink-0 mt-0.5 stroke-[1.5px]"
aria-hidden
/>
{feature.text}
</li>
)
})}
</ul>
</div>
))}
</div>
</SectionContainerWithCn>
)
}
@@ -0,0 +1,19 @@
/* Marching-dashes flow on the connector lines between sources, workloads, and
* the application. Every connector path is authored source -> target, so a
* negative offset makes the dashes travel in the direction of the flow. */
.flow {
stroke-dasharray: 3 9;
animation: flow 0.9s linear infinite;
}
@keyframes flow {
to {
stroke-dashoffset: -12;
}
}
@media (prefers-reduced-motion: reduce) {
.flow {
animation: none;
}
}
+15
View File
@@ -0,0 +1,15 @@
import type { Metadata } from 'next'
import { ComputeContent } from './_components/ComputeContent'
import { mdAlternates } from '@/lib/md-alternates'
export const metadata: Metadata = {
title: 'Compute | Supabase',
description:
'Run AI-agent sandboxes and production backends on one runtime, next to your database. Now in Private Alpha.',
alternates: mdAlternates('compute'),
}
export default function ComputePage() {
return <ComputeContent />
}
+1
View File
@@ -12,6 +12,7 @@ const SEGMENT_TO_PRODUCT: Record<string, PRODUCT_NAMES> = {
auth: PRODUCT_NAMES.AUTHENTICATION,
storage: PRODUCT_NAMES.STORAGE,
'edge-functions': PRODUCT_NAMES.FUNCTIONS,
compute: PRODUCT_NAMES.COMPUTE,
realtime: PRODUCT_NAMES.REALTIME,
}
@@ -0,0 +1,178 @@
import * as Sentry from '@sentry/nextjs'
const corsHeaders = {
'Access-Control-Allow-Origin': '*',
'Access-Control-Allow-Headers': 'authorization, x-client-info, apikey, content-type',
'Access-Control-Allow-Methods': 'POST, OPTIONS',
}
const isValidEmail = (email: string): boolean => {
const emailPattern = /^[\w-\.+]+@([\w-]+\.)+[\w-]{2,8}$/
return emailPattern.test(email)
}
const RATE_LIMIT_WINDOW = 60 * 1000 // 1 minute
const RATE_LIMIT_MAX = 5
const ipRequestMap = new Map<string, { count: number; resetAt: number }>()
const MAX_FIELD_LENGTH = 255
const MAX_DETAILS_LENGTH = 2000
const USE_CASE_OPTIONS = ['sandboxes', 'services', 'both']
const isFilledString = (value: unknown, maxLength: number): value is string =>
typeof value === 'string' && value.trim().length > 0 && value.length <= maxLength
const isOptionalString = (value: unknown, maxLength: number): boolean =>
value === undefined || value === null || (typeof value === 'string' && value.length <= maxLength)
export async function OPTIONS() {
return new Response(null, {
headers: corsHeaders,
status: 204,
})
}
export async function POST(req: Request) {
const HUBSPOT_PORTAL_ID = process.env.HUBSPOT_PORTAL_ID
const HUBSPOT_FORM_GUID = process.env.HUBSPOT_COMPUTE_WAITLIST_FORM_GUID
// x-vercel-forwarded-for is set by the platform and cannot be spoofed by the client.
const ip =
req.headers.get('x-vercel-forwarded-for')?.split(',')[0]?.trim() ||
req.headers.get('x-forwarded-for')?.split(',')[0]?.trim() ||
'unknown'
const now = Date.now()
// Drop expired entries so the map doesn't grow unbounded on a long-lived instance.
for (const [key, value] of ipRequestMap) {
if (now >= value.resetAt) ipRequestMap.delete(key)
}
const entry = ipRequestMap.get(ip)
if (entry && now < entry.resetAt) {
if (entry.count >= RATE_LIMIT_MAX) {
return new Response(JSON.stringify({ message: 'Too many requests. Try again later.' }), {
headers: { ...corsHeaders, 'Content-Type': 'application/json' },
status: 429,
})
}
entry.count++
} else {
ipRequestMap.set(ip, { count: 1, resetAt: now + RATE_LIMIT_WINDOW })
}
let body: any
try {
body = await req.json()
} catch {
return new Response(JSON.stringify({ message: 'Invalid JSON body' }), {
headers: { ...corsHeaders, 'Content-Type': 'application/json' },
status: 400,
})
}
const { firstName, lastName, email, company, useCase, details, honeypot } = body
// Anti-spam honeypot: real users never fill this hidden field.
if (honeypot) {
return new Response(JSON.stringify({ message: 'Submission successful' }), {
headers: { ...corsHeaders, 'Content-Type': 'application/json' },
status: 200,
})
}
if (
!isFilledString(firstName, MAX_FIELD_LENGTH) ||
!isFilledString(lastName, MAX_FIELD_LENGTH) ||
!isFilledString(email, MAX_FIELD_LENGTH)
) {
return new Response(JSON.stringify({ message: 'Name and email are required' }), {
headers: { ...corsHeaders, 'Content-Type': 'application/json' },
status: 422,
})
}
if (
!isOptionalString(company, MAX_FIELD_LENGTH) ||
!isOptionalString(details, MAX_DETAILS_LENGTH) ||
!(
useCase === undefined ||
useCase === null ||
useCase === '' ||
USE_CASE_OPTIONS.includes(useCase)
)
) {
return new Response(JSON.stringify({ message: 'Invalid form values' }), {
headers: { ...corsHeaders, 'Content-Type': 'application/json' },
status: 422,
})
}
if (!isValidEmail(email)) {
return new Response(JSON.stringify({ message: 'Invalid email address' }), {
headers: { ...corsHeaders, 'Content-Type': 'application/json' },
status: 422,
})
}
try {
if (!HUBSPOT_PORTAL_ID || !HUBSPOT_FORM_GUID) {
throw new Error('HubSpot credentials not configured')
}
const fields = [
{ objectTypeId: '0-1', name: 'firstname', value: firstName },
{ objectTypeId: '0-1', name: 'lastname', value: lastName },
{ objectTypeId: '0-1', name: 'email', value: email },
...(company ? [{ objectTypeId: '0-1', name: 'company', value: company }] : []),
...(useCase ? [{ objectTypeId: '0-1', name: 'compute_use_case', value: useCase }] : []),
...(details ? [{ objectTypeId: '0-1', name: 'message', value: details }] : []),
]
const response = await fetch(
`https://api.hsforms.com/submissions/v3/integration/submit/${HUBSPOT_PORTAL_ID}/${HUBSPOT_FORM_GUID}`,
{
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
fields,
context: {
pageUri: 'https://supabase.com/compute',
pageName: 'Compute Private Alpha Waitlist',
},
legalConsentOptions: {
consent: {
consentToProcess: true,
text: 'By submitting this form, you acknowledge that Supabase Compute is offered as a private alpha preview, provided on an "as is" and "as available" basis without warranties of any kind. It is intended for internal evaluation only and should not be used to serve production workloads or your own end customers. Supabase may modify, suspend, or discontinue the preview at any time without notice, and there is no guarantee it will become a generally available product. You confirm you are authorized to submit this request on behalf of your organization, and that you have read and understood our Privacy Policy.',
},
},
}),
}
)
if (!response.ok) {
// Read as text: HubSpot doesn't always return JSON on errors, and a parse
// failure here would mask the real status behind the catch-all 500 below.
const errorBody = await response.text()
Sentry.captureException(new Error(`HubSpot form submission failed: ${errorBody}`))
return new Response(JSON.stringify({ message: 'Submission failed. Please try again.' }), {
headers: { ...corsHeaders, 'Content-Type': 'application/json' },
status: response.status,
})
}
return new Response(JSON.stringify({ message: 'Submission successful' }), {
headers: { ...corsHeaders, 'Content-Type': 'application/json' },
status: 200,
})
} catch (error: any) {
Sentry.captureException(error)
return new Response(JSON.stringify({ message: 'Submission failed. Please try again.' }), {
headers: { ...corsHeaders, 'Content-Type': 'application/json' },
status: 500,
})
}
}
+2 -32
View File
@@ -1,7 +1,6 @@
import { FeatureItem, type Feature } from '~/components/FeatureItem'
import SectionContainer from '~/components/Layouts/SectionContainer'
import type { LucideIcon } from 'lucide-react'
import React, { type FC, type ReactNode } from 'react'
import { cn } from 'ui'
import type { FC, ReactNode } from 'react'
interface Props {
id: string
@@ -10,12 +9,6 @@ interface Props {
features: Feature[]
}
type Feature = {
icon: LucideIcon | any
heading: string
subheading: string
}
const Support: FC<Props> = (props) => {
return (
<SectionContainer id={props.id} className="flex flex-col gap-4 md:gap-8">
@@ -32,27 +25,4 @@ const Support: FC<Props> = (props) => {
)
}
interface FeatureItemProps {
feature: Feature
}
const FeatureItem: FC<FeatureItemProps> = ({ feature }) => {
const Icon: LucideIcon = feature.icon
const iconSize = 7
const iconWidth = `w-${iconSize}`
const iconHeight = `h-${iconSize}`
return (
<li className="flex flex-col gap-2 text-sm">
<Icon className={cn('stroke-1 mb-2 text-foreground-lighter', iconWidth, iconHeight)} />
<div className="w-full h-px overflow-hidden flex items-start bg-border-muted">
<span className={cn('h-full bg-foreground-lighter', iconWidth)} />
</div>
<h4 className="text-foreground text-lg lg:text-xl mt-1">{feature.heading}</h4>
<p className="text-foreground-lighter text-sm">{feature.subheading}</p>
{/* <TextLink hasChevron label="Read story" url={feature.url} className="mt-4" /> */}
</li>
)
}
export default Support
+29
View File
@@ -0,0 +1,29 @@
import type { LucideIcon } from 'lucide-react'
import type { FC } from 'react'
export type Feature = {
icon: LucideIcon
heading: string
subheading: string
}
/**
* Icon, hairline rule, heading, subheading — the feature-list item shared by the
* Enterprise support section and the Compute highlights. The rule's leading
* segment is deliberately the same width as the icon, so keep the two `w-7`
* classes in sync if the icon size changes.
*/
export const FeatureItem: FC<{ feature: Feature }> = ({ feature }) => {
const Icon = feature.icon
return (
<li className="flex flex-col gap-2 text-sm">
<Icon className="stroke-1 mb-2 w-7 h-7 text-foreground-lighter" />
<div className="w-full h-px overflow-hidden flex items-start bg-border-muted">
<span className="h-full w-7 bg-foreground-lighter" />
</div>
<h4 className="text-foreground text-lg lg:text-xl mt-1">{feature.heading}</h4>
<p className="text-foreground-lighter text-sm">{feature.subheading}</p>
</li>
)
}
+7 -1
View File
@@ -1,7 +1,7 @@
import { ChevronRight } from 'lucide-react'
import Link from 'next/link'
import React from 'react'
import { cn } from 'ui'
import { Badge, cn } from 'ui'
const MenuItem = React.forwardRef<
React.ElementRef<'a'>,
@@ -10,6 +10,10 @@ const MenuItem = React.forwardRef<
icon?: string
hasChevron?: boolean
hoverColor?: 'foreground' | 'brand'
badge?: {
label: string
variant?: 'default' | 'warning' | 'success' | 'destructive' | 'secondary'
}
}
>(
(
@@ -22,6 +26,7 @@ const MenuItem = React.forwardRef<
hasChevron,
children,
hoverColor = 'foreground',
badge,
...props
},
ref
@@ -68,6 +73,7 @@ const MenuItem = React.forwardRef<
>
{title}
</p>
{badge && <Badge variant={badge.variant}>{badge.label}</Badge>}
{hasChevron && (
<ChevronRight
strokeWidth={2}
+5
View File
@@ -63,6 +63,11 @@ export const MobileMenu = ({ open, setOpen, menu }: Props) => {
description={component.description_short}
icon={component.icon}
onClick={() => setOpen(false)}
badge={
component.name === 'Compute'
? { label: 'Private Alpha', variant: 'default' }
: undefined
}
/>
))}
<div>
@@ -37,6 +37,11 @@ export const ProductDropdown = () => {
icon={product.icon}
className="h-fit p-0"
hasChevron
badge={
product.name === 'Compute'
? { label: 'Private Alpha', variant: 'default' }
: undefined
}
/>
</NavigationMenuLink>
))}
+44
View File
@@ -0,0 +1,44 @@
# Supabase Compute
> Run AI-agent sandboxes and production backends on one runtime, next to your database. Now in Private Alpha.
Supabase Compute is a fully managed compute service built for agentic development. Use it as a sandbox platform offering short-lived, isolated environments to execute code, or as a place to run always-on HTTP services written in any language. Everything runs in the same region and network as your primary Postgres database.
## Key Features
- **Two workload shapes**: ephemeral sandboxes for untrusted code and always-on HTTP services, on one runtime
- **Any language**: Node, Deno, or any Dockerfile in Private Alpha — no wrapper scripts or glue code
- **Next to your database**: same region and network as Postgres, with single-digit-millisecond queries
- **Scale to zero**: idle sandboxes and services suspend automatically and resume in under a second
- **No wall-clock limits**: jobs and pipelines run as long as the work takes
- **Preconfigured trust boundary**: access control via Supabase Auth with short-lived credentials; workloads reach the database and Storage with the permissions carried by their key
- **Per-workload firewalls**: define which external endpoints and ports each sandbox or service can reach
- **Cloaked secrets**: scope secrets per workload; optionally have Supabase inject values only on requests to approved services
- **Automatic patching and dependency scanning**: kernel security patches applied automatically; runtime alerts for newly disclosed vulnerabilities
- **Safe releases**: branching, progressive rollouts, automatic rollback on bad deploys, and auto-scaling
- **Built-in observability**: execution logs, traces, audit logs, and usage metrics in the Supabase dashboard
## Deployment
- Supabase CLI: `supabase compute deploy`
- MCP server: agents provision and manage sandboxes and services programmatically
- Management API: script deployments from any environment
- GitHub Actions: ready-made CI/CD workflows
- Compute skill: teach your coding agent the full surface
## Common Use Cases
- Sandboxes for AI agents executing untrusted code
- Agent frameworks and long-running agent sessions
- Transcription, embedding, and data pipelines without runtime limits
- Always-on HTTP APIs and backend services
- Background jobs and scheduled work
- MCP server hosting
## Availability
Compute is in Private Alpha behind a waitlist. Existing Edge Functions keep running unchanged; because Compute supports the Deno runtime, functions can migrate as-is when you choose.
## Links
- Product page: https://supabase.com/compute
+4
View File
@@ -26,6 +26,10 @@ const footerData = [
text: 'Functions',
url: '/edge-functions',
},
{
text: 'Compute',
url: '/compute',
},
{
text: 'Realtime',
url: '/realtime',
+13
View File
@@ -63,6 +63,19 @@ const MainProducts: ProductType = {
label: '',
url: '/edge-functions',
},
[PRODUCT_SHORTNAMES.COMPUTE]: {
name: PRODUCT_NAMES.COMPUTE,
icon: products.compute.icon[24],
description: (
<>
Run <strong>AI-agent sandboxes and backend services</strong> on one runtime, next to your
database.
</>
),
description_short: 'Sandboxes and backend services',
label: 'Private Alpha',
url: '/compute',
},
[PRODUCT_SHORTNAMES.REALTIME]: {
name: PRODUCT_NAMES.REALTIME,
icon: products.realtime.icon[24],
+63
View File
@@ -2132,6 +2132,7 @@ Supabase's Command Line Interface (CLI) tool provides developers with a powerful
5. Environment management: Handle multiple environments (development, staging, production) efficiently.
6. Seed data management: Populate your database with test data for consistent development and testing.
7. CI/CD integration: Incorporate Supabase operations into your continuous integration and deployment pipelines.
8. Parallel local projects: Run a separate local project for each app, git worktree, or named environment with the experimental \`supabase stack\` commands.
## The CLI is particularly valuable for:
- Development teams working on Supabase projects collaboratively
@@ -2154,6 +2155,68 @@ By leveraging the Supabase CLI, you can significantly improve your development w
availableOnSelfHosted: true,
},
},
{
title: 'Parallel local projects',
subtitle: 'Run a local Supabase project for every app or git worktree.',
description: `
The Supabase CLI can run more than one local Supabase project on the same machine. Each local project belongs to its project directory and git branch. Every app or git worktree gets its own Postgres database, Auth, Storage, and other services, with separate ports and data.
With the default ports, a second \`supabase start\` on the same machine fails with a port conflict. The experimental \`supabase stack\` commands assign ports automatically and keep them stable across restarts. One app can also run several named local projects, such as \`dev\` and \`test\`, so a destructive test run never touches your development data.
## Key benefits
1. One local project per worktree: Run coding agents in separate git worktrees, each against its own database.
2. Automatic ports: The CLI assigns ports from a shared range and keeps them across restarts. Remove the fixed ports from \`config.toml\` once, or create the project with the stack commands turned on.
3. Named environments: Start \`--stack dev\` and \`--stack test\` side by side in one app.
4. Docker or native runtime: Run in containers, or as processes on your machine without Docker on Linux and on macOS on Apple silicon. Use Docker when you run several local projects on one machine.
5. Services start on demand: Postgres starts right away. Other services start on their first request and stop when idle, which keeps idle local projects light.
## Parallel local projects are valuable for:
- Developers running coding agents in git worktrees
- Teams that keep separate development and test databases on one machine
- Freelancers and consultants switching between several client apps
- CI jobs and agent sandboxes that run without a Docker daemon
The \`supabase stack\` commands are experimental. Their interface can change between releases. See the documentation for the full workflow and limitations.
`,
icon: Terminal,
products: [ADDITIONAL_PRODUCTS.PLATFORM],
heroImage: '',
docsUrl: 'https://supabase.com/docs/guides/local-development/running-multiple-local-projects',
slug: 'parallel-local-projects',
status: {
stage: PRODUCT_STAGES.PUBLIC_ALPHA,
availableOnSelfHosted: true,
},
},
{
title: 'Native runtime for local development',
subtitle: 'Run a local Supabase project as processes on your machine, without Docker.',
description: `
The Supabase CLI can run a local Supabase project as native processes instead of containers. Coding agent sandboxes and CI runners often have no Docker daemon. There, \`supabase stack start\` downloads verified service binaries and runs Postgres, Auth, Storage, and the other services directly on the host.
It is the same local project you get from \`supabase start\`, with the same services and most of the same \`config.toml\` settings. The CLI picks Docker when its daemon responds, then Podman, then native. To require one, pass \`--runtime docker\`, \`--runtime podman\`, or \`--runtime native\`.
## Key benefits
1. Works without Docker: Bring up a real local Supabase project in environments that can't run a container engine.
2. Verified binaries: The CLI downloads service archives from Supabase's GitHub releases and checks them before extracting.
3. Same project, same services: Postgres with the full extension set, Auth, PostgREST, Realtime, Storage, Edge Functions, Studio, and more.
## The native runtime is valuable for:
- Coding agents running in sandboxes with no container engine
- CI jobs on runners without a Docker daemon
The native runtime supports Linux on amd64 and arm64 and macOS on Apple silicon. Windows and Intel Macs use Docker. For several local projects on one machine, Docker remains the recommended runtime. The \`supabase stack\` commands are experimental.
`,
icon: Terminal,
products: [ADDITIONAL_PRODUCTS.PLATFORM],
heroImage: '',
docsUrl: 'https://supabase.com/docs/guides/local-development/docker-and-native-runtimes',
slug: 'native-local-runtime',
status: {
stage: PRODUCT_STAGES.PUBLIC_ALPHA,
availableOnSelfHosted: true,
},
},
{
title: 'Management API',
subtitle: 'Manage your projects programmatically.',
+8 -1
View File
@@ -272,7 +272,14 @@ const data: () => {
description: (
<>
Version-control your schema and run the full stack locally with{' '}
<code className="text-xs">supabase start</code>.
<code className="text-xs">supabase start</code>. With the experimental{' '}
<a
href="/docs/guides/local-development/running-multiple-local-projects#turn-on-the-stack-commands"
className="hover:text-foreground underline"
>
stack setting
</a>
, run one local project per app and worktree.
</>
),
icon: SquareTerminal,
+1 -1
View File
@@ -204,7 +204,7 @@ const data: () => {
},
{
name: 'Claude & Cursor',
description: 'AI-powered local development',
description: 'AI-powered local development, one local project per git worktree',
},
{
name: 'Figma',
+1
View File
@@ -12,6 +12,7 @@
"export": "next export",
"start": "next start",
"lint": "eslint .",
"lint:ratchet": "tsx node_modules/eslint-config-supabase/ratchet-eslint-rules.ts --rules-file node_modules/eslint-config-supabase/ratchet-rules.json",
"clean": "rimraf .next .turbo tsconfig.tsbuildinfo",
"pretypecheck": "next typegen",
"typecheck": "pnpm run content:build:core && tsc --noEmit",
+3 -15
View File
@@ -136,22 +136,11 @@ const validateBlogFrontmatterImages = (frontmatter, filePath) => {
}
/**
* Fixes Safari dates sorting bug
* Sort posts by their full publish timestamp to keep same-day posts in the correct order
*/
const sortDates = (a, b, direction = 'desc') => {
const isAsc = direction === 'asc'
var reg = /-|:|T|\+/ //The regex on which matches the string should be split (any used delimiter) -> could also be written like /[.:T\+]/
var parsed = [
//an array which holds the date parts for a and b
a.date.split(reg), //Split the datestring by the regex to get an array like [Year,Month,Day]
b.date.split(reg),
]
var dates = [
//Create an array of dates for a and b
new Date(parsed[0][0], parsed[0][1], parsed[0][2]), //Constructs an date of the above parsed parts (Year,Month...
new Date(parsed[1][0], parsed[1][1], parsed[1][2]),
]
return isAsc ? dates[0] - dates[1] : dates[1] - dates[0] //Returns the difference between the date (if b > a then a - b < 0)
const diff = new Date(a.date).getTime() - new Date(b.date).getTime()
return direction === 'asc' ? diff : -diff
}
/**
@@ -319,7 +308,6 @@ try {
const encodedDescription = xmlEncode(post.description)
const formattedDate = dayjs(post.date)
.utcOffset(0, true)
.startOf('day')
.format('ddd, DD MMM YYYY HH:mm:ss [-0700]')
return `<item>
+1
View File
@@ -55,6 +55,7 @@
"HUBSPOT_ENTERPRISE_FORM_GUID",
"HUBSPOT_PARTNERSHIP_FORM_GUID",
"HUBSPOT_PARTNER_INTAKE_FORM_GUID",
"HUBSPOT_COMPUTE_WAITLIST_FORM_GUID",
"NOTION_FORMS_API_KEY",
"OPENAI_API_KEY",
"EMAIL_REPORT_SLACK_WEBHOOK",
+4 -3
View File
@@ -1,5 +1,5 @@
import dotenv from 'dotenv'
import path from 'path'
import dotenv from 'dotenv'
// Load .env.local before reading process.env
dotenv.config({
@@ -14,13 +14,15 @@ const toBoolean = (value?: string) => {
}
// Default service role key for local development
const DEFAULT_SERVICE_ROLE_KEY = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZS1kZW1vIiwicm9sZSI6InNlcnZpY2Vfcm9sZSIsImV4cCI6MTk4MzgxMjk5Nn0.EGIM96RAZx35lJzdJsyH-qQwv8Hdp7fsn3W0YpN81IU"
const DEFAULT_SERVICE_ROLE_KEY =
'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZS1kZW1vIiwicm9sZSI6InNlcnZpY2Vfcm9sZSIsImV4cCI6MTk4MzgxMjk5Nn0.EGIM96RAZx35lJzdJsyH-qQwv8Hdp7fsn3W0YpN81IU'
export const env = {
STUDIO_URL: process.env.STUDIO_URL || 'http://localhost:8082',
API_URL: process.env.API_URL || 'http://127.0.0.1:54321',
IS_PLATFORM: toBoolean(process.env.IS_PLATFORM || 'false'),
STUDIO_FRAMEWORK: process.env.STUDIO_FRAMEWORK || 'next',
EMAIL: process.env.EMAIL,
PASSWORD: process.env.PASSWORD,
PROJECT_REF: process.env.PROJECT_REF || undefined,
@@ -44,7 +46,6 @@ export const env = {
IS_APP_RUNNING_ON_LOCALHOST:
process.env.STUDIO_URL?.includes('localhost') || process.env.STUDIO_URL?.includes('127.0.0.1'),
SERVICE_ROLE_KEY: process.env.SERVICE_ROLE_KEY || DEFAULT_SERVICE_ROLE_KEY,
}
@@ -1,5 +1,6 @@
import { expect, Page } from '@playwright/test'
import { env } from '../env.config.js'
import { dropTable, query } from '../utils/db/index.js'
import { dismissToastsIfAny } from '../utils/dismiss-toast.js'
import { openTableContextMenu } from '../utils/table-helpers.js'
@@ -199,6 +200,14 @@ test.describe('API Access Toggle', () => {
})
test('shows Manage access link when editing an existing table', async ({ page, ref }) => {
// TODO(tanstack): the entity-types pg-meta query that waitForTableToLoad
// waits on consistently times out (>30s) under the TanStack Start server
// but not under Next — see PR #51141. Re-enable once the root cause is found.
test.skip(
env.STUDIO_FRAMEWORK === 'tanstack',
'pg-meta entity-types query hangs under the TanStack Start server (#51141)'
)
const tableName = `${TABLE_NAME_PREFIX}_edit`
await using _ = await withSetupCleanup(
async () => {
@@ -257,6 +266,14 @@ test.describe('API Access Toggle', () => {
page,
ref,
}) => {
// TODO(tanstack): the entity-types pg-meta query that waitForTableToLoad
// waits on consistently times out (>30s) under the TanStack Start server
// but not under Next — see PR #51141. Re-enable once the root cause is found.
test.skip(
env.STUDIO_FRAMEWORK === 'tanstack',
'pg-meta entity-types query hangs under the TanStack Start server (#51141)'
)
const tableName = `${TABLE_NAME_PREFIX}_preserve_grants`
await using _ = await withSetupCleanup(
async () => {
Loaded 100 of 113 files, more files were not shown because too many files have changed in this diff. Show more