Merge branch 'master' into jordi/debug-73-migrate-reports-queries-to-otel-endpoint

This commit is contained in:
Jordi Enric authored and GitHub committed 2026-08-03 10:41:39 +02:00
commit 6dac1f22ba
1894 files changed
+51500 -19514

No files matched your search

+30 -24
View File
@@ -1,15 +1,21 @@
---
name: vitest
description: Vitest fast unit testing framework powered by Vite with Jest-compatible API. Use when writing tests, mocking, configuring coverage, or working with test filtering and fixtures.
description: >-
Vitest API and config reference (Jest-compatible) — mocking with vi.*, spies,
fake timers, coverage configuration, fixtures, snapshots, and test filtering.
Use for Vitest API and configuration questions anywhere in the monorepo; for
Studio-specific test strategy and component-test setup, start with
studio-testing and studio-mock-api-tests.
metadata:
author: Anthony Fu
version: "2026.1.28"
version: '2026.1.28'
source: Generated from https://github.com/vitest-dev/vitest, scripts located at https://github.com/antfu/skills
---
Vitest is a next-generation testing framework powered by Vite. It provides a Jest-compatible API with native ESM, TypeScript, and JSX support out of the box. Vitest shares the same config, transformers, resolvers, and plugins with your Vite app.
**Key Features:**
- Vite-native: Uses Vite's transformation pipeline for fast HMR-like test updates
- Jest-compatible: Drop-in replacement for most Jest test suites
- Smart watch mode: Only reruns affected tests based on module graph
@@ -22,31 +28,31 @@ Vitest is a next-generation testing framework powered by Vite. It provides a Jes
## Core
| Topic | Description | Reference |
|-------|-------------|-----------|
| Configuration | Vitest and Vite config integration, defineConfig usage | [core-config](references/core-config.md) |
| CLI | Command line interface, commands and options | [core-cli](references/core-cli.md) |
| Test API | test/it function, modifiers like skip, only, concurrent | [core-test-api](references/core-test-api.md) |
| Describe API | describe/suite for grouping tests and nested suites | [core-describe](references/core-describe.md) |
| Expect API | Assertions with toBe, toEqual, matchers and asymmetric matchers | [core-expect](references/core-expect.md) |
| Hooks | beforeEach, afterEach, beforeAll, afterAll, aroundEach | [core-hooks](references/core-hooks.md) |
| Topic | Description | Reference |
| ------------- | --------------------------------------------------------------- | -------------------------------------------- |
| Configuration | Vitest and Vite config integration, defineConfig usage | [core-config](references/core-config.md) |
| CLI | Command line interface, commands and options | [core-cli](references/core-cli.md) |
| Test API | test/it function, modifiers like skip, only, concurrent | [core-test-api](references/core-test-api.md) |
| Describe API | describe/suite for grouping tests and nested suites | [core-describe](references/core-describe.md) |
| Expect API | Assertions with toBe, toEqual, matchers and asymmetric matchers | [core-expect](references/core-expect.md) |
| Hooks | beforeEach, afterEach, beforeAll, afterAll, aroundEach | [core-hooks](references/core-hooks.md) |
## Features
| Topic | Description | Reference |
|-------|-------------|-----------|
| Mocking | Mock functions, modules, timers, dates with vi utilities | [features-mocking](references/features-mocking.md) |
| Snapshots | Snapshot testing with toMatchSnapshot and inline snapshots | [features-snapshots](references/features-snapshots.md) |
| Coverage | Code coverage with V8 or Istanbul providers | [features-coverage](references/features-coverage.md) |
| Test Context | Test fixtures, context.expect, test.extend for custom fixtures | [features-context](references/features-context.md) |
| Concurrency | Concurrent tests, parallel execution, sharding | [features-concurrency](references/features-concurrency.md) |
| Filtering | Filter tests by name, file patterns, tags | [features-filtering](references/features-filtering.md) |
| Topic | Description | Reference |
| ------------ | -------------------------------------------------------------- | ---------------------------------------------------------- |
| Mocking | Mock functions, modules, timers, dates with vi utilities | [features-mocking](references/features-mocking.md) |
| Snapshots | Snapshot testing with toMatchSnapshot and inline snapshots | [features-snapshots](references/features-snapshots.md) |
| Coverage | Code coverage with V8 or Istanbul providers | [features-coverage](references/features-coverage.md) |
| Test Context | Test fixtures, context.expect, test.extend for custom fixtures | [features-context](references/features-context.md) |
| Concurrency | Concurrent tests, parallel execution, sharding | [features-concurrency](references/features-concurrency.md) |
| Filtering | Filter tests by name, file patterns, tags | [features-filtering](references/features-filtering.md) |
## Advanced
| Topic | Description | Reference |
|-------|-------------|-----------|
| Vi Utilities | vi helper: mock, spyOn, fake timers, hoisted, waitFor | [advanced-vi](references/advanced-vi.md) |
| Environments | Test environments: node, jsdom, happy-dom, custom | [advanced-environments](references/advanced-environments.md) |
| Type Testing | Type-level testing with expectTypeOf and assertType | [advanced-type-testing](references/advanced-type-testing.md) |
| Projects | Multi-project workspaces, different configs per project | [advanced-projects](references/advanced-projects.md) |
| Topic | Description | Reference |
| ------------ | ------------------------------------------------------- | ------------------------------------------------------------ |
| Vi Utilities | vi helper: mock, spyOn, fake timers, hoisted, waitFor | [advanced-vi](references/advanced-vi.md) |
| Environments | Test environments: node, jsdom, happy-dom, custom | [advanced-environments](references/advanced-environments.md) |
| Type Testing | Type-level testing with expectTypeOf and assertType | [advanced-type-testing](references/advanced-type-testing.md) |
| Projects | Multi-project workspaces, different configs per project | [advanced-projects](references/advanced-projects.md) |
+49 -21
View File
@@ -1,42 +1,70 @@
# Supabase Monorepo
pnpm 10 + Turborepo monorepo. Requires Node >= 22.
pnpm 11 + Turborepo monorepo. Requires Node >= 22.13.
## Structure
| Directory | Purpose |
| ----------------- | ------------------------------------------------------------ |
| `apps/studio` | Supabase Studio/Dashboard — Next.js (pages router), React 19 |
| `apps/docs` | Documentation site |
| `apps/www` | Marketing website |
| `packages/ui` | Shared UI components (shadcn/ui based) |
| `packages/common` | Shared utilities and telemetry constants |
| `e2e/studio` | Playwright E2E tests for Studio |
| Directory | Purpose |
| ------------------------ | --------------------------------------------------------------------------- |
| `apps/studio` | Supabase Studio/Dashboard — has its own `apps/studio/CLAUDE.md` (see below) |
| `apps/docs` | Documentation site — Next.js app router, MDX (port 3001) |
| `apps/www` | Marketing website — Next.js, app + pages (port 3000) |
| `apps/design-system` | Component demos — source of truth for Studio UI patterns (port 3003) |
| `apps/ui-library` | shadcn-style registry site for Supabase UI blocks (port 3004) |
| `apps/lite-studio` | Lightweight Studio — different stack: React Router 7 + Vite + Tailwind v4 |
| `packages/ui` | Shared UI components (shadcn/ui based) — `import { Button } from 'ui'` |
| `packages/ui-patterns` | Composite components — subpath imports, e.g. `ui-patterns/AssistantChat` |
| `packages/common` | Shared utils, telemetry constants, feature flags |
| `packages/api-types` | Generated platform Management API types |
| `packages/pg-meta` | SQL builders for Postgres introspection (`SafeSqlFragment`) |
| `packages/shared-data` | Static data: pricing, plans, regions, error codes |
| `e2e/studio`, `e2e/docs` | Playwright E2E tests |
| `supabase/` | Local Supabase project: edge functions, migrations, config.toml |
## Common Commands
```bash
pnpm install # install dependencies
pnpm dev:studio # run Studio dev server
pnpm test:studio # run Studio unit tests (vitest)
pnpm --prefix e2e/studio run e2e # run Studio E2E tests (playwright)
pnpm build --filter=studio # build Studio
pnpm lint --filter=studio # lint Studio
pnpm typecheck # typecheck all packages
pnpm dev:studio # run Studio dev server → http://localhost:8082
pnpm dev:docs # run docs dev server
pnpm dev:www # run www dev server
pnpm test:studio # Studio unit tests (vitest)
pnpm e2e # Studio E2E tests (playwright)
pnpm build --filter=studio # build Studio
pnpm lint --filter=studio # lint Studio
pnpm typecheck # typecheck all packages
pnpm format # Prettier write (check: pnpm test:prettier)
pnpm generate:types # local DB types → supabase/functions/common/database-types.ts
pnpm api:codegen # platform Management API types → packages/api-types
```
## 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.
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`.
## Conventions
**UI** — import from `'ui'`, use `_Shadcn_` suffixed variants for form primitives. Check `packages/ui/index.tsx` before creating new primitives.
**UI** — import from `'ui'`; primitives are shadcn/ui-based and exported unsuffixed (`Input`, `Select`, `Form`, …). Use `Button` — the in-house component and the standard everywhere (a raw shadcn `Button_Shadcn_` also exists but is rarely the right choice). Check `packages/ui/index.tsx` before creating new primitives. Higher-level patterns live in `packages/ui-patterns`.
**Styling** — Tailwind only, semantic tokens (`bg-muted`, `text-foreground-light`), no hardcoded colors.
**Exports** — named exports only; default exports are allowed only where a framework requires them (`pages/**`, `app/**`, config files — the eslint preset has the exact carve-out list). Lint-enforced across all apps via `eslint-config-supabase` (severity `warn` everywhere; hard-enforced in Studio by the lint ratchet).
**Language** — Use U.S. English everywhere.
**Studio shortcuts** — when adding or changing repeated Studio UI actions, use the shared shortcut registry and primitives in `apps/studio/state/shortcuts/` and `apps/studio/components/ui/Shortcut*.tsx`. Prefer registered, discoverable shortcuts over one-off keyboard listeners; keep `G then ...` chords for navigation.
## Skills
The skills in `.claude/skills/` are the source of truth for conventions — load the relevant ones before working, don't guess:
- `copywriting` — any user-facing text, anywhere in the monorepo
- `docs-content` — anything under `apps/docs`
- `telemetry-standards` — PostHog events, `packages/common/telemetry-constants.ts`
- `dev-toolbar-review` — `packages/dev-tools`, `packages/common/posthog-client.ts`, `packages/common/feature-flags.tsx`
- `safe-sql-execution` — any code that builds or executes SQL against user databases
- `react-hook-form` — writing or modifying any form code, anywhere in the monorepo
- `vitest` / `vercel-composition-patterns` — generic unit-testing and React composition references
## Studio
Pages router. Co-locate sub-components with parent. Avoid barrel re-export files.
See studio-\* skills for detailed studio conventions.
Before working on anything in `apps/studio`, read `apps/studio/CLAUDE.md` if it isn't already in context — it maps Studio tasks to required skills and covers the TanStack Start migration rules.
+10
View File
@@ -1,4 +1,14 @@
{
"permissions": {
"deny": [
"Edit(packages/api-types/types/**)",
"Edit(**/routeTree.gen.ts)",
"Edit(**/__generated__/**)",
"Edit(apps/docs/features/docs/generated/**)",
"Edit(apps/www/.generated/**)",
"Edit(supabase/functions/common/database-types.ts)"
]
},
"hooks": {
"SessionStart": [
{
+1 -1
View File
@@ -1,6 +1,6 @@
---
name: copywriting
description: Write or audit UI copy (buttons, labels, empty states, error messages, tooltips, form text) anywhere in the monorepo. Always check this before shipping or reviewing user-facing text.
description: Write or audit UI copy (buttons, labels, empty states, error messages, tooltips, form text) anywhere in the monorepo. Load it before shipping or reviewing any user-facing text — including when copy is incidental to the task, like a new feature that adds buttons, toasts, dialogs, or validation messages.
---
# Copywriting
+12 -1
View File
@@ -1,6 +1,7 @@
---
name: dev-toolbar-review
description: Use when reviewing PRs that touch packages/dev-tools/, packages/common/posthog-client.ts,
description: Safety rules for the dev toolbar, PostHog client, and feature flags. Use
when writing or reviewing any change to packages/dev-tools/, packages/common/posthog-client.ts,
or packages/common/feature-flags.tsx. Covers environment guards, flag override cookies,
telemetry event subscription, and SSE stream safety.
---
@@ -30,10 +31,12 @@ so PRs touching only those files won't auto-request review. Watch for these in t
**Files:** `packages/dev-tools/index.ts`, `DevToolbar.tsx`, `DevToolbarTrigger.tsx`, `DevToolbarContext.tsx`
The toolbar uses two layers of protection:
- **Build-time tree-shaking** in `index.ts`: `process.env.NODE_ENV !== 'development'` ternaries that replace components with noops/stubs so the implementation is eliminated from production bundles.
- **Runtime guards** in components: `IS_LOCAL_DEV` checks — `DevToolbar` and `DevToolbarTrigger` return `null` to hide themselves, while `DevToolbarProvider` passes children through (`<>{children}</>`) to preserve the component tree.
**Check for:**
- Guards being removed or broadened. The toolbar is expanding to staging and preview deploys but must remain invisible in production.
- Tree-shaking ternaries in `index.ts` staying intact — these are the primary production safety mechanism.
- New components or exports that bypass the existing guard pattern.
@@ -43,14 +46,17 @@ The toolbar uses two layers of protection:
**Files:** `packages/dev-tools/DevToolbar.tsx`, `packages/common/posthog-client.ts`, `packages/common/feature-flags.tsx`
The toolbar writes two cookies that override feature flags locally:
- `x-ph-flag-overrides` — PostHog flag overrides
- `x-cc-flag-overrides` — ConfigCat flag overrides
These are read by:
- `posthog-client.ts:getFeatureFlag()` — checks the PostHog override cookie before querying the SDK
- `feature-flags.tsx` — merges both override cookies into the flag store during initialization
**Check for:**
- Cookie name changes (must stay in sync across writer and all readers)
- Changes to the merge/precedence logic in `feature-flags.tsx` (currently: `vercel-flag-overrides` first, then `x-cc-flag-overrides` takes precedence in local dev)
- Override cookies being read outside the `IS_LOCAL_DEV` / `isLocalDev` guard — overrides must never affect production flag evaluation
@@ -66,6 +72,7 @@ and `identify`. Note: `captureExperimentExposure` calls `posthog.capture()` dire
without emitting to dev listeners — experiment exposure events are invisible in the toolbar.
**Check for:**
- Changes to `emitToDevListeners` or `subscribeToEvents` that could introduce side effects on the actual capture path (e.g., throwing errors, blocking, mutating event data)
- The listener set (`devListeners`) being iterated synchronously in a way that could delay event dispatch
- New PostHog client methods that capture events but don't call `emitToDevListeners` (gap in toolbar visibility)
@@ -78,6 +85,7 @@ The toolbar connects to `${apiUrl}/telemetry/stream` via Server-Sent Events to d
server-side telemetry. Uses exponential backoff on connection errors.
**Check for:**
- Changes to the SSE endpoint URL or `session_id` cookie handling
- Reconnection logic changes that could cause excessive retries or connection leaks
- Note: the stream endpoint lives in the platform repo — cross-repo changes need coordinated review
@@ -85,16 +93,19 @@ server-side telemetry. Uses exponential backoff on connection errors.
### 5. App-Level Mounting
**Provider + toolbar panel** (`DevToolbarProvider`, `DevToolbar`):
- `apps/studio/pages/_app.tsx`
- `apps/www/pages/_app.tsx`, `apps/www/app/providers.tsx`
- `apps/docs/features/app.providers.tsx`
**Trigger button** (`DevToolbarTrigger`) — rendered separately in nav/header components:
- `apps/studio/components/layouts/Navigation/LayoutHeader/LayoutHeader.tsx`
- `apps/www/components/Nav/index.tsx`
- `apps/docs/components/Navigation/NavigationMenu/TopNavBar.tsx`
**Check for:**
- Provider being added or removed from an app
- `apiUrl` prop changes (must point to the correct platform API)
- Rendering order changes that could affect the toolbar's access to PostHog context
+97
View File
@@ -0,0 +1,97 @@
---
name: docs-content
description: Write, edit, organize, and review Supabase content anywhere in apps/docs — guides, explainers, tutorials, troubleshooting entries, reference docs, and partials. Use for MDX/TOML authoring, frontmatter, navigation, terminology, links, code samples, content listings, and docs validation.
---
# Supabase docs authoring
## Sources of truth
Before changing docs content:
1. Read `apps/docs/CONTRIBUTING.md` for content types, structure, components, and
style.
2. Read `apps/docs/WORD_LIST.md` for preferred terminology, spelling, and
capitalization.
3. Inspect nearby content of the same type and the relevant navigation section
before deciding on file placement or structure. Guides, explainers, and
tutorials live under `apps/docs/content/guides`. Troubleshooting entries live
under `apps/docs/content/troubleshooting` and use TOML frontmatter — follow
`_template.mdx` in that directory rather than a guide's YAML frontmatter.
Reference docs are generated from `apps/docs/spec` and library source, so
look for the spec file or repo definition instead of editing rendered output
directly.
When guidance conflicts, follow `apps/docs/CONTRIBUTING.md`. Match literal code,
API names, UI labels, and third-party product names even when they differ from the
word list.
## Writing workflow
1. Identify the document type: explainer, tutorial, guide, or reference, per
`apps/docs/CONTRIBUTING.md`. A guide is a concise procedure for a targeted
task; a tutorial covers a larger goal and includes more explanatory context;
an explainer is conceptual and prose-based; reference content is factual,
like a dictionary entry. Troubleshooting entries follow their own TOML
structure rather than these four types.
2. Define the reader's goal and prerequisites before drafting.
3. Classify substantial sections as contextual, procedural, or reference content.
In a mixed page, group sections by information type so that context doesn't
interrupt the procedural path.
4. For a long or mixed page, add a short introduction that links to its major
section groups and tells readers when to use each one. Skip this navigation
when a short page is already easy to scan.
5. Connect contextual sections to their corresponding procedures when useful.
Add introductions to section groups, transitions between information types,
and outcomes after procedures. Don't link every adjacent section.
6. Use second person, present tense, short paragraphs, and ordered steps for
sequential actions.
7. Search `apps/docs/WORD_LIST.md` when introducing or reviewing technical terms,
UI actions, abbreviations, and potentially ambiguous language.
8. Keep code samples executable in their stated context and consistent with
repository formatting. Clearly mark intentionally omitted code. Use lowercase
SQL keywords.
9. Reuse repeated content through `apps/docs/content/_partials` instead of copying
it.
10. Add new guide, explainer, and tutorial pages to
`apps/docs/components/Navigation/NavigationMenu/NavigationMenu.constants.ts`.
File placement alone doesn't add a page to navigation. Troubleshooting
entries are indexed automatically and don't need a navigation entry.
11. Use `/docs/...` paths for pages in Supabase docs and site-root paths such as
`/dashboard` for pages outside docs. Use descriptive link text and sparse
admonitions with the appropriate severity.
## Validation
From `apps/docs`, run:
```bash
pnpm lint:mdx
pnpm build:guides-markdown
```
`pnpm lint:mdx` covers all content under `apps/docs/content`, including
troubleshooting entries. `pnpm build:guides-markdown` only applies to guides,
explainers, and tutorials.
From the repository root, run `pnpm format` to apply Prettier to any changed
MDX (and other) files. This enforces repo-wide formatting rules, including
lowercase SQL keyword casing in code samples.
Run broader type checking or tests when the change affects MDX components,
content listings, navigation code, or generated output.
For a mixed page, verify that context and procedures are grouped, introductory
navigation links resolve to the intended sections, related context and procedures
are cross-referenced where useful, and transitions make the reading path clear.
Treat lint replacements as suggestions when context matters. Rewrite the sentence
instead of applying a replacement that changes its technical meaning.
Anchor IDs are generated from heading text at render time, and nothing in CI
checks that `#anchor` links still resolve. Before renaming, removing, or
substantially rewording a heading, run
`grep -rn "#<old-anchor-slug>" apps/docs/content` to find in-page and
cross-file links that target it, and update every match. If a heading needs a
stable anchor independent of its wording, pin it with a custom anchor, for
example `## Some heading [#some-heading]`.
+284
View File
@@ -0,0 +1,284 @@
---
name: react-hook-form
description: Correct React Hook Form usage anywhere in the monorepo — data flow, subscriptions,
reset, dirty state, number inputs, and controlled-input rules. Load this BEFORE
writing or modifying ANY form code, adding a field to an existing form, touching
watch/useWatch/formState/getValues/setValue/reset, wiring a form into a dialog or
sheet, or building a submit/cancel footer — even when the change looks trivial.
The codebase contains widespread RHF anti-patterns; without this skill you will
copy them. For form layout and which components to use, also load
studio-ui-patterns.
---
# React Hook Form
How to write forms that stay correct as they grow. The existing codebase is **not**
a safe reference: `form.watch()` off prop-drilled form objects, subscription-only
watches, unguarded `valueAsNumber`, and `?? undefined` controlled values are all
common in older code and all wrong. Follow this skill, not the neighboring file.
**Policy — fix what you touch.** New code must follow these rules. When you modify
existing form code, upgrade the specific fields/hooks/components you're editing to
match (e.g. a component you touch that calls `form.watch` gets converted to
`useWatch`). Leave untouched code alone, but tell the user about anti-patterns you
noticed and didn't fix. Never add new violations: `react-hook-form/no-use-watch`
is ratcheted in Studio CI — any increase in the warning count fails the build.
## Mental model: subscriptions decide who re-renders
RHF is uncontrolled at heart. Values live in refs; nothing re-renders unless a
subscription says so. Every read API is a subscription decision:
| API | Subscribes | Re-renders | Use for |
| ----------------------------- | ---------- | -------------------------- | ---------------------------------------------- |
| `useWatch({ control, name })` | yes | only the calling component | reactive value reads, anywhere |
| `useFormState({ control })` | yes | only the calling component | `isDirty`/`errors`/etc. outside the form owner |
| `formState` (destructured) | yes | the `useForm` owner | form state **in the owner component only** |
| `form.watch(name)` | yes | the **entire form tree** | avoid — lint-flagged, see below |
| `getValues()` | no | never | event handlers and `onSubmit` only |
| `subscribe()` | callback | none | side effects outside render |
Two facts explain most of the bugs we've shipped:
1. **`form.watch()` and `form.formState` hoist their subscription to the `useForm`
owner**, no matter which component calls them. A child that reads
`form.watch('x')` off a prop works today only because the whole tree re-renders
on every change — it silently goes stale the moment anyone adds `React.memo`
between owner and child, and until then it re-renders every sibling on every
keystroke. A no-arg `form.watch()` sets `watchAll` and re-renders the tree on
every field change for the life of the form.
2. **`formState` is a Proxy** — reading a property is what arms the subscription.
Destructure it (`const { isDirty } = form.formState`), never pass the object
around or read it conditionally (`a && formState.isValid` may never subscribe).
Enforced by `react-hook-form/destructuring-formstate` (error).
### Reading values, by location
- **In the component that owns `useForm`:** destructure `formState`; prefer
`useWatch` over `form.watch` even here (the `no-use-watch` rule flags every
`watch`, and `useWatch` scopes the re-render if the JSX is later extracted).
- **In any child component or custom hook:** accept `control` (not the whole
`form`) and use `useWatch({ control, name })` / `useFormState({ control })`.
Inside `<Form {...form}>` (which _is_ `FormProvider`), `useFormContext()` +
`useWatch({ name })` also works and avoids prop-drilling entirely.
- **Consume the return value.** Never call a watch for its subscription side
effect and then read via `getValues()` — the watch list and the read list will
drift apart (it has already happened; fields silently lost reactivity). The
value you render must _be_ the value you subscribed to.
- **One read path per value per render.** Mixing `useWatch('x')` on one line and
`getValues('x')` a few lines later lets the two disagree within a single render.
- **Name what you watch.** `useWatch({ control })` with no `name` re-renders on
every keystroke in every field. Subscribe to the specific names you use.
- `watch(callback)` is deprecated — use `subscribe()` for render-free listeners,
and always return its cleanup from `useEffect`.
```tsx
// ❌ common in the codebase — all three subscriptions hoist to the form owner
function Fields({ form }: { form: UseFormReturn<FormValues> }) {
form.watch(['storageType', 'totalSize']) // return value discarded
const { errors } = form.formState // prop-form formState
const size = form.getValues('totalSize') // non-reactive read in render
...
}
// ✅ child subscribes for itself and consumes what it watches
function Fields({ control }: { control: Control<FormValues> }) {
const [storageType, totalSize] = useWatch({ control, name: ['storageType', 'totalSize'] })
const { errors } = useFormState({ control })
...
}
```
## The canonical form
zod schema → `z.infer` type → `useForm` with `zodResolver` and **complete**
`defaultValues` → `<Form {...form}>` → `FormField` render-prop per field →
`FormItemLayout` → `FormControl` → primitive from `ui`. Layout/container choices
(Card vs Sheet, `layout=` variants) are covered by the `studio-ui-patterns` skill
and the demos in `apps/design-system/registry/default/example/`
(`form-patterns-pagelayout.tsx`, `form-patterns-sidepanel.tsx`) — check them
before inventing structure.
```tsx
// Module level — static references, not recreated on every render
const FORM_ID = 'pool-config-form'
const FormSchema = z.object({
name: z.string().min(1, 'Name is required'),
maxConnections: z
.union([z.literal(''), z.coerce.number().gte(1, 'Must be at least 1')])
.refine((v) => v !== '', 'Max connections is required'),
})
type FormValues = z.infer<typeof FormSchema>
const defaultValues: FormValues = { name: '', maxConnections: '' }
// Inside the component
const form = useForm<FormValues>({
resolver: zodResolver(FormSchema),
defaultValues,
})
<Form {...form}>
<form id={FORM_ID} onSubmit={form.handleSubmit(onSubmit)}>
<FormField
control={form.control}
name="name"
render={({ field }) => (
<FormItemLayout layout="horizontal" label="Name">
<FormControl>
<Input {...field} />
</FormControl>
</FormItemLayout>
)}
/>
</form>
</Form>
```
Define the schema, `type`, static `defaultValues`, and the form's id at module
level, outside the component. Rebuilding them per render is wasted work and
unstable references — RHF reads `defaultValues` only on the first render, but
anything else comparing against these objects sees a fresh identity each time.
When they genuinely depend on runtime data, build the schema with `useMemo` and
feed server-driven defaults through the `values` option (next section) instead
of hoisting.
Submit buttons living outside the `<form>` (sheet/dialog footers) use the same
module-level `FORM_ID` via `form={FORM_ID}` on the button. A module-level id is
only safe for singleton forms — if the component can mount more than once at a
time, duplicate ids make external buttons submit the first matching form, so
mint a per-instance id with `useId()` and share it between the `<form>` and its
buttons.
## defaultValues, server data, and reset
- **Provide a complete `defaultValues` object — every field, no `undefined`.**
`isDirty`, `dirtyFields`, and Cancel-reset all compare against it; a missing or
`undefined` default breaks all three, and `undefined` also makes React treat the
input as uncontrolled (see below).
- **Form populated from an API? Use the `values` option, not a hand-rolled
effect.** `values` reacts to the query resolving and resets the form for you;
computing `defaultValues` from a query that may not have loaded freezes whatever
happened to be in cache at mount. Add
`resetOptions: { keepDirtyValues: true }` when a background refetch must not
clobber the user's in-progress edits. (Good examples:
`components/interfaces/Settings/Database/ConnectionLogging.tsx`,
`components/interfaces/Storage/EditBucketModal.tsx`.)
- **After a successful mutation, re-baseline the form** in `onSuccess` so the
saved state becomes the new baseline (`isDirty` returns to false, Cancel now
reverts to the saved values). Prefer what the server actually persisted: if the
form uses `values` and the mutation invalidates the query, the refetch handles
this for you; if the mutation returns the updated resource, `reset(response)`.
`reset(submittedValues)` is the fallback for APIs that store exactly what was
sent — if the server normalizes or fills values, it baselines the form to data
that was never saved. A bare `reset()` reverts to the _previous_ defaults —
wrong after a save.
- Cancel buttons call `form.reset()`. This only visually restores fields whose
values round-trip through defined, controlled values — which is why the null
rules below matter.
## Controlled inputs: never let `value` flip to `undefined`
React decides controlled vs uncontrolled per render from whether `value` is
defined. A field whose value can be `undefined` (or becomes `undefined` on reset)
flips modes: console warnings, and — worse — `reset()` stops clearing the visible
text because React abandoned the DOM value. `value={field.value ?? undefined}` is
a bug, not a fix.
- Text fields: default to `''`, never `null`/`undefined`.
- **Normalize `null` from the API at the form boundary** (`growthPercent ?? ''`
when building defaults) and convert back on submit (`'' → null`). Do not paper
over a `null` default with a `placeholder` that looks like a value: the user
sees "50", the form holds `null`, and every downstream comparison
(`defaultValues.growthPercent !== watched` → `null !== 50`) reports a permanent
phantom change while Cancel silently fails to reset the field.
- Selects/radios: default to `''` or a real option value; checkboxes/switches to
`false`.
## Number inputs
The blessed pattern keeps `''` as the "empty" sentinel so the input stays
controlled, and lets zod coerce on validation (see `maxConnections` above):
`z.union([z.literal(''), z.coerce.number()...]).refine((v) => v !== '', '…')`
with a plain `<Input {...field} type="number" />`.
If you instead wire `onChange` through `e.target.valueAsNumber` (or
`valueAsNumber: true`), an empty or partially-typed input produces `NaN`, which
lands in form state and propagates into every calculation, price preview, and
`value` attribute downstream. Guard it with the **same empty sentinel the
field's schema declares** — with the `''`-union schema above:
`field.onChange(Number.isNaN(e.target.valueAsNumber) ? '' : e.target.valueAsNumber)`.
Never let `NaN` into form state.
A nullable API field (`null` = "unset", e.g. a platform default applies)
doesn't change the in-form sentinel — keep `''` inside the form and convert at
the boundaries:
```tsx
// inbound: null → '' when building defaults/values
values: { growthPercent: data.growth_percent ?? '' },
// schema: '' stays the in-form sentinel, zod coerces real input
growthPercent: z.union([z.literal(''), z.coerce.number().gte(10).lte(100)]),
// outbound: '' → null in onSubmit
mutate({ growth_percent: values.growthPercent === '' ? null : values.growthPercent })
```
If `null` does end up in form state (some existing forms hold it), keep it out
of both the input and the coercion: render via `value={field.value ?? ''}`, and
don't pass the value through `z.coerce.number()` — `Number(null)` is `0`, so a
nullable field fed into the coercing union silently validates empty as `0`.
Either way it's one sentinel per field, used consistently across defaults,
schema, `onChange`, rendering, and the submit mapping.
## Dirty state and change detection
- Gate Save on `isDirty`; show Cancel only when dirty. In the owner, destructure
from `form.formState`; anywhere else, `useFormState({ control })`.
- When the form lives in a Sheet or Dialog, also wire dirty dismissal:
`useConfirmOnClose` + `DiscardChangesConfirmationDialog`. Route Cancel,
Escape, and backdrop through the guard; call the raw `onClose` on successful
submit so you do not prompt after save. Details:
`apps/design-system/content/docs/ui-patterns/modality.mdx` (Dirty form
dismissal) and the studio-ui-patterns skill Sheets section.
- To show _which_ fields changed (review/summary dialogs), read `dirtyFields`
from the same subscription instead of hand-comparing
`defaultValues.x !== watchedX`. RHF already does that comparison correctly;
hand-rolled versions break on the null-vs-placeholder mismatch and must be
kept in sync with the watch list by hand.
- `setValue` outside user input needs explicit flags:
`setValue('x', v, { shouldDirty: true, shouldValidate: true })` — otherwise the
change is invisible to `isDirty` and validation.
## Disabling and gating
If a field must not be edited (plan tier, permissions, cooldown), disable the
field itself — a notice next to an editable input gates nothing. Wire the same
condition into both the notice and the control. Permission checks come from
`useAsyncCheckPermissions`; disabled buttons that need an explanation use
`ButtonTooltip`.
Caution: `register`/`useController` `disabled: true` removes the field's value
from submission data. For "visible but locked" fields whose value must survive
submit, use the input's own `disabled`/`readOnly` prop (as `FormField` +
primitive props do) rather than RHF-level disabling, or the form-level
`disabled` option to freeze everything during async work.
## Submit and mutations
`onSubmit` receives validated, typed data — trust it; don't re-read via
`getValues()`. Mutations follow Studio conventions: `onSuccess` → `toast.success`
- `reset(values)` (or query invalidation when using `values:`), `onError` →
`toast.error`; pass the mutation's `isPending` to the button's `loading` prop.
Default validation `mode: 'onSubmit'` is right for most forms — pick another mode
deliberately, not by copying.
## Lint rules in force (Studio)
| Rule | Level | Meaning |
| ------------------------------------------- | ---------------- | ------------------------------------------------ |
| `react-hook-form/destructuring-formstate` | error | destructure `formState`, never hold the object |
| `react-hook-form/no-access-control` | error | don't reach into `control` internals |
| `react-hook-form/no-nested-object-setvalue` | error | `setValue('a.b', v)`, not `setValue('a', {b:v})` |
| `react-hook-form/no-use-watch` | warn (ratcheted) | use `useWatch`, not `watch` |
+15 -1
View File
@@ -1,6 +1,20 @@
---
name: safe-sql-execution
description: Safely execute SQL queries against a user database without risking SQL injection or other security vulnerabilities.
description: >-
Use whenever code will build, return, fetch, or execute SQL that runs against
a user's real Postgres database — even when the request reads like an ordinary
feature or bug fix and never says "security," "injection," or
"SafeSqlFragment." This covers: writing or editing any pg-meta function, query
builder, or endpoint that builds/returns SQL for database objects (tables,
views, functions, DB triggers, indexes, RLS policies); interpolating a
schema/table/column/search/route-param value into SQL text; storing, fetching,
or re-running SQL that round-trips from the database (a policy's definition, a
function/view definition, a snippet's saved content); and any
"Run"/"Apply"/"Execute" action that sends SQL to a project's database (SQL
editor run-selection, policy editor apply, snippet runner). Load this BEFORE
writing such code, not only when reviewing a finished diff. Skip only for
changes that never touch SQL text or execution — styling, unrelated data
hooks, non-SQL form validation, or UI layout work.
---
# Safe SQL execution
@@ -1,175 +0,0 @@
---
name: studio-best-practices
description: React and TypeScript best practices for Supabase Studio. Use when writing
or reviewing Studio components — covers boolean naming, component structure, loading/error
states, state management, custom hooks, event handlers, conditional rendering,
performance, and TypeScript conventions.
---
# Studio Best Practices
Applies to `apps/studio/**/*.{ts,tsx}`.
## Boolean Naming
Use descriptive prefixes — derive from existing state rather than storing separately:
- `is` — state/identity: `isLoading`, `isPaused`, `isNewRecord`
- `has` — possession: `hasPermission`, `hasData`
- `can` — capability: `canUpdateColumns`, `canDelete`
- `should` — conditional behavior: `shouldFetch`, `shouldRender`
Extract complex conditions into named variables:
```tsx
// ❌ inline multi-condition
{
!isSchemaLocked && isTableLike(selectedTable) && canUpdateColumns && !isLoading && <Button />
}
// ✅ named variable
const canShowAddButton =
!isSchemaLocked && isTableLike(selectedTable) && canUpdateColumns && !isLoading
{
canShowAddButton && <Button />
}
```
Derive booleans — don't store them:
```tsx
// ❌ stored derived state
const [isFormValid, setIsFormValid] = useState(false)
useEffect(() => {
setIsFormValid(name.length > 0 && email.includes('@'))
}, [name, email])
// ✅ derived
const isFormValid = name.length > 0 && email.includes('@')
```
## Component Structure
See `vercel-composition-patterns` skill for compound component and composition patterns.
Keep components under 200–300 lines. Split when you see:
- Multiple distinct UI sections
- Complex conditional rendering
- Multiple unrelated `useState` calls
- Hard to understand at a glance
Co-locate sub-components in the same directory as the parent. Avoid barrel re-export files.
Extract repeated JSX patterns into small components.
## Data Fetching
All data fetching uses TanStack Query (React Query). See `studio-queries` skill for query/mutation patterns and `studio-error-handling` skill for error display conventions.
### Loading / Error / Success Pattern
Top level:
```tsx
const { data, error, isLoading, isError, isSuccess } = useQuery(...)
if (isLoading) return <GenericSkeletonLoader />
if (isError) return <AlertError error={error} subject="Failed to load data" />
if (isSuccess && data.length === 0) return <EmptyState />
return <DataDisplay data={data} />
```
Use early returns — avoid deeply nested conditionals.
Inline:
```tsx
<div>
{isLoading && <InlineLoader />}
{isError && <InlineError error={error} />}
{isSuccess && data.length === 0 && <EmptyState />}
{isSuccess && data.length > 0 && <DataDisplay data={data} />}
</div>
```
## State Management
Keep state as local as possible; lift only when needed.
Group related form state with `react-hook-form` rather than multiple `useState` calls. See `studio-ui-patterns` skill for form layout and component conventions.
```tsx
// ❌ multiple related useState
const [name, setName] = useState('')
const [email, setEmail] = useState('')
// ✅ grouped with react-hook-form
const form = useForm<FormValues>({ defaultValues: { name: '', email: '' } })
```
## Custom Hooks
Extract complex or reusable logic into hooks. Return objects, not arrays:
```tsx
// ❌ array return (hard to extend)
return [value, toggle]
// ✅ object return
return { value, toggle, setTrue, setFalse }
```
## Event Handlers
- Prop callbacks: `on` prefix (`onClose`, `onSave`)
- Internal handlers: `handle` prefix (`handleSubmit`, `handleCancel`)
Use `useCallback` for handlers passed to memoized children; avoid unnecessary inline arrow functions.
## Conditional Rendering
```tsx
// Simple show/hide
<>{isVisible && <Component />}</>
// Binary choice
<>{isLoading ? <Spinner /> : <Content />}</>
// Multiple conditions — use early returns, not nested ternaries
if (isLoading) return <Spinner />
if (isError) return <Error />
return <Content />
```
## Performance
`useMemo` for genuinely expensive computations (measured, not assumed). Don't wrap everything — only optimize when you have a measured problem or are passing values to memoized children.
## TypeScript
Define prop interfaces explicitly. Use discriminated unions for complex state:
```tsx
type AsyncState<T> =
| { status: 'idle' }
| { status: 'loading' }
| { status: 'success'; data: T }
| { status: 'error'; error: Error }
```
Avoid `as any` / `as Type` casts. Validate at boundaries with zod:
```tsx
// ❌ type cast
const user = apiResponse as User
// ✅ zod parse
const user = userSchema.parse(apiResponse)
// or safe:
const result = userSchema.safeParse(apiResponse)
```
## Testing
Extract logic into `.utils.ts` pure functions and test exhaustively. See the `studio-testing` skill for the full testing strategy and decision tree.
+4 -4
View File
@@ -1,9 +1,9 @@
---
name: studio-e2e-tests
description: Write and run Playwright E2E tests for Supabase Studio. Use when asked
to run e2e tests, write new E2E tests, or debug flaky tests. Covers running commands,
avoiding race conditions, waiting strategies, selectors, helper functions, and CI
vs local differences.
description: Write and run Playwright E2E tests for Supabase Studio (e2e/studio).
Use when asked to run e2e tests, write new E2E tests, or debug flaky or failing
Playwright tests. Covers running commands, avoiding race conditions, waiting
strategies, selectors, helper functions, and CI vs local differences.
---
# E2E Studio Tests
@@ -1,8 +1,9 @@
---
name: studio-error-handling
description: Error display and troubleshooting pattern for Supabase Studio. Use when
rendering API errors in the UI, adding inline troubleshooting steps for a new
error type, or wiring up the AI assistant debug button from an error state.
showing a failed API request or query error in the UI (AlertError, toast, inline
message), adding troubleshooting steps for a new error type, or wiring up the AI
assistant debug button from an error state.
---
# Studio Error Handling Pattern
+2 -1
View File
@@ -1,7 +1,8 @@
---
name: studio-queries
description: React Query conventions for data fetching in Supabase Studio. Use when
writing or reviewing query hooks, mutation hooks, or query keys in apps/studio/data/.
writing or reviewing query hooks, mutation hooks, or query keys in apps/studio/data/
— including adding the first fetch or mutation for a new API endpoint or resource.
Covers queryOptions pattern, keys.ts structure, mutation hook template, and imperative
fetching.
---
+14 -14
View File
@@ -1,9 +1,9 @@
---
name: studio-testing
description: Testing strategy for Supabase Studio. Use when writing tests, deciding what
type of test to write, extracting logic from components into testable utility
functions, or reviewing test coverage. Covers unit tests, component tests,
and E2E test selection criteria.
description: Testing strategy for Supabase Studio. Use when writing tests, deciding
whether a change needs tests and which type, extracting logic from components into
testable utility functions, or reviewing test coverage. Covers unit tests, component
tests, and E2E test selection criteria.
---
# Studio Testing Strategy
@@ -162,14 +162,14 @@ try/finally for resource cleanup. For E2E execution details, see the
## Codebase References
| What | Where |
| ----------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| What | Where |
| ----------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Util test examples | `apps/studio/tests/components/Grid/Grid.utils.test.ts`, `apps/studio/tests/components/Billing/TaxID.utils.test.ts`, `apps/studio/tests/components/Editor/SpreadsheetImport.utils.test.ts` |
| Component test examples | `apps/studio/tests/features/logs/LogsFilterPopover.test.tsx`, `apps/studio/tests/components/CopyButton.test.tsx` |
| E2E test example | `e2e/studio/features/filter-bar.spec.ts` |
| E2E helpers pattern | `e2e/studio/utils/filter-bar-helpers.ts` |
| Custom render | `apps/studio/tests/lib/custom-render.tsx` |
| MSW mock setup | `apps/studio/tests/lib/msw.ts` (`addAPIMock`) |
| Test README | `apps/studio/tests/README.md` |
| Vitest config | `apps/studio/vitest.config.ts` |
| Related skills | `studio-e2e-tests` (running E2E), `vitest` (API reference), `vercel-composition-patterns` (component architecture) |
| Component test examples | `apps/studio/tests/features/logs/LogsFilterPopover.test.tsx`, `apps/studio/tests/components/CopyButton.test.tsx` |
| E2E test example | `e2e/studio/features/filter-bar.spec.ts` |
| E2E helpers pattern | `e2e/studio/utils/filter-bar-helpers.ts` |
| Custom render | `apps/studio/tests/lib/custom-render.tsx` |
| MSW mock setup | `apps/studio/tests/lib/msw.ts` (`addAPIMock`) |
| Test README | `apps/studio/tests/README.md` |
| Vitest config | `apps/studio/vitest.config.ts` |
| Related skills | `studio-e2e-tests` (running E2E), `vitest` (API reference), `vercel-composition-patterns` (component architecture) |
+5 -1
View File
@@ -34,7 +34,7 @@ Docs: `apps/design-system/content/docs/ui-patterns/forms.mdx`
- Use `react-hook-form` + `zod`
- Use `FormItemLayout` instead of manually composing `FormItem`/`FormLabel`/`FormMessage`/`FormDescription`
- Wrap inputs with `FormControl`; use `_Shadcn_` imports from `ui` for primitives
- Wrap inputs with `FormControl`; import primitives from `ui`
Layout selection:
@@ -125,6 +125,10 @@ Forms in sheets:
- `layout="horizontal"` for wider sheets
- `layout="vertical"` for narrow sheets (`size="sm"` or below)
- When the sheet contains a form, wire dirty dismissal with `useConfirmOnClose` +
`DiscardChangesConfirmationDialog` (Cancel, Escape, and backdrop). Source of
truth: `apps/design-system/content/docs/ui-patterns/modality.mdx` (Dirty form
dismissal). Also see the react-hook-form skill for `isDirty` destructuring.
## Copy
+11 -3
View File
@@ -1,8 +1,10 @@
---
name: telemetry-standards
description: PostHog event tracking standards for Supabase Studio. Use when reviewing
PRs for telemetry compliance or implementing new event tracking. Covers event naming,
property conventions, approved patterns, and implementation guide.
description: PostHog event tracking standards for Supabase Studio. Use when adding
useTrack() calls, defining events in packages/common/telemetry-constants.ts,
implementing tracking for a new feature, or reviewing PRs for telemetry compliance.
Covers event naming, property conventions, approved patterns, and implementation
guide.
---
# Telemetry Standards for Supabase Studio
@@ -19,16 +21,19 @@ opened, clicked, submitted, created, removed, updated, intended, evaluated, adde
enabled, disabled, copied, exposed, failed, converted, closed, completed, applied, sent, moved
**Flag these:**
- Unapproved verbs (saved, viewed, seen, pressed, etc.)
- Wrong order: `click_product_card` → should be `product_card_clicked`
- Wrong casing: `productCardClicked` → should be `product_card_clicked`
**Good examples:**
- `product_card_clicked`
- `backup_button_clicked`
- `sql_query_submitted`
**Common mistakes with corrections:**
- `database_saved` → `save_button_clicked` or `database_updated` (unapproved verb)
- `click_backup_button` → `backup_button_clicked` (wrong order)
- `dashboardViewed` → don't track passive views on page load
@@ -39,10 +44,12 @@ enabled, disabled, copied, exposed, failed, converted, closed, completed, applie
**Casing:** camelCase preferred for new events. The codebase has existing snake_case properties (e.g., `schema_name`, `table_name`) — when adding properties to an existing event, match its established convention.
**Names must be self-explanatory:**
- `{ productType: 'database', planTier: 'pro' }`
- `{ assistantType: 'sql', suggestionType: 'optimization' }`
**Flag these:**
- Generic names: `label`, `value`, `name`, `data`
- PascalCase properties
- Inconsistent names across similar events (e.g., `assistantType` in one event, `aiType` in a related event)
@@ -117,6 +124,7 @@ When reviewing a PR, flag these as **required changes:**
5. **Inaccurate docs** — `@page`/`@source` descriptions that don't match the actual implementation
When a PR adds user-facing interactions (buttons, forms, toggles, modals) **without** tracking, suggest:
- "This adds a user interaction that may benefit from tracking."
- Propose the event name following `[object]_[verb]` convention
- Propose the `useTrack()` call with suggested properties
+1 -1
View File
@@ -82,7 +82,7 @@ knowledge_base:
code_guidelines:
filePatterns:
# Studio code conventions — React/TS, UI patterns, composition, data fetching, errors
- files: '.claude/skills/{studio-best-practices,studio-ui-patterns,vercel-composition-patterns,studio-queries,studio-error-handling}/SKILL.md'
- files: '.claude/skills/{studio-best-practices,studio-ui-patterns,vercel-composition-patterns,studio-queries,studio-error-handling,react-hook-form}/SKILL.md'
applyTo: 'apps/studio/**/*.{ts,tsx}'
# Studio unit / component test conventions
- files: '.claude/skills/{studio-testing,studio-mock-api-tests}/SKILL.md'
@@ -1,45 +0,0 @@
name: Dashboard PR Reminder
on:
schedule:
# Run at 10am Singapore Time (2am UTC)
- cron: '0 2 * * *'
# Run at 10am US Eastern Time (2pm UTC = 10am EDT / 9am EST)
- cron: '0 14 * * *'
workflow_dispatch: # Allow manual trigger for testing
permissions:
pull-requests: read
contents: read
jobs:
check-dashboard-prs:
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with:
persist-credentials: false
sparse-checkout: |
scripts
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: Find stale Dashboard PRs and notify Slack
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
SLACK_WEBHOOK_URL: ${{ secrets.SLACK_DASHBOARD_WEBHOOK_URL }}
run: pnpm tsx scripts/actions/find-stale-dashboard-prs.ts | pnpm tsx scripts/actions/send-slack-pr-notification.ts
+186
View File
@@ -0,0 +1,186 @@
name: Docs E2E Tests
on:
pull_request:
types: [opened, synchronize, reopened, ready_for_review, converted_to_draft]
branches: ['master']
paths:
- 'apps/docs/content/guides/**/*.mdx'
- 'apps/docs/content/troubleshooting/**/*.mdx'
- 'apps/docs/content/_partials/**'
- 'e2e/docs/features/**'
- 'e2e/docs/utils/**'
- 'e2e/docs/scripts/**'
- 'e2e/docs/playwright.config.ts'
- 'e2e/docs/package.json'
- 'e2e/docs/tsconfig.json'
- 'pnpm-lock.yaml'
- '.github/workflows/docs-e2e.yml'
workflow_dispatch:
inputs:
base_url:
description: 'Base URL to test against'
required: false
default: 'https://supabase.com'
type: string
page_paths:
description: 'Comma-separated /docs/... paths to test (required for manual runs)'
required: false
default: ''
type: string
concurrency:
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
cancel-in-progress: true
permissions:
contents: read
statuses: read
pull-requests: read
env:
CI: true
jobs:
e2e:
name: Docs E2E
if: github.event_name == 'workflow_dispatch' || github.event.pull_request.draft == false
timeout-minutes: 30
runs-on: blacksmith-4vcpu-ubuntu-2404
steps:
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with:
persist-credentials: false
# Need full history on PRs so we can diff against the base branch.
# Use string '0' — numeric 0 is falsy in GitHub Actions expressions.
fetch-depth: ${{ github.event_name == 'pull_request' && '0' || '1' }}
sparse-checkout: |
e2e/docs
scripts
patches
apps/docs/content/guides
apps/docs/content/troubleshooting
apps/docs/content/_partials
apps/docs/scripts/federated-content/sources
- name: Use Node.js
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
with:
node-version-file: '.nvmrc'
# Map changed owned content (guides, troubleshooting, partials) to page
# URLs. Harness-only PRs resolve to skip=true and exit before Playwright.
- name: Resolve docs E2E scope
id: scope
env:
EVENT_NAME: ${{ github.event_name }}
BASE_REF: ${{ github.base_ref }}
PAGE_PATHS_INPUT: ${{ inputs.page_paths }}
run: |
if [ "$EVENT_NAME" = "workflow_dispatch" ]; then
if [ -z "$PAGE_PATHS_INPUT" ]; then
echo "skip=true" >> "$GITHUB_OUTPUT"
echo "paths=" >> "$GITHUB_OUTPUT"
echo "Manual run requires the page_paths input."
exit 0
fi
echo "skip=false" >> "$GITHUB_OUTPUT"
printf 'paths=%s\n' "$PAGE_PATHS_INPUT" >> "$GITHUB_OUTPUT"
exit 0
fi
git diff --name-only --diff-filter=ACMR "origin/$BASE_REF"...HEAD \
| node --experimental-strip-types e2e/docs/scripts/resolve-docs-scope.ts
- name: Skip Playwright (no in-scope pages)
if: steps.scope.outputs.skip == 'true'
run: echo "No in-scope docs pages changed; skipping Playwright suite."
- uses: pnpm/action-setup@0ebf47130e4866e96fce0953f49152a61190b271 # v6.0.9
if: steps.scope.outputs.skip != 'true'
name: Install pnpm
with:
run_install: false
- name: Enable pnpm store cache
if: steps.scope.outputs.skip != 'true'
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
with:
node-version-file: '.nvmrc'
cache: 'pnpm'
# Vercel skips the docs preview when a PR only changes the harness
# (e2e/docs, workflow). Wait for a preview only when apps/docs changed.
- name: Detect docs app changes
if: steps.scope.outputs.skip != 'true' && github.event_name == 'pull_request'
id: filter
uses: dorny/paths-filter@de90cc6fb38fc0963ad72b210f1f284cd68cea36 # v3.0.2
with:
filters: |
docs_app:
- 'apps/docs/**'
# Vercel's GitHub App stopped writing GitHub Deployment objects on
# 2026-02-17 (broken app auth), so vercel/wait-for-deployment-action
# times out polling that API even though the preview builds fine.
# Poll the "Vercel – docs" commit status instead — Vercel keeps posting
# those — then resolve the deployment it points to via Vercel's own API
# to get the actual preview URL. See scripts/waitForVercelDocsPreview.js.
- name: Wait for Vercel docs preview
if: steps.scope.outputs.skip != 'true' && github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name == github.repository && steps.filter.outputs.docs_app == 'true'
id: deployment
run: node scripts/waitForVercelDocsPreview.js
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
HEAD_SHA: ${{ github.event.pull_request.head.sha }}
VERCEL_TOKEN: ${{ secrets.VERCEL_TOKEN }}
VERCEL_TEAM_ID: ${{ secrets.VERCEL_TEAM_ID }}
- name: Resolve base URL
if: steps.scope.outputs.skip != 'true'
id: base-url
env:
EVENT_NAME: ${{ github.event_name }}
BASE_URL_INPUT: ${{ inputs.base_url }}
DEPLOYMENT_URL: ${{ steps.deployment.outputs.deployment-url }}
DOCS_APP_CHANGED: ${{ steps.filter.outputs.docs_app }}
run: |
if [ "$EVENT_NAME" = "workflow_dispatch" ]; then
printf 'url=%s\n' "$BASE_URL_INPUT" >> "$GITHUB_OUTPUT"
echo "use_bypass=false" >> "$GITHUB_OUTPUT"
elif [ "$DOCS_APP_CHANGED" = "true" ] && [ -n "$DEPLOYMENT_URL" ]; then
printf 'url=%s\n' "$DEPLOYMENT_URL" >> "$GITHUB_OUTPUT"
echo "use_bypass=true" >> "$GITHUB_OUTPUT"
else
# Harness-only PRs have no docs preview; test against production.
echo "url=https://supabase.com" >> "$GITHUB_OUTPUT"
echo "use_bypass=false" >> "$GITHUB_OUTPUT"
fi
- name: Install dependencies
if: steps.scope.outputs.skip != 'true'
run: pnpm install --frozen-lockfile --filter=e2e-docs...
- name: Install Playwright Chromium
if: steps.scope.outputs.skip != 'true'
run: pnpm -C e2e/docs exec playwright install chromium --with-deps --only-shell
- name: Run docs E2E
if: steps.scope.outputs.skip != 'true'
working-directory: e2e/docs
run: pnpm run e2e:docs
env:
PLAYWRIGHT_BASE_URL: ${{ steps.base-url.outputs.url }}
DOCS_E2E_PAGE_PATHS: ${{ steps.scope.outputs.paths }}
VERCEL_AUTOMATION_BYPASS_SECRET: ${{ steps.base-url.outputs.use_bypass == 'true' && secrets.VERCEL_AUTOMATION_BYPASS_DOCS || '' }}
- name: Upload Playwright report
if: failure() && steps.scope.outputs.skip != 'true'
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
with:
name: docs-playwright-report
path: |
e2e/docs/playwright-report/
e2e/docs/test-results/
retention-days: 7
+9 -9
View File
@@ -3,7 +3,7 @@ name: Update Mgmt Api Docs
on:
schedule:
# Run at 00:00 UTC every Monday
- cron: '0 0 * * 1'
- cron: "0 0 * * 1"
workflow_dispatch:
permissions:
@@ -18,7 +18,7 @@ jobs:
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with:
persist-credentials: false
ref: master
ref: ${{ github.ref }}
sparse-checkout: |
apps/docs
patches
@@ -32,8 +32,8 @@ jobs:
- name: Use Node.js
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
with:
node-version-file: '.nvmrc'
cache: 'pnpm'
node-version-file: ".nvmrc"
cache: "pnpm"
- name: Install deps
run: pnpm install --frozen-lockfile
@@ -55,8 +55,8 @@ jobs:
uses: peter-evans/create-pull-request@c5a7806660adbe173f04e3e038b0ccdcd758773c # v6.1.0
with:
token: ${{ steps.app-token.outputs.token }}
commit-message: 'feat: update mgmt api docs'
title: 'feat: update mgmt api docs'
body: 'This PR updates mgmt api docs automatically.'
branch: 'gha/auto-update-mgmt-api-docs'
base: 'master'
commit-message: "feat: update mgmt api docs"
title: "feat: update mgmt api docs"
body: "This PR updates mgmt api docs automatically."
branch: "gha/auto-update-mgmt-api-docs"
base: "master"
+54
View File
@@ -6,6 +6,11 @@ on:
paths:
- 'apps/docs/**/*.ts*'
- 'apps/docs/spec/**/*.json'
- 'apps/docs/.env.development'
- 'apps/docs/package.json'
- 'e2e/docs/local-smoke/**'
- 'e2e/docs/playwright.local-smoke.config.ts'
- 'e2e/docs/package.json'
# Cancel old builds on new commit for same workflow + branch/PR
concurrency:
@@ -70,3 +75,52 @@ jobs:
echo "GITHUB_CLIENT_ID=dummy-id" >> .env
echo "GITHUB_SECRET=dummy-secret" >> .env
pnpm run test:docs
local-dev-smoke:
name: Local dev smoke (no credentials)
runs-on: blacksmith-4vcpu-ubuntu-2404
steps:
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with:
persist-credentials: false
sparse-checkout: |
apps/docs
examples
packages
supabase
patches
e2e/docs
- 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: Install Playwright Chromium
run: pnpm -C e2e/docs exec playwright install chromium --with-deps --only-shell
# Deliberately does not set DOCS_GITHUB_APP_*, SUPABASE_SECRET_KEY,
# OPENAI_API_KEY, or DOCS_REVALIDATION_KEYS — their absence here is what
# verifies `pnpm run dev:docs` still works without private credentials.
- name: Run local dev smoke tests
run: pnpm run e2e:docs:local-smoke
- name: Upload Playwright report
if: failure()
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
with:
name: docs-local-smoke-playwright-report
path: |
e2e/docs/playwright-report-local-smoke/
e2e/docs/test-results/
retention-days: 7
+1 -1
View File
@@ -11,7 +11,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- id: label
uses: actions/labeler@634933edcd8ababfe52f92936142cc22ac488b1b # v6.0.1
uses: actions/labeler@b8dd2d9be0f68b860e7dae5dae7d772984eacd6d # v6.2.0
- name: Comment when api-deploy-required is auto-applied
if: contains(steps.label.outputs.new-labels, 'api-deploy-required')
+2 -2
View File
@@ -29,10 +29,10 @@ jobs:
with:
role-to-assume: ${{ secrets.PROD_AWS_ROLE }}
aws-region: us-east-1
- uses: docker/login-action@465a07811f14bebb1938fbed4728c6a1ff8901fc # v2.2.0
- uses: docker/login-action@af1e73f918a031802d376d3c8bbc3fe56130a9b0 # v4.4.0
with:
registry: public.ecr.aws
- uses: docker/login-action@465a07811f14bebb1938fbed4728c6a1ff8901fc # v2.2.0
- uses: docker/login-action@af1e73f918a031802d376d3c8bbc3fe56130a9b0 # v4.4.0
with:
registry: ghcr.io
username: ${{ github.actor }}
+3 -3
View File
@@ -46,7 +46,7 @@ jobs:
- uses: docker/setup-buildx-action@885d1462b80bc1c1c7f0b00334ad271f09369c55 # v2.10.0
- name: Login to DockerHub
uses: docker/login-action@465a07811f14bebb1938fbed4728c6a1ff8901fc # v2.2.0
uses: docker/login-action@af1e73f918a031802d376d3c8bbc3fe56130a9b0 # v4.4.0
with:
username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_PASSWORD }}
@@ -83,7 +83,7 @@ jobs:
tags: |
type=raw,value=${{ needs.settings.outputs.image_version }}_${{ env.arch }}
- uses: docker/login-action@465a07811f14bebb1938fbed4728c6a1ff8901fc # v2.2.0
- uses: docker/login-action@af1e73f918a031802d376d3c8bbc3fe56130a9b0 # v4.4.0
with:
username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_PASSWORD }}
@@ -115,7 +115,7 @@ jobs:
steps:
- uses: docker/setup-buildx-action@885d1462b80bc1c1c7f0b00334ad271f09369c55 # v2.10.0
- uses: docker/login-action@465a07811f14bebb1938fbed4728c6a1ff8901fc # v2.2.0
- uses: docker/login-action@af1e73f918a031802d376d3c8bbc3fe56130a9b0 # v4.4.0
with:
username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_PASSWORD }}
+1 -1
View File
@@ -13,7 +13,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Label Stale Issues
uses: actions/stale@28ca1036281a5e5922ead5184a1bbf96e5fc984e # v9.0.0
uses: actions/stale@1e223db275d687790206a7acac4d1a11bd6fe629 # v10.4.0
with:
days-before-issue-stale: 30
stale-issue-message: 'After 30 days of inactivity, this issue has been marked as stale. Commenting (or other activity) will remove the stale label.'
@@ -36,3 +36,8 @@ jobs:
- name: Build
if: steps.filter.outputs.studio == 'true'
run: docker build . -f apps/studio/Dockerfile --target production -t supabase-studio:local --build-arg NEXT_PUBLIC_STUDIO_AUTH_MODE=supabase --no-cache
# Reuses the shared stages (deps/dev) from the first build's layer
# cache; only the tanstack build/runtime stages run fresh.
- name: Build (tanstack)
if: steps.filter.outputs.studio == 'true'
run: docker build . -f apps/studio/Dockerfile --target production -t supabase-studio:local-tanstack --build-arg NEXT_PUBLIC_STUDIO_AUTH_MODE=supabase --build-arg STUDIO_FRAMEWORK=tanstack
+1 -1
View File
@@ -93,7 +93,7 @@ jobs:
with:
role-to-assume: ${{ secrets.PROD_AWS_ROLE }}
aws-region: us-east-1
- uses: docker/login-action@465a07811f14bebb1938fbed4728c6a1ff8901fc # v2.2.0
- uses: docker/login-action@af1e73f918a031802d376d3c8bbc3fe56130a9b0 # v4.4.0
if: steps.filter.outputs.studio == 'true' && !github.event.pull_request.head.repo.fork
with:
registry: public.ecr.aws
+6
View File
@@ -37,6 +37,12 @@ jobs:
node-version-file: '.nvmrc'
cache: 'pnpm'
# Needs no dependencies, so it runs before install and fails fast. Catches a
# class of bug that only breaks on case-insensitive filesystems (macOS and
# Windows dev machines) and is therefore invisible to typecheck/lint on CI.
- name: Check for case-sensitivity hazards
run: node scripts/check-case-hazards.mjs
- name: Install deps
run: pnpm install --frozen-lockfile
+1 -1
View File
@@ -123,8 +123,8 @@ next-env.d.ts
!.claude/scripts/
!.claude/skills/
.claude/skills/me-*
CLAUDE.md
!.claude/CLAUDE.md
CLAUDE.local.md
#include template .env file for docker-compose
!docker/.env
-2
View File
@@ -1,3 +1 @@
engine-strict=true
update-notifier=false
@jsr:registry=https://npm.jsr.io
+1 -1
View File
@@ -97,7 +97,7 @@ You can run any of the sites individually by using the scope name. For example:
pnpm dev:www
```
Note: Particularly for `www` make sure you have copied `apps/www/.env.local.example` to `apps/www/.env.local`
Note: Particularly for `www` make sure you have copied `apps/www/.env.local.example` to `apps/www/.env.local`. For `docs`, see [`apps/docs/DEVELOPERS.md`](./apps/docs/DEVELOPERS.md).
#### Shared components
+1 -1
View File
@@ -57,7 +57,7 @@ You can also [self-host](https://supabase.com/docs/guides/hosting/overview) and
- [Storage](https://github.com/supabase/storage-api) a RESTful API for managing files in S3, with Postgres handling permissions.
- [pg_graphql](http://github.com/supabase/pg_graphql/) a PostgreSQL extension that exposes a GraphQL API.
- [postgres-meta](https://github.com/supabase/postgres-meta) is a RESTful API for managing your Postgres, allowing you to fetch tables, add roles, and run queries, etc.
- [Kong](https://github.com/Kong/kong) is a cloud-native API gateway.
- [Envoy](https://github.com/envoyproxy/envoy) is a cloud-native, high-performance edge and service proxy.
#### Client libraries
@@ -147,6 +147,7 @@ export default function Component() {
const chart = key as keyof typeof chartConfig
return (
<button
tabIndex={0}
key={chart}
data-active={activeChart === chart}
className="relative z-30 flex flex-1 flex-col justify-center gap-1 border-t px-6 py-4 text-left even:border-l data-[active=true]:bg-surface-100 sm:border-l sm:border-t-0 sm:px-8 sm:py-6"
+77
View File
@@ -2502,6 +2502,17 @@ export const Index: Record<string, any> = {
subcategory: "undefined",
chunks: []
},
"skip-to-content-demo": {
name: "skip-to-content-demo",
type: "components:example",
registryDependencies: undefined,
component: React.lazy(() => import("@/registry/default/example/skip-to-content-demo")),
source: "",
files: ["registry/default/example/skip-to-content-demo.tsx"],
category: "undefined",
subcategory: "undefined",
chunks: []
},
"page-container-demo": {
name: "page-container-demo",
type: "components:example",
@@ -2601,6 +2612,72 @@ export const Index: Record<string, any> = {
subcategory: "undefined",
chunks: []
},
"connect-interstitial-demo": {
name: "connect-interstitial-demo",
type: "components:example",
registryDependencies: undefined,
component: React.lazy(() => import("@/registry/default/example/connect-interstitial-demo")),
source: "",
files: ["registry/default/example/connect-interstitial-demo.tsx"],
category: "undefined",
subcategory: "undefined",
chunks: []
},
"connect-interstitial-action-error": {
name: "connect-interstitial-action-error",
type: "components:example",
registryDependencies: undefined,
component: React.lazy(() => import("@/registry/default/example/connect-interstitial-action-error")),
source: "",
files: ["registry/default/example/connect-interstitial-action-error.tsx"],
category: "undefined",
subcategory: "undefined",
chunks: []
},
"connect-interstitial-logo-pair": {
name: "connect-interstitial-logo-pair",
type: "components:example",
registryDependencies: undefined,
component: React.lazy(() => import("@/registry/default/example/connect-interstitial-logo-pair")),
source: "",
files: ["registry/default/example/connect-interstitial-logo-pair.tsx"],
category: "undefined",
subcategory: "undefined",
chunks: []
},
"connect-interstitial-logo-single": {
name: "connect-interstitial-logo-single",
type: "components:example",
registryDependencies: undefined,
component: React.lazy(() => import("@/registry/default/example/connect-interstitial-logo-single")),
source: "",
files: ["registry/default/example/connect-interstitial-logo-single.tsx"],
category: "undefined",
subcategory: "undefined",
chunks: []
},
"connect-interstitial-logo-unknown": {
name: "connect-interstitial-logo-unknown",
type: "components:example",
registryDependencies: undefined,
component: React.lazy(() => import("@/registry/default/example/connect-interstitial-logo-unknown")),
source: "",
files: ["registry/default/example/connect-interstitial-logo-unknown.tsx"],
category: "undefined",
subcategory: "undefined",
chunks: []
},
"connect-interstitial-logo-uploaded": {
name: "connect-interstitial-logo-uploaded",
type: "components:example",
registryDependencies: undefined,
component: React.lazy(() => import("@/registry/default/example/connect-interstitial-logo-uploaded")),
source: "",
files: ["registry/default/example/connect-interstitial-logo-uploaded.tsx"],
category: "undefined",
subcategory: "undefined",
chunks: []
},
"page-layout-auth-emails": {
name: "page-layout-auth-emails",
type: "components:example",
+8 -3
View File
@@ -1,4 +1,5 @@
import { ScrollArea } from 'ui'
import { SkipToContent } from 'ui-patterns/SkipToContent'
import { MobileSidebarSheet } from '@/components/mobile-sidebar-sheet'
import { SideNavigation } from '@/components/side-navigation'
@@ -12,18 +13,22 @@ interface AppLayoutProps {
export default async function AppLayout({ children }: AppLayoutProps) {
return (
<>
<SkipToContent href="#main" />
<TopNavigation />
<MobileSidebarSheet />
<main className="flex-1 max-w-site mx-auto w-full border-l border-r border-b">
<div className="flex-1 max-w-site mx-auto w-full border-l border-r border-b">
<div className="flex-1 items-start md:grid md:grid-cols-[220px_minmax(0,1fr)] lg:grid-cols-[240px_minmax(0,1fr)]">
<aside className="fixed top-10 z-30 hidden h-[calc(100vh-3rem)] w-full shrink-0 md:sticky md:block border-r">
<ScrollArea className="h-full">
<SideNavigation />
</ScrollArea>
</aside>
{children}
{/* Content-only landmark: sidebar must stay outside so skip/Tab don't land in the nav */}
<main id="main" tabIndex={-1} className="outline-hidden scroll-mt-12 min-w-0">
{children}
</main>
</div>
</main>
</div>
<SiteFooter />
</>
)
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
-53
View File
@@ -1,53 +0,0 @@
import { Source_Code_Pro } from 'next/font/google'
import localFont from 'next/font/local'
export const customFont = localFont({
variable: '--font-custom',
display: 'swap',
fallback: ['Circular', 'custom-font', 'Helvetica Neue', 'Helvetica', 'Arial', 'sans-serif'],
src: [
{
path: './CustomFont-Book.woff2',
weight: '400',
style: 'normal',
},
{
path: './CustomFont-BookItalic.woff2',
weight: '400',
style: 'italic',
},
{
path: './CustomFont-Medium.woff2',
weight: '500',
style: 'normal',
},
{
path: './CustomFont-Bold.woff2',
weight: '700',
style: 'normal',
},
{
path: './CustomFont-BoldItalic.woff2',
weight: '700',
style: 'italic',
},
{
path: './CustomFont-Black.woff2',
weight: '800',
style: 'normal',
},
{
path: './CustomFont-BlackItalic.woff2',
weight: '800',
style: 'italic',
},
],
})
export const sourceCodePro = Source_Code_Pro({
subsets: ['latin'],
fallback: ['Source Code Pro', 'Office Code Pro', 'Menlo', 'monospace'],
variable: '--font-source-code-pro',
display: 'swap',
weight: ['400', '500', '600', '700'],
})
+3 -3
View File
@@ -3,11 +3,11 @@ import '@/styles/globals.css'
import type { Metadata, Viewport } from 'next'
import { customFont, sourceCodePro } from './fonts'
import { Providers } from './Providers'
import { Toaster } from './toaster'
import { inter, manrope, sourceCodePro } from '@/lib/fonts'
const className = `${customFont.variable} ${sourceCodePro.variable}`
const className = `${inter.variable} ${manrope.variable} ${sourceCodePro.variable}`
const BASE_PATH = process.env.NEXT_PUBLIC_BASE_PATH || '/design-system'
@@ -126,7 +126,7 @@ export default async function Layout({ children }: RootLayoutProps) {
{/* [Danny]: This has to be an inline style tag here and not a separate component due to next/font */}
<style
dangerouslySetInnerHTML={{
__html: `:root{--font-custom:${customFont.style.fontFamily};--font-source-code-pro:${sourceCodePro.style.fontFamily};}`,
__html: `:root{--font-sans:${inter.style.fontFamily};--font-heading:${manrope.style.fontFamily};--font-source-code-pro:${sourceCodePro.style.fontFamily};}`,
}}
/>
</head>
@@ -7,6 +7,7 @@ export function ClickCounter() {
return (
<button
tabIndex={0}
onClick={() => setCount(count + 1)}
className="whitespace-nowrap rounded-lg bg-gray-700 px-3 py-1 text-sm font-medium tabular-nums text-gray-100 hover:bg-gray-500 hover:text-white"
>
@@ -60,10 +60,11 @@ const ColorPalette = () => {
const isCopied = copied === reference
return (
<button
tabIndex={0}
key={step * 100}
type="button"
onClick={() => handleCopy(reference)}
className="group relative flex aspect-square w-full items-center justify-center rounded-sm border border-overlay/40 transition hover:scale-[1.05] focus:outline-none focus-visible:ring-2 focus-visible:ring-foreground"
className="group relative flex aspect-square w-full items-center justify-center rounded-sm border border-overlay/40 transition-transform hover:scale-[1.05] focus-ring"
style={{ backgroundColor: reference }}
title={reference}
>
@@ -21,7 +21,7 @@ import {
TabsList,
TabsTrigger,
} from 'ui'
import { Admonition } from 'ui-patterns/admonition'
import { Admonition } from 'ui-patterns/Admonition'
import { ComponentProps } from './component-props'
import { SonnerExpandConfig } from './sonner-expand-config'
+10
View File
@@ -70,6 +70,11 @@ export const docsConfig: DocsConfig = {
href: '/docs/ui-patterns/charts',
items: [],
},
{
title: 'Connect Interstitials',
href: '/docs/ui-patterns/connect-interstitials',
items: [],
},
{
title: 'Empty States',
href: '/docs/ui-patterns/empty-states',
@@ -242,6 +247,11 @@ export const docsConfig: DocsConfig = {
href: '/docs/fragments/single-value-field-array',
items: [],
},
{
title: 'Skip to Content',
href: '/docs/fragments/skip-to-content',
items: [],
},
],
},
{
@@ -25,14 +25,68 @@ All interactive page elements should be reachable by keyboard. Given the below i
Chromium-based browsers and Firefox handle this automatically via the Tab key. Safari, by default, requires the Option key to also be held down. Enabling _Keyboard navigation_ on macOS Settings [removes this requirement](https://mayank.co/blog/safari-focus/#keyboard-navigation) but makes links non-tabbable as a result.
Interactive page elements should also provide visual feedback upon selection via a `focus-visible` state. We use consistent focus styles such as `inset-focus` so users recognize this state instantly.
Interactive page elements should also provide visual feedback upon selection via a `focus-visible` state. We use one shared focus ring so users recognize this state instantly.
[Button](components/button) has all of the above built-in. Bespoke interactive elements however, such as the below interactive [Table Row](components/table#examples), require these props to be added manually:
### Focus ring recipe
Prefer the shared utilities over inventing local styles:
| Utility | Use when |
| ------------- | -------------------------------------------------------------------------- |
| `focus-ring` | Buttons, inputs, and most controls (offset **ring**) |
| `focus-inset` | Dense or flush surfaces such as interactive table rows (inset **outline**) |
```tsx
className = 'focus-ring'
// or
className = 'relative cursor-pointer focus-inset'
```
These expand to:
**`focus-ring`**
```txt
outline-hidden
focus-visible:ring-2
focus-visible:ring-ring
focus-visible:ring-offset-2
focus-visible:ring-offset-background
```
**`focus-inset`**
Uses `outline` (not `ring`) so it paints reliably on interactive `<tr>`s. Tailwind `ring` is `box-shadow`, which browsers often skip on `display: table-row` (notably Safari). Do not put `focus-ring` or raw `ring-*` on a `<tr>`, and do not add `outline-hidden` alongside `focus-inset`. `outline-hidden` sets `outline-style: none` and will hide the indicator.
```txt
&:focus-visible {
outline-style: solid
outline-width: 2px
outline-offset: -2px
outline-color: var(--ring)
border-radius: var(--radius-md)
}
```
`outline-hidden` is always on (not `focus-visible:`-prefixed) so mouse click does not show the browser’s default outline; the focus indicator replaces it for keyboard focus only.
Rules:
- Prefer `:focus-visible` over `:focus` so click/tap does not show a focus indicator
- Never use `outline-none` / `outline-hidden` without a ring or outline replacement
- Always use the shared color (`ring-ring` / `outline-ring`). Variants (primary, danger, warning) do not change focus colour
- Do not animate the focus indicator; avoid `transition-all` / `transition` on controls that show one (prefer `transition-colors`)
- Prefer `focus-ring` / `focus-inset` over copy-pasting the class stack
- On interactive `<tr>`s, use `focus-inset` only. `focus-ring` will look fine in some browsers and invisible in others
When the focused element is not the thing that should show the ring (e.g. a wrapping `Link` with `group`, or an `InputGroup` parent using `:has()`), keep the explicit `group-focus-visible:ring-*` / `has-[…]:focus-visible:ring-*` stack. The utilities bake in `:focus-visible` on the same element and do not compose as `group-focus-visible:focus-ring`.
[Button](components/button) has focus, `tabIndex`, and the shared ring built-in. The same explicit `tabIndex` default is also baked into Checkbox, Switch, Select Trigger, Toggle, Accordion Trigger, Collapsible Trigger, Dropdown Menu Trigger, Popover Trigger, Dialog Trigger, Sheet Trigger, Alert Dialog Trigger, and the Sidebar Menu and action buttons. Bespoke interactive elements however, such as the below interactive [Table Row](components/table#examples), require these props to be added manually:
```tsx showLineNumbers {4-14}
<TableRow
key={id}
className="relative cursor-pointer h-16 inset-focus"
className="relative cursor-pointer h-16 focus-inset"
onClick={(event) => {
if (event.currentTarget !== event.target) return
handleBucketNavigation(bucket.id, event)
@@ -62,11 +116,13 @@ Individual options inside of a group can be reached by arrow keys (↑ ↓ ←
### Jumping ahead
Some keyboard-navigable content may be contain hundreds or thousands of items. Help users jump to specific content with the following mitigation strategies:
Some keyboard-navigable content may contain hundreds or thousands of items. Help users jump to specific content with:
- Search and filtering
- Pagination or virtualization
- “Jump to” shortcuts to skip ahead
- Skip links and “jump to” shortcuts
Apps with persistent header and sidebar chrome should expose a skip link as the first focusable element. Use the shared [Skip to Content](fragments/skip-to-content) fragment which owns the component API, usage sample, and target landmark contract.
## Screen readers
@@ -1,6 +1,6 @@
---
title: Button
description: Displays a button or a component that looks like a button.
description: Displays a button or a link that looks like a button.
featured: true
component: true
---
@@ -144,5 +144,6 @@ Inside [Admonition](../fragments/admonition#split-button-with-dropdown) actions,
- Enabled buttons default to `tabIndex={0}` (keyboard accessible)
- Disabled buttons default to `tabIndex={-1}` (removed from tab order)
- You can still override with an explicit `tabIndex` prop when needed
- Keyboard focus uses the shared `focus-ring` utility; variants do not change ring colour
You therefore don't need to manually set `tabIndex`, as Button handles it automatically based on its `disabled` state.
@@ -13,7 +13,7 @@ source:
<ComponentPreview name="label-demo" peekCode wide />
<Admonition
variant="warning"
type="warning"
title="Do not use this Label component in a Form"
>
@@ -58,6 +58,21 @@ import { toast } from 'sonner'
toast('Event has been created.')
```
## When to use
Use a toast for short-lived, non-blocking feedback or to confirm an operation
after its originating surface has closed or navigated away.
Do not use a toast as the only feedback for:
- Field validation. Use `FormMessage` or `FieldError` beside the field.
- A failed submission that leaves the form or interstitial visible. Show a
`role="alert"` message in destructive text near the actions.
- A blocking or materially changed page state. Use an inline state component
such as `Admonition` or `ErrorDisplay`.
Keep error copy specific and include the next step when it is not obvious.
## Expand
You can change the amount of toasts visible through the visibleToasts prop.
@@ -221,7 +221,7 @@ Avoid adding other actions when using row-level navigation, as multiple interact
When implementing row-level navigation, pay close attention to [Accessibility](/accessibility#focus-management) requirements. The row must be keyboard accessible with proper focus management. Also consider these affordances:
- Handle `Enter` and `Space` key presses for activation
- Provide visual focus indicators using classes like `inset-focus`
- Provide visual focus indicators using classes like `focus-inset`
- Support modifier keys (`Ctrl`/`Cmd`, middle-click) for opening links in new tabs
- Consider using the shared `createNavigationHandler` function to handle modifier keys
- Avoid bubbling up action events from _within_ the row
@@ -24,7 +24,7 @@ Avoid title-only Admonitions in new code. A callout with `title` should also inc
## Usage
```tsx
import { Admonition } from 'ui-patterns/admonition'
import { Admonition } from 'ui-patterns/Admonition'
```
```tsx
@@ -0,0 +1,53 @@
---
title: Skip to Content
description: Keyboard-accessible skip link that jumps past chrome to the main landmark.
component: true
fragment: true
---
<ComponentPreview name="skip-to-content-demo" peekCode wide />
Keyboard-accessible skip link composed from [Button](../components/button) for apps with persistent header and sidebar chrome. Hidden until focused via Tab, then slides into view so users can jump past navigation to the main landmark.
The preview above shows the focused appearance. In product apps, the link stays off-screen until keyboard focus.
See [Accessibility](../accessibility#jumping-ahead) for more information on when to use skip links, and how they coexist alongside other navigation aids.
## Usage
```tsx
import { SkipToContent } from 'ui-patterns/SkipToContent'
```
```tsx
<SkipToContent href="#main" />
<main id="main" tabIndex={-1} className="scroll-mt-(--header-height) outline-hidden">
{children}
</main>
```
Place the skip link at the root of the app chrome so Tab reaches it before navigation. The target landmark must be content only. Do not wrap a sidebar inside the same `<main>`, otherwise a Tab after skip will land in the sidebar navigation.
## Props
### `href`
Hash href to the main content landmark, e.g. `#main`.
### `children`
Link label. Defaults to `Skip to content`.
### `className`
Optional classes merged onto the positioning wrapper (useful for demos or layout overrides).
## Target landmark
Callers own the landmark the skip link points at:
- Matching `id`
- `tabIndex={-1}` so Enter moves focus onto the landmark
- `scroll-mt` when a sticky header is present
- `outline-hidden` so the landmark has no visible focus ring. The subsequent Tab will land on the first interactive child
+4 -4
View File
@@ -45,7 +45,7 @@ function app() {
}
```
**Default props**: All icons default to `size={24}`. Stroke and fill defaults come from the source SVG's root attributes (e.g. `stroke-width="1"` on the root `<svg>` becomes the component's default `strokeWidth`). You can override those defaults at the call site, but any `stroke`, `fill`, or `stroke-width` set on individual child paths will still win.
**Default props**: All icons default to `size={24}`. Stroke icons use `strokeWidth={1.5}` and fill-only icons use `stroke="none"`. These defaults come from the source SVG's root attributes and can be intentionally overridden at the call site.
### Adding new custom icons
@@ -54,7 +54,7 @@ Follow these steps to add a new custom icon to the Supabase icon library.
1. **Create SVG file**: Add your SVG file to `packages/icons/src/raw-icons/` with a kebab-case name (e.g., `my-new-icon.svg`). Make sure it follows these requirements:
- Exported at 24×24px with `viewBox="0 0 24 24"`
- Uses `stroke="currentColor"` for strokes (no hardcoded colors)
- Uses `stroke-width="1.5"` (deviate based on optical weight if necessary)
- Uses `stroke-width="1.5"`
- Uses `fill="none"` for fills (no hardcoded colors)
- Icon content is optically centered and around 18×18px within the 24×24 frame
- Any unnecessary elements like `<clipPath>`, `<defs>`, and `<g>` wrappers have been removed
@@ -62,7 +62,7 @@ Follow these steps to add a new custom icon to the Supabase icon library.
For **fill-only icons** (e.g. logos that use shapes instead of strokes), add `stroke="none"` to the root `<svg>` element. The build will propagate this so the component never renders an unwanted stroke.
Prefer putting shared styling like `fill`, `stroke`, `stroke-width`, `stroke-linecap`, and `stroke-linejoin` on the root `<svg>`. The build propagates those root attributes as the component's defaults, and also converts them to camel-case for React compatibility (e.g. `strokeWidth`). If you put those attributes on individual child paths instead, they become fixed path-level styling and will override props passed to the component.
Put shared styling like `fill`, `stroke`, `stroke-width`, `stroke-linecap`, and `stroke-linejoin` on the root `<svg>`. The build validates this contract, propagates the root attributes as the component's defaults, and converts them to camel-case for React compatibility (e.g. `strokeWidth`). Child-level stroke styling is rejected because it would override props passed to the component.
2. **Build the component**: Run `npm run build:icons` from inside the `packages/icons` directory
@@ -132,4 +132,4 @@ Note `stroke="none"` on the root to prevent unwanted strokes, and `fill="current
### Troubleshooting
If your SVG specifies `stroke-width` attributes on individual child paths, they will override the component's `strokeWidth` prop. Keep shared stroke attributes on the root `<svg>` and remove them from individual paths if you want consumers to control stroke weight.
The icon build reports the source file and invalid attribute when an SVG does not follow the stroke or fill-only contract. Run `pnpm validate:icons` from `packages/icons` to check sources without regenerating components.
@@ -0,0 +1,268 @@
---
title: Connect Interstitials
description: Shared layout guidance for focused authorisation, invite, marketplace, CLI, and credit redemption flows.
---
Connect interstitials are focused, single-card flows that sit outside the main
Studio shell. Use the shared `InterstitialLayout` family instead of building
bespoke centered cards, logos, account rows, or organisation selectors.
<ComponentPreview
name="connect-interstitial-demo"
description="Centered 400px card with partner branding, account row, and a single primary action"
align="start"
className="p-0"
padded={false}
peekCode
wide
/>
## Use this pattern for
This pattern fits short-lived connect flows: partner authorisation and consent
(OAuth, MCP, Stripe Projects), organisation invites, marketplace and billing
connections (AWS Marketplace, Vercel install, credit redemption), and CLI or
device-code sign-in. Use the same shell for their loading, error, success, and
wrong-account states.
Do not use it for normal authenticated Studio pages. Those should use the
standard [page layout](./layout) patterns.
## Source of truth
```tsx
import { OrganizationSelector } from '@/components/interfaces/Connect/OrganizationSelector'
import {
InterstitialAccountRow,
InterstitialLayout,
LogoBox,
LogoPair,
PartnerLogo,
SupabaseLogo,
} from '@/components/layouts/InterstitialLayout'
```
## Basic shape
Use `InterstitialLayout` for the outer card, then put route-specific content in
`px-6 pb-6`. Widen the card only when the flow embeds a real tool, such as
project linking.
```tsx
<InterstitialLayout
logo={
<LogoPair
left={<PartnerLogo src={`${BASE_PATH}/img/icons/stripe-icon.svg`} alt="Stripe" />}
right={<SupabaseLogo />}
/>
}
title="Authorize Stripe Projects"
description="This will create an organization on your behalf in Supabase"
>
<div className="px-6 pb-6">
<InterstitialAccountRow displayName={displayName} />
<Button variant="primary" block>
Continue
</Button>
</div>
</InterstitialLayout>
```
```tsx
<InterstitialLayout
logo={<LogoPair left={<VercelLogo />} right={<SupabaseLogo />} />}
title="Connect Vercel project"
containerClassName="items-start"
cardClassName="max-w-[900px]"
>
<div className="px-6 pb-6">{projectLinker}</div>
</InterstitialLayout>
```
## Logos
Use `LogoPair` when the user is connecting two known services, and
`SupabaseLogo` alone for first-party flows or when the requester has no trusted
mark. `PartnerLogo` fills the 48px box edge-to-edge; `LogoBox` is for custom
inset marks or logos that need their own background. Store new partner icons in
`apps/studio/public/img/icons`.
### Pairing
| Requester logo | Header treatment |
| -------------------------------------------------------- | ---------------------------------------------------- |
| Curated partner / MCP client, or a trusted uploaded icon | `LogoPair` with requester left, `SupabaseLogo` right |
| Unknown, missing, blocked, or failed-to-load icon | `SupabaseLogo` alone. Do not invent an initial tile. |
The user is usually arriving from the third-party app. The interstitial should
confirm they are connecting to Supabase. Put the requester name in the title and
description; do not manufacture a letter avatar to fill the left side of a pair.
**Known services.** When both sides are curated (or otherwise known), pair them.
Theme-reactive tiles are fine when both marks have matching light/dark
treatment.
<ComponentPreview
name="connect-interstitial-logo-pair"
description="LogoPair when the user is connecting two services"
align="start"
className="p-0"
padded={false}
peekCode
wide
/>
**No trusted requester mark.** If the icon is missing, blocked, or fails to
load, show `SupabaseLogo` alone. Do not invent an initial tile to fill the
pair.
<ComponentPreview
name="connect-interstitial-logo-unknown"
description="No trusted requester mark: Supabase alone"
align="start"
className="p-0"
padded={false}
peekCode
wide
/>
**Uploaded organisation OAuth icons.** Icons published via Studio’s OAuth app
builder are unclassified bitmaps — we do not know if they were authored for
light or dark. Treat the pair as light on both Studio themes: fixed light tile
chrome (`border-black/10 bg-white`, `SupabaseLogo forceLight`) on both sides.
Do not invent a dark variant for the upload. Toggle the docs theme to dark to
see the light tiles hold against the Studio chrome.
<ComponentPreview
name="connect-interstitial-logo-uploaded"
description="Uploaded OAuth app icon: forced-light tiles on both sides"
align="start"
className="p-0"
padded={false}
peekCode
wide
/>
### Assets
Treat Connect logos as assets, not theme tokens.
**Default to light.** Prefer a single static light mark inside `LogoBox`.
Light assets read fine on both light and dark Studio themes. That is the
default for Connect tiles.
**Keep pairs matched.** In a `LogoPair`, both marks must use the same
treatment: both light, or both dark. Do not mix a light partner tile with a
dark-theme-only Supabase treatment, or the reverse. Theme-aware dark variants
are fine for curated partners that already have them, but then both sides of
the pair should use the dark set together.
**What not to do**
- Do not invent light/dark pairs for arbitrary remote OAuth icons.
- Do not recolour vendor SVGs with theme CSS. Monochrome identity-provider
masks (for example GitHub on sign-in) stay a separate pattern.
**Where logos come from on `/authorize`**
- Curated partner logos resolve from allowlisted `redirect_uri` hosts, or from
a trusted partner name when `redirect_uri` is localhost / loopback (local MCP
clients). Do not resolve curated logos from self-asserted `name` or `website`
on a remote host. Those pairs may use theme tiles and dark assets when the
partner has them.
- Published organisation OAuth app icons uploaded in Studio remain trusted
remote images, paired with forced-light tiles on both sides.
- Everything else falls back to `SupabaseLogo` alone.
- If the requester name looks like a known partner but `redirect_uri` is a
remote host outside that partner's allowlist, show a caution admonition.
Localhost MCP redirects are excluded.
## Account row
Use `InterstitialAccountRow` for signed-in context. Do not recreate it locally.
```tsx
<InterstitialAccountRow avatarUrl={avatarUrl} displayName={displayName} action={signOutButton} />
```
## Organisation selection
Use `OrganizationSelector` when the flow needs an organisation pick. Extend it
for new states instead of inventing a parallel card style.
```tsx
<OrganizationSelector
organizations={linkableOrganizations}
selectedSlug={selectedOrgSlug}
onSelect={setSelectedOrgSlug}
createLabel="Create new organization"
onCreate={() => setShowOrgCreationDialog(true)}
/>
```
## Actions
Prefer one full-width primary action. A full-width text button is fine for a
secondary action that still belongs in the flow.
### Action feedback
Match feedback to its scope:
- Use `FormMessage` or `FieldError` beside a field when that field needs to
change.
- Show a submission or action failure as simple destructive text below the
actions. Separate it with a subtle divider when needed for composition. Keep
the current account, selections, and actions visible so the user can retry.
- Use `Admonition` when the whole interstitial is blocked or has materially
changed state, such as an invalid link, wrong account, or partially completed
setup.
- Use a toast only for non-blocking feedback or a completed action whose
originating surface is no longer visible. A toast must not be the only
feedback for a failure the user needs to resolve on the current card.
```tsx
<InterstitialActionError error={actionError} />
```
Clear stale action feedback when the user retries or changes a relevant
selection. Error copy should say what failed and, when it is not obvious, what
the user can do next. When passive supporting copy occupies the same footer
region, replace it with the action error until the error is cleared instead of
stacking both messages.
<ComponentPreview
name="connect-interstitial-action-error"
description="Retryable action error shown beside the actions"
align="start"
className="p-0"
padded={false}
peekCode
wide
/>
## States
Keep loading, invalid, error, and success states inside the same card when the
route can explain them. Use `ShimmeringLoader` for loading, and `Admonition`
for warning, error, note, and success copy.
<ComponentPreview
name="connect-interstitial-logo-single"
description="Wrong-account warning inside the same interstitial card"
align="start"
className="p-0"
padded={false}
peekCode
wide
/>
## Copy
Use sentence case. Prefer `sign in` over `login`. Titles and primary actions
should follow `Verb -> Thing`, for example `Authorize Stripe Projects` or
`Install Vercel`.
Keep the layout title static across states and put state-specific copy in the
body. Header descriptions should stay short and should not end with a full
stop.
@@ -42,7 +42,7 @@ Keep repeated-row validation in the form schema or shared validation helper, not
Build a custom row when the cells are mixed controls, such as an input paired with a `Select`.
## Best Practices
## Best practices
1. **Always use FormItemLayout**: Use `FormItemLayout` instead of manually composing `FormItem`, `FormLabel`, `FormMessage`, and `FormDescription`.
@@ -55,9 +55,9 @@ Build a custom row when the cells are mixed controls, such as an input paired wi
4. **Use Cards for grouping**: Wrap form sections in `Card` components with `CardContent` and `CardFooter` for actions.
5. **Handle dirty state**: Show cancel buttons and disable save buttons based on `form.formState.isDirty`. Make sure you destructure `isDirty` from `form.formState` (see https://react-hook-form.com/docs/useform/formstate)
5. **Handle dirty state**: Show cancel buttons and disable save buttons based on `form.formState.isDirty`. Make sure you destructure `isDirty` from `form.formState` (see https://react-hook-form.com/docs/useform/formstate). When the form is in a dialog or sheet, also follow [Dirty form dismissal](./modality#dirty-form-dismissal) so Cancel, Escape, and backdrop ask before discarding unsaved changes.
6. **Error handling**: Always use mutations with `onSuccess` and `onError` callbacks that show toast notifications.
6. **Error handling**: Match feedback to its scope. Use `FormMessage` or `FieldError` for field validation. Show submission failures inline near the form actions when the user needs to retry or change something. Reserve toasts for non-blocking feedback or completed operations whose originating surface is no longer visible.
7. **Loading states**: Show loading states on submit buttons using the `loading` prop.
@@ -7,11 +7,13 @@ Supabase has a necessarily complex navigation system to handle multiple products
## Components
### NavMenu
### [Nav Menu](../components/nav-menu)
A horizontal list of related views within a consistent PageLayout context, allowing for clearer page-level organisation. Activating a NavMenu item should trigger a URL change.
[NavMenu component guidelines](../components/nav-menu)
### [Skip To Content](../fragments/skip-to-content)
Keyboard-accessible skip link for apps with persistent header and sidebar chrome. Hidden until focused via Tab, then slides into view so users can jump past navigation to the main landmark. See [Accessibility](../accessibility#jumping-ahead) for broader context.
## Page titles
+23
View File
@@ -0,0 +1,23 @@
import { Inter, Manrope, Source_Code_Pro } from 'next/font/google'
export const manrope = Manrope({
variable: '--font-manrope',
display: 'swap',
fallback: ['system-ui', 'Helvetica Neue', 'Helvetica', 'Arial', 'sans-serif'],
subsets: ['latin'],
})
export const inter = Inter({
variable: '--font-inter',
display: 'swap',
fallback: ['system-ui', 'Helvetica Neue', 'Helvetica', 'Arial', 'sans-serif'],
subsets: ['latin'],
})
export const sourceCodePro = Source_Code_Pro({
subsets: ['latin'],
fallback: ['Source Code Pro', 'Office Code Pro', 'Menlo', 'monospace'],
variable: '--font-source-code-pro',
display: 'swap',
weight: ['400', '500', '600', '700'],
})
+3 -3
View File
@@ -13,14 +13,14 @@
"start": "next start",
"lint": "eslint .",
"content:build": "contentlayer2 build",
"clean": "rimraf node_modules .next .turbo",
"clean": "rimraf .next .turbo tsconfig.tsbuildinfo",
"typecheck": "contentlayer2 build && tsc --noEmit -p tsconfig.json"
},
"dependencies": {
"@hookform/resolvers": "^3.1.1",
"@tanstack/react-table": "catalog:",
"contentlayer2": "0.4.6",
"common": "workspace:*",
"contentlayer2": "0.4.6",
"date-fns": "^2.30.0",
"dayjs": "1.11.13",
"eslint-config-supabase": "workspace:*",
@@ -57,6 +57,7 @@
"@types/lodash.template": "4.5.0",
"@types/react": "catalog:",
"@types/react-dom": "catalog:",
"@typescript/native": "catalog:",
"config": "workspace:*",
"mdast-util-toc": "^6.1.1",
"npm-run-all": "^4.1.5",
@@ -66,7 +67,6 @@
"tailwindcss": "catalog:",
"tsconfig": "workspace:*",
"tsx": "catalog:",
"@typescript/native": "catalog:",
"typescript": "catalog:",
"unist-builder": "3.0.0"
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 137 B

@@ -147,6 +147,7 @@ export default function Component() {
const chart = key as keyof typeof chartConfig
return (
<button
tabIndex={0}
key={chart}
data-active={activeChart === chart}
className="relative z-30 flex flex-1 flex-col justify-center gap-1 border-t px-6 py-4 text-left even:border-l data-[active=true]:bg-surface-100 sm:border-l sm:border-t-0 sm:px-8 sm:py-6"
@@ -80,7 +80,7 @@ export default function ChartComposedActions() {
showYAxis={true}
YAxisProps={{
tickFormatter: (value) => `${value}k`,
width: 80,
width: 36,
}}
isFullHeight={true}
/>
@@ -92,7 +92,7 @@ export default function ChartComposedActions() {
showYAxis={true}
YAxisProps={{
tickFormatter: (value) => `${value}k`,
width: 80,
width: 36,
}}
isFullHeight={true}
/>
@@ -93,7 +93,7 @@ export default function ComposedChartBasic() {
showYAxis={true}
YAxisProps={{
tickFormatter: (value) => `${value}k`,
width: 80,
width: 36,
}}
isFullHeight={true}
/>
@@ -129,7 +129,7 @@ export default function ComposedChartBasic() {
showYAxis={true}
YAxisProps={{
tickFormatter: (value) => `${value}k`,
width: 80,
width: 36,
}}
isFullHeight={true}
/>
@@ -73,7 +73,7 @@ export default function ChartComposedTable() {
showYAxis={true}
YAxisProps={{
tickFormatter: (value) => `${value}k`,
width: 80,
width: 36,
}}
isFullHeight={true}
/>
@@ -6,7 +6,7 @@ import {
DropdownMenuItem,
DropdownMenuTrigger,
} from 'ui'
import { Admonition } from 'ui-patterns/admonition'
import { Admonition } from 'ui-patterns/Admonition'
export default function AdmonitionButtonSplitDemo() {
return (
@@ -1,5 +1,5 @@
import { Button } from 'ui'
import { Admonition } from 'ui-patterns/admonition'
import { Admonition } from 'ui-patterns/Admonition'
export default function AdmonitionDemo() {
return (
@@ -1,5 +1,5 @@
import { Button } from 'ui'
import { Admonition } from 'ui-patterns/admonition'
import { Admonition } from 'ui-patterns/Admonition'
export default function AdmonitionDemo() {
return (
@@ -1,4 +1,4 @@
import { Admonition } from 'ui-patterns/admonition'
import { Admonition } from 'ui-patterns/Admonition'
export default function AdmonitionDescriptionOnly() {
return (
@@ -1,4 +1,4 @@
import { Admonition } from 'ui-patterns/admonition'
import { Admonition } from 'ui-patterns/Admonition'
export default function AdmonitionDestructive() {
return (
@@ -1,5 +1,5 @@
import { Button } from 'ui'
import { Admonition } from 'ui-patterns/admonition'
import { Admonition } from 'ui-patterns/Admonition'
export default function AdmonitionDemo() {
return (
@@ -1,5 +1,5 @@
import { Button, Card, CardContent, CardHeader, CardTitle } from 'ui'
import { Admonition } from 'ui-patterns/admonition'
import { Admonition } from 'ui-patterns/Admonition'
export default function AdmonitionDemo() {
return (
@@ -1,4 +1,4 @@
import { Admonition } from 'ui-patterns/admonition'
import { Admonition } from 'ui-patterns/Admonition'
export default function AdmonitionSuccess() {
return (
@@ -1,4 +1,4 @@
import { Admonition } from 'ui-patterns/admonition'
import { Admonition } from 'ui-patterns/Admonition'
export default function AdmonitionWarning() {
return (
@@ -12,7 +12,7 @@ import {
AlertDialogTrigger,
Button,
} from 'ui'
import { Admonition } from 'ui-patterns/admonition'
import { Admonition } from 'ui-patterns/Admonition'
const resetTemplate = async () => {
await new Promise((resolve) => setTimeout(resolve, 1200))
@@ -0,0 +1,33 @@
import { Button } from 'ui'
import {
AccountRow,
InterstitialActionError,
InterstitialShell,
LogoPair,
StripeLogo,
SupabaseLogo,
} from './connect-interstitial-shared'
export default function ConnectInterstitialActionError() {
return (
<InterstitialShell
logo={<LogoPair left={<StripeLogo />} right={<SupabaseLogo />} />}
title="Authorize Stripe Projects"
description="This will create an organization on your behalf in Supabase"
>
<div className="flex flex-col gap-4">
<AccountRow displayName="alex@example.com" />
<div className="flex flex-col gap-2">
<Button variant="primary" block>
Authorize Stripe Projects
</Button>
<Button variant="text" block>
Cancel
</Button>
<InterstitialActionError error="Failed to authorize Stripe Projects. Please try again." />
</div>
</div>
</InterstitialShell>
)
}
@@ -0,0 +1,30 @@
import { Button } from 'ui'
import {
AccountRow,
InterstitialShell,
LogoPair,
SignOutButton,
StripeLogo,
SupabaseLogo,
} from './connect-interstitial-shared'
export default function ConnectInterstitialDemo() {
return (
<InterstitialShell
logo={<LogoPair left={<StripeLogo />} right={<SupabaseLogo />} />}
title="Authorize Stripe Projects"
description="This will create an organization on your behalf in Supabase"
>
<div className="flex flex-col gap-4">
<AccountRow displayName="alex@example.com" action={<SignOutButton />} />
<Button variant="primary" block>
Authorize Stripe Projects
</Button>
<Button variant="text" block>
Cancel
</Button>
</div>
</InterstitialShell>
)
}
@@ -0,0 +1,27 @@
import { Button } from 'ui'
import {
AccountRow,
InterstitialShell,
LogoPair,
SignOutButton,
StripeLogo,
SupabaseLogo,
} from './connect-interstitial-shared'
export default function ConnectInterstitialLogoPair() {
return (
<InterstitialShell
logo={<LogoPair left={<StripeLogo />} right={<SupabaseLogo />} />}
title="Authorize Stripe Projects"
description="This will create an organization on your behalf in Supabase"
>
<div className="flex flex-col gap-4">
<AccountRow displayName="alex@example.com" action={<SignOutButton />} />
<Button variant="primary" block>
Authorize Stripe Projects
</Button>
</div>
</InterstitialShell>
)
}
@@ -0,0 +1,26 @@
import { Button } from 'ui'
import { Admonition } from 'ui-patterns/Admonition'
import { AccountRow, InterstitialShell, SupabaseLogo } from './connect-interstitial-shared'
export default function ConnectInterstitialLogoSingle() {
return (
<InterstitialShell
logo={<SupabaseLogo />}
title="Join organization"
description="You have been invited to Acme Labs"
>
<div className="flex flex-col gap-4">
<Admonition
type="warning"
title="Wrong account"
description="Sign in with the Supabase account that received this invite, then open the link again."
/>
<AccountRow displayName="alex@example.com" />
<Button variant="primary" block>
Sign out and continue
</Button>
</div>
</InterstitialShell>
)
}
@@ -0,0 +1,25 @@
import { Button } from 'ui'
import {
AccountRow,
InterstitialShell,
SignOutButton,
SupabaseLogo,
} from './connect-interstitial-shared'
export default function ConnectInterstitialLogoUnknown() {
return (
<InterstitialShell
logo={<SupabaseLogo />}
title="Authorize Acme"
description="Acme is requesting access to your organization"
>
<div className="flex flex-col gap-4">
<AccountRow displayName="alex@example.com" action={<SignOutButton />} />
<Button variant="primary" block>
Authorize Acme
</Button>
</div>
</InterstitialShell>
)
}
@@ -0,0 +1,40 @@
import { Button } from 'ui'
import {
AccountRow,
InterstitialShell,
LogoBox,
LogoPair,
SignOutButton,
SupabaseLogo,
} from './connect-interstitial-shared'
/** Stand-in uploaded OAuth icon: checked-in solid-colour bitmap (not a real brand). */
function UploadedAppLogo() {
return (
<LogoBox className="border-black/10 bg-white">
<img
alt="Acme"
src={`${process.env.NEXT_PUBLIC_BASE_PATH || '/design-system'}/img/icons/acme-oauth-icon.png`}
className="size-full object-cover"
/>
</LogoBox>
)
}
export default function ConnectInterstitialLogoUploaded() {
return (
<InterstitialShell
logo={<LogoPair left={<UploadedAppLogo />} right={<SupabaseLogo forceLight />} />}
title="Authorize Acme"
description="Acme is requesting access to your organization"
>
<div className="flex flex-col gap-4">
<AccountRow displayName="alex@example.com" action={<SignOutButton />} />
<Button variant="primary" block>
Authorize Acme
</Button>
</div>
</InterstitialShell>
)
}
@@ -0,0 +1,137 @@
import { ArrowRightLeft, LogOut } from 'lucide-react'
import { Avatar, AvatarFallback, Button, Card, CardContent, CardHeader, cn } from 'ui'
export function LogoBox({
children,
className,
}: {
children: React.ReactNode
className?: string
}) {
return (
<div
className={cn(
'flex size-12 items-center justify-center overflow-hidden rounded-xl border bg-muted',
className
)}
>
{children}
</div>
)
}
export function LogoPair({ left, right }: { left: React.ReactNode; right: React.ReactNode }) {
return (
<div className="flex items-center justify-center gap-2.5">
{left}
<ArrowRightLeft className="size-4 text-foreground-muted" />
{right}
</div>
)
}
export function StripeLogo() {
return (
<LogoBox className="border-[#533afd] bg-[#533afd]">
<svg viewBox="0 0 512 512" className="size-full" aria-hidden>
<path
fill="#fff"
fillRule="evenodd"
d="m132 380 248-52.593V132l-248 53.208z"
clipRule="evenodd"
/>
</svg>
</LogoBox>
)
}
export function SupabaseLogo({ forceLight = false }: { forceLight?: boolean } = {}) {
return (
<LogoBox className={forceLight ? 'border-black/10 bg-white' : 'bg-surface-75'}>
<svg viewBox="0 0 109 113" className="size-7" aria-hidden>
<path
d="M63.708 110.284c-2.86 3.601-8.658 1.628-8.727-2.97L53.974 40.063h45.22c8.19 0 12.758 9.46 7.665 15.874L63.708 110.284Z"
fill="#3ECF8E"
/>
<path
d="M45.317 2.071c2.86-3.601 8.658-1.628 8.726 2.97l.442 67.251H9.831C1.64 72.292-2.928 62.832 2.166 56.418L45.317 2.071Z"
fill="#3ECF8E"
/>
</svg>
</LogoBox>
)
}
export function AccountRow({
displayName,
action,
}: {
displayName: string
action?: React.ReactNode
}) {
return (
<Card className={cn('shadow-none', !action && 'border-muted bg-surface-200/50')}>
<CardContent
className={cn('flex gap-3 border-none', action ? 'items-center px-4 py-3' : 'p-3')}
>
<Avatar className="size-8 border border-muted">
<AvatarFallback className="text-xs">A</AvatarFallback>
</Avatar>
<div className="min-w-0 flex-1">
<p className="text-xs text-foreground-light">Signed in as</p>
<p className="truncate text-sm text-foreground">{displayName}</p>
</div>
{action}
</CardContent>
</Card>
)
}
export function InterstitialShell({
logo,
title,
description,
children,
}: {
logo: React.ReactNode
title: string
description?: string
children: React.ReactNode
}) {
return (
<div className="flex min-h-[520px] w-full items-center justify-center bg-studio px-2 py-6">
<Card className="w-full max-w-[400px] overflow-hidden">
<CardHeader className="items-center gap-0 space-y-0 border-0 px-6 py-6 text-center">
<div className="mb-4 flex justify-center">{logo}</div>
<div className="flex flex-col items-center gap-1">
<h1 className="text-balance text-lg font-medium tracking-tight text-foreground">
{title}
</h1>
{description ? (
<p className="m-0 px-3 text-balance text-sm leading-tight text-foreground-lighter">
{description}
</p>
) : null}
</div>
</CardHeader>
<div className="px-6 pb-6">{children}</div>
</Card>
</div>
)
}
export function InterstitialActionError({ error }: { error?: React.ReactNode }) {
if (!error) return null
return (
<div className="mt-3 border-t border-muted pt-5">
<p role="alert" className="text-center text-xs text-destructive text-balance">
{error}
</p>
</div>
)
}
export function SignOutButton() {
return <Button variant="default" icon={<LogOut />} className="px-2" aria-label="Sign out" />
}
@@ -1,6 +1,6 @@
import Link from 'next/link'
import { Button } from 'ui'
import { Admonition } from 'ui-patterns/admonition'
import { Admonition } from 'ui-patterns/Admonition'
const bucketId = 'user_avatars'
@@ -434,6 +434,7 @@ export default function FormPatternsPageLayout() {
<p className="text-xs text-foreground-lighter">
Drag and drop or{' '}
<button
tabIndex={0}
type="button"
onClick={() => fileUploadRef.current?.click()}
className="underline cursor-pointer hover:text-foreground-light"
@@ -316,9 +316,10 @@ export default function FormPatternsSidePanel() {
<FormControl className="col-span-6">
<div className="flex gap-4 items-center">
<button
tabIndex={0}
type="button"
onClick={() => uploadButtonRef.current?.click()}
className="flex items-center justify-center h-10 w-10 shrink-0 text-foreground-lighter hover:text-foreground-light overflow-hidden rounded-full bg-cover border hover:border-strong focus-visible:outline-brand-600"
className="flex items-center justify-center h-10 w-10 shrink-0 text-foreground-lighter hover:text-foreground-light overflow-hidden rounded-full bg-cover border hover:border-strong focus-ring"
style={{
backgroundImage: logoUrl ? `url("${logoUrl}")` : 'none',
}}
@@ -424,6 +425,7 @@ export default function FormPatternsSidePanel() {
<p className="text-xs text-foreground-lighter">
Drag and drop or{' '}
<button
tabIndex={0}
type="button"
onClick={() => fileUploadRef.current?.click()}
className="underline cursor-pointer hover:text-foreground-light"
@@ -27,7 +27,7 @@ import {
NavMenuItem,
Switch,
} from 'ui'
import { Admonition } from 'ui-patterns/admonition'
import { Admonition } from 'ui-patterns/Admonition'
import { FormItemLayout } from 'ui-patterns/form/FormItemLayout/FormItemLayout'
import { PageBreadcrumbs } from 'ui-patterns/PageBreadcrumbs'
import { PageContainer } from 'ui-patterns/PageContainer'
@@ -139,6 +139,7 @@ export default function PageLayoutAuthEmails() {
{subPages.map((page) => (
<NavMenuItem key={page.id} active={activePage === page.id}>
<button
tabIndex={0}
type="button"
aria-pressed={activePage === page.id}
className="h-full cursor-pointer appearance-none bg-transparent text-inherit"
@@ -212,6 +213,7 @@ function TemplatesPage({ onNavigateToSmtp }: { onNavigateToSmtp: () => void }) {
{AUTHENTICATION_TEMPLATES.map((template) => (
<CardContent key={template.title} className="p-0">
<button
tabIndex={0}
type="button"
className="flex w-full items-center justify-between px-6 py-4 text-left transition-colors hover:bg-surface-200"
>
@@ -248,7 +250,11 @@ function TemplatesPage({ onNavigateToSmtp }: { onNavigateToSmtp: () => void }) {
key={template.id}
className="flex h-full w-full items-center justify-between p-0 transition-colors hover:bg-surface-200"
>
<button type="button" className="flex flex-1 flex-col px-6 py-4 text-left">
<button
tabIndex={0}
type="button"
className="flex flex-1 flex-col px-6 py-4 text-left"
>
<h3 className="text-sm text-foreground">{template.title}</h3>
<p className="text-sm text-foreground-lighter">{template.purpose}</p>
</button>
@@ -263,7 +269,12 @@ function TemplatesPage({ onNavigateToSmtp }: { onNavigateToSmtp: () => void }) {
</FormControl>
)}
/>
<button type="button" className="py-6 pr-6" aria-label="Edit template">
<button
tabIndex={0}
type="button"
className="py-6 pr-6"
aria-label="Edit template"
>
<ChevronRight size={16} className="text-foreground-muted" />
</button>
</div>
@@ -261,6 +261,7 @@ export default function PageLayoutEdgeFunction() {
{pages.map((page) => (
<NavMenuItem key={page.id} active={activePage === page.id}>
<button
tabIndex={0}
type="button"
aria-pressed={activePage === page.id}
className="h-full cursor-pointer appearance-none bg-transparent text-inherit"
@@ -20,6 +20,7 @@ export default function PageNavDemo() {
{pages.map((page) => (
<NavMenuItem key={page.id} active={activePage === page.id}>
<button
tabIndex={0}
type="button"
aria-pressed={activePage === page.id}
className="h-full cursor-pointer appearance-none bg-transparent text-inherit"
@@ -0,0 +1,19 @@
import { SkipToContent } from 'ui-patterns/SkipToContent'
export default function SkipToContentDemo() {
return (
<div className="relative w-full overflow-hidden rounded-md border bg-studio">
<div className="flex items-center border-b px-4 py-3 text-sm text-foreground-muted">
Demo header / navigation
</div>
{/* Preview shows the focused appearance; production hides until Tab */}
<SkipToContent href="#skip-demo-main" className="relative left-3 top-2 translate-y-0" />
<main id="skip-demo-main" tabIndex={-1} className="outline-hidden p-6 pt-2">
<p className="text-sm text-foreground-light">
Main content landmark. In a real app, Tab once to reveal the skip link, then Enter to jump
here.
</p>
</main>
</div>
)
}
@@ -15,6 +15,7 @@ export default function SuccessCheckSelected() {
return (
<button
tabIndex={0}
key={option}
type="button"
aria-pressed={isSelected}
@@ -70,7 +70,7 @@ export default function TableRowLinkActions() {
{policies.map((policy) => (
<TableRow
key={policy.id}
className="relative cursor-pointer inset-focus"
className="relative cursor-pointer focus-inset"
onClick={(event) => {
if (event.currentTarget !== event.target) return
handlePolicyNavigation(policy.id, event)
@@ -54,7 +54,7 @@ export default function TableRowLink() {
{buckets.map((bucket) => (
<TableRow
key={bucket.id}
className="relative cursor-pointer inset-focus"
className="relative cursor-pointer focus-inset"
onClick={(event) => {
if (event.currentTarget !== event.target) return
handleBucketNavigation(bucket.id, event)
+35
View File
@@ -1351,6 +1351,11 @@ export const examples: Registry = [
type: 'components:example',
files: ['example/info-tooltip-demo.tsx'],
},
{
name: 'skip-to-content-demo',
type: 'components:example',
files: ['example/skip-to-content-demo.tsx'],
},
{
name: 'page-container-demo',
type: 'components:example',
@@ -1398,6 +1403,36 @@ export const examples: Registry = [
type: 'components:example',
files: ['example/page-layout-settings.tsx'],
},
{
name: 'connect-interstitial-demo',
type: 'components:example',
files: ['example/connect-interstitial-demo.tsx'],
},
{
name: 'connect-interstitial-action-error',
type: 'components:example',
files: ['example/connect-interstitial-action-error.tsx'],
},
{
name: 'connect-interstitial-logo-pair',
type: 'components:example',
files: ['example/connect-interstitial-logo-pair.tsx'],
},
{
name: 'connect-interstitial-logo-single',
type: 'components:example',
files: ['example/connect-interstitial-logo-single.tsx'],
},
{
name: 'connect-interstitial-logo-unknown',
type: 'components:example',
files: ['example/connect-interstitial-logo-unknown.tsx'],
},
{
name: 'connect-interstitial-logo-uploaded',
type: 'components:example',
files: ['example/connect-interstitial-logo-uploaded.tsx'],
},
{
name: 'page-layout-auth-emails',
type: 'components:example',
+59 -3
View File
@@ -9,9 +9,30 @@
@source './../../../packages/ui/src/**/*.{tsx,ts,js}';
@source './../../../packages/ui-patterns/src/**/*.{tsx,ts,js}';
@theme inline {
--font-sans:
var(--font-inter), Inter, Helvetica Neue, Helvetica, ui-sans-serif, system-ui, sans-serif;
--font-heading: var(--font-manrope, var(--font-sans));
--font-mono: var(--font-source-code-pro), 'Source Code Pro', ui-monospace, Menlo, monospace;
}
@theme {
/* added to get max-w-site */
--container-site: 128rem;
/* font sizing and weights optimized for Inter */
--text-sm: 0.8125rem;
--text-base: 0.9375rem;
--text-lg: 1rem;
--text-xl: 1.125rem;
--text-2xl: 1.375rem;
--text-3xl: 1.75rem;
--text-4xl: 2.125rem;
--text-5xl: 2.875rem;
--text-6xl: 3.625rem;
--text-7xl: 4.375rem;
--text-8xl: 5.875rem;
--text-9xl: 7.875rem;
--font-weight-normal: 450;
}
@layer base {
@@ -30,9 +51,7 @@
--chart-4: 280 65% 60%;
--chart-5: 340 75% 55%;
}
}
@layer base {
* {
@apply border-border;
}
@@ -40,13 +59,35 @@
@apply scroll-smooth;
}
body {
@apply bg-default text-foreground;
@apply bg-default text-foreground font-normal;
/* font-feature-settings: "rlig" 1, "calt" 1; */
font-synthesis-weight: none;
text-rendering: optimizeLegibility;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
code,
.code-content,
pre,
kbd,
samp,
.font-mono {
--text-xs: 0.75rem;
--text-sm: 0.875rem;
--text-base: 1rem;
--text-lg: 1.125rem;
--text-xl: 1.25rem;
--text-2xl: 1.5rem;
--text-3xl: 1.875rem;
--text-4xl: 2.25rem;
--text-5xl: 3rem;
--text-6xl: 3.75rem;
--text-7xl: 4.5rem;
--text-8xl: 6rem;
--text-9xl: 8rem;
--font-weight-normal: 400;
}
}
@layer utilities {
@@ -76,6 +117,21 @@
}
}
h1:not(.font-mono),
h2:not(.font-mono),
h3:not(.font-mono),
h4:not(.font-mono),
h5:not(.font-mono),
h6:not(.font-mono),
.h1:not(.font-mono),
.h2:not(.font-mono),
.h3:not(.font-mono),
.h4:not(.font-mono),
.h5:not(.font-mono),
.h6:not(.font-mono) {
@apply font-heading font-semibold;
}
.rdg-cell {
padding-inline: 0.5rem;
}
+2 -2
View File
@@ -22,5 +22,5 @@ NEXT_PUBLIC_MARKETPLACE_API_URL="https://fgxbxpvumhvzrhqngsyu.supabase.co"
NEXT_PUBLIC_MARKETPLACE_PUBLISHABLE_KEY="sb_publishable_VuF5ZvGqj6ODhZgN1J_vMw_YbiEs1R6"
# Supabase project containing docs content information
NEXT_PUBLIC_MARKETPLACE_API_URL="https://otqhrpbxhxkrhrnjqbba.supabase.co/"
NEXT_PUBLIC_MARKETPLACE_PUBLISHABLE_KEY="sb_publishable_ZVVKKu1s88KsSBWVYlou-g_phb2OJVQ"
NEXT_PUBLIC_SUPABASE_URL="https://otqhrpbxhxkrhrnjqbba.supabase.co/"
NEXT_PUBLIC_SUPABASE_ANON_KEY="sb_publishable_ZVVKKu1s88KsSBWVYlou-g_phb2OJVQ"
+11 -2
View File
@@ -29,8 +29,11 @@ yarn-error.log*
public/sitemap.xml
# Per-source llms files (generated by build:llms, served by www)
public/llms/
# Generated guide and reference markdown files
public/markdown/
# Generated guide and reference markdown files. manifest.json is committed
# with a placeholder empty array so middleware.ts's import always resolves,
# even when build:markdown hasn't run (e.g. in local dev, which skips it).
public/markdown/*
!public/markdown/manifest.json
public/docs.tar.gz
public/docs/
@@ -44,6 +47,12 @@ public/docs/
# scripts/federated-content/fetch-federated-content.ts
/content/guides/graphql/
/content/guides/graphql.mdx
/content/guides/deployment/terraform.mdx
/content/guides/deployment/terraform/tutorial.mdx
/content/guides/deployment/ci/
/content/guides/ai/python/
/content/guides/database/extensions/wrappers/*
!/content/guides/database/extensions/wrappers/overview.mdx
# Downloaded TypeDoc dumps under spec/reference/<lib>/<ver>/. Regenerated by
# `cd apps/docs/spec && make download.tsdoc.v2`. Hand-authored files in the
+107 -79
View File
@@ -8,13 +8,14 @@ Here are some general guidelines on writing docs for Supabase.
## General principles
Docs should be helpful, quick to read, and easy to understand. We have an audience of global readers who speak different native languages.
Write helpful, concise, and understandable documentation. We have a global audience whose members speak different native languages.
To make docs as clear as possible:
- Write for the user. Think about what task they want to complete by reading your doc. Tell them what, and only what, they need to know.
- Write like you talk. Use words and sentences that sound natural when speaking. Cut unnecessary words. Read your writing out loud to help you choose the clearest and simplest phrases.
- Each paragraph should have one topic only. Start a new paragraph whenever you change the topic. Don't worry about paragraphs being too short.
- Write like you talk. Conversational English is easier for a global audience to understand and localize. Many readers who use English as an additional language learn conversational rather than academic English. Use words and sentences that sound natural when speaking. Cut unnecessary words. Read your writing out loud to help you choose the clearest and simplest phrases.
- Prefer short, direct sentences. Express one relationship at a time, and avoid unnecessary compound structures. This makes each sentence easier to understand, localize, and interpret consistently.
- Cover one topic in each paragraph. Start a new paragraph whenever you change the topic. Don't worry about paragraphs being too short.
- Avoid using idioms and colloquialisms, such as `piece of cake`. These phrases are often specific to a region or culture.
- Refer to the reader as `you`. Don't use `we` to refer to the reader. Use `we` only to refer to the Supabase team.
@@ -31,7 +32,7 @@ Explainers help the reader to learn a topic. They are conceptual and mostly pros
- Some examples of _when_ to use it
- A high-level explanation of _how_ it works
They shouldn't include:
Explainers don't include:
- Instructions on how to use it
@@ -39,7 +40,7 @@ They shouldn't include:
Tutorials are goal-oriented. They help a reader to finish a large, complex goal, such as setting up a web app that uses multiple Supabase features.
Tutorials mix prose explanations with procedures (lists of steps for the reader to follow). They provide context for why certain instructions are given.
Tutorials mix prose explanations with procedures. Procedures are lists of steps for the reader to follow. Tutorials provide context for why certain instructions are given.
For inspiration, see [an example of a tutorial](https://supabase.com/docs/guides/getting-started/tutorials/with-nextjs).
@@ -47,22 +48,35 @@ For inspiration, see [an example of a tutorial](https://supabase.com/docs/guides
Guides are also goal-oriented, but they focus on shorter, more targeted tasks. For example, a guide might explain how to set up user login for an app.
Guides contain mostly procedures. Think of an instruction manual for building a desk: it's a list of concise steps that the user can go through quickly.
Guides contain mostly procedures: concise steps that readers can follow in sequence.
For inspiration, see [an example of a guide](https://supabase.com/docs/guides/auth/auth-email).
Begin each guide with a sentence that declares its intent, such as `This guide explains how to set up email login.` This helps readers and agents confirm that the guide matches their goal and expected outcome.
Keep procedures focused on what the reader must do. Move substantial background or conceptual explanations into a separate section or an explainer. Cross-reference the authoritative explanation instead of repeating it in the procedure. This keeps the action path scannable, gives readers optional depth, and maintains one source of truth.
- Recommended: `This guide explains how to enable Row Level Security. To learn how Row Level Security controls access, see [Row Level Security](...).`
- Not recommended: Begin with several paragraphs about how Row Level Security works before stating what the guide helps the reader do.
**Mixed information types:** When a guide contains substantial context or reference material, group sections by information type. Keep contextual and reference sections separate from the procedure group so that background information doesn't interrupt the action path.
**Navigation:** Begin a long guide with a short outline of its major section groups. Link to each group and state when a reader should use it. Don't add section navigation to a short guide when the headings are already easy to scan.
**Cross-references and glue:** Connect contextual sections to their corresponding procedures when the relationship helps readers navigate. Add a brief introduction to each section group, a transition when the information type changes, and an outcome after a procedure. Add links selectively rather than linking every adjacent section.
For inspiration, see [an example of a guide](/docs/guides/auth/auth-email-passwordless).
### Reference
References are factual and to the point. Think of dictionary entries.
They should include:
References include:
- Function parameters
- Return types
- Code samples
- Warnings for critical errors (for example, missteps that can cause data loss)
- Warnings about critical errors, such as missteps that can cause data loss
They shouldn't include:
References don't include:
- Explanations of the context for a feature
- Examples of use cases
@@ -72,7 +86,7 @@ They shouldn't include:
Most docs pages are contained in the `apps/docs/content` directory. Some docs sections are federated from other repositories, for example [`pg_graphql`](https://github.com/supabase/pg_graphql/tree/master/docs). Reference docs are generated from spec files in the `spec` directory.
You can usually identify a federated or reference doc because it uses a Next.js dynamic route (for example, `[[...slug]].tsx`). Look for the spec file import or the repo definition to find the content location.
You can usually identify a federated or reference doc because it uses a Next.js dynamic route. For example, it might use `[[...slug]].tsx`. Look for the spec file import or the repo definition to find the content location.
Example spec file import:
@@ -94,12 +108,12 @@ Check the sections for [guide structure](#guide-structure) and [reference struct
## Guide structure
The Supabase docs use [MDX](https://mdxjs.com/). Guides are written in unstructured prose as MDX documents.
The Supabase docs use [MDX](https://mdxjs.com/). Guides are MDX documents that combine concise prose with structured procedures.
Adding a new guide requires:
- YAML frontmatter
- A navigation entry (in a separate file)
- A navigation entry in a separate file
Frontmatter looks like this. `title` is mandatory. There are also optional properties that you can use to control the page display, including `subtitle`, `tocVideo`, and `hideToc`.
@@ -112,7 +126,7 @@ hideToc: true
The navigation is defined in [`NavigationMenu.constants.ts`](https://github.com/supabase/supabase/blob/master/apps/docs/components/Navigation/NavigationMenu/NavigationMenu.constants.ts).
Add an entry with the `name`, `url`, and (optional) `icon` for your page.
Add an entry with the `name`, `url`, and optional `icon` for your page.
## Reference structure
@@ -120,13 +134,13 @@ Reference docs are produced from the reference specs and library source code. A
### Common spec file
Each type of library (for example, language SDK or CLI) has a common spec file. For example, see the [spec file for the language SDKs](https://github.com/supabase/supabase/blob/master/apps/docs/spec/common-client-libs-sections.json). This file contains definitions for the common SDK functions:
Each type of library, such as a language SDK or CLI, has a common spec file. For example, see the [spec file for the language SDKs](https://github.com/supabase/supabase/blob/master/apps/docs/spec/common-client-libs-sections.json). This file contains definitions for the common SDK functions:
- **id** - Identifies the function
- **title** - Human-readable title
- **slug** - URL slug
- **product** - Supabase product that owns the function. For example, database operations are owned by `database`, and auth functions are owned by`auth`
- **type** - `function` for a structured function definition or `markdown` for a prose explainer section.
- `id`: Identifies the function
- `title`: Provides the human-readable title
- `slug`: Provides the URL slug
- `product`: Identifies the Supabase product that owns the function. For example, database operations are owned by `database`, and Auth operations are owned by `auth`.
- `type`: Uses `function` for a structured function definition or `markdown` for a prose explainer section
To add a new function, manually add an entry to this common file.
@@ -140,10 +154,10 @@ Each function contains a description, code examples, and optional notes. The par
If you're a library maintainer, follow these steps when updating function parameters or return values:
1. Get your changes merged to `master` in your library
2. This will kick off an action that automatically updates the spec file in the library's `gh-pages` branch
3. Run `make` in `/spec` of the `supabase/supabase` repo. This will regenerate all of the `tsdoc` files that the docs site uses
4. You should now see the changes you've made in the docs site locally
1. Merge your changes into the library's `master` branch.
2. Wait for the action to update the specification in the `gh-pages` branch.
3. Run `make` from `apps/docs/spec` in the `supabase/supabase` repository.
4. Verify the changes on your local documentation site.
## Content reuse
@@ -153,23 +167,31 @@ To use a partial, import it into your MDX file. You can also set up a partial to
## Components and elements
Docs include normal Markdown elements such as lists, and custom components such as admonitions (callouts).
Docs include normal Markdown elements such as lists and custom components such as admonitions, also known as callouts.
Here are some guidelines for using elements:
### Admonitions
Admonitions (or callouts) draw reader attention to an important point or an aside. They highlight important information, but get less effective if they're overused.
Admonitions draw reader attention to an important point or an aside. They highlight important information, but get less effective if they're overused.
Use admonitions sparingly. Don't stack them on top of each other.
Use an admonition when a reader might otherwise miss information that affects the outcome of their task, or when you want to separate helpful but optional guidance from the main flow. Don't use an admonition for information that belongs in the main explanation or procedure.
Use admonitions sparingly. Don't stack them on top of each other or use them as decoration.
Begin every admonition with its impact and purpose: the "so what." Use the first sentence to tell the reader why the information matters, such as what could happen, what changes, or what benefit they gain. Add background or instructions after the impact is clear.
For example:
- Recommended: `Deleting this project permanently removes its database and backups. Export any data that you want to keep before you continue.`
- Not recommended: `Before you continue, there are a few things that you should know about project deletion.`
Choose the appropriate `type` for your admonition:
- `danger` to warn the user about any missteps that could cause data loss or data leaks
- `deprecation` to notify the user about features that are (or will soon be) deprecated
- `caution` to warn about anything that could cause a bug or serious user inconvenience
- `tip` to point out helpful but optional actions
- `note` for anything else
- `danger`: Warn about actions or conditions that could cause data loss, expose sensitive data, or create another severe and difficult-to-reverse outcome. State the consequence first, and then explain how to avoid it.
- `deprecation`: Identify a deprecated feature or behavior. State how the change affects the reader, and then provide the supported alternative or migration path.
- `caution`: Warn about behavior that could cause bugs, failed operations, unexpected results, or serious inconvenience but doesn't rise to the severity of `danger`.
- `note`: Highlight an important prerequisite, constraint, clarification, or optional shortcut that doesn't represent a risk. If the information is essential to completing a step, include it in the procedure instead.
```
<Admonition type="note" title="Optional title">
@@ -189,13 +211,13 @@ Keep code lines short to avoid scrolling. For example, you can split long shell
- **JavaScript/TypeScript**
The `supabase` repo uses Prettier, which also formats JS/TS in code blocks. Your PR is blocked from merging if the Prettier check fails. Ensure that your code blocks are formatted by running `npm run format`, or by setting up auto-formatting in your IDE.
The `supabase` repository uses Prettier, which also formats JS/TS in code blocks. Your PR is blocked from merging if the Prettier check fails. From the repository root, run `pnpm format`, or set up automatic formatting in your IDE.
- **SQL**
Prefer lowercase for SQL. For example, `select * from table` rather than `SELECT * FROM table`.
Optionally specify a filename for the codeblock by including it after the opening backticks and language specifier:
Optionally specify a filename for the code block by including it after the opening backticks and language specifier:
````md
```ts environment.ts
@@ -211,6 +233,16 @@ Optionally highlight lines by using `mark=${lineNumber}`.
```
````
### Emphasis
Use **bold**, _italics_, and `code` formatting for distinct purposes. Don't use them interchangeably or to add visual emphasis alone.
- **Bold**: Mark UI labels the reader interacts with, such as buttons, menu items, and field names. For example, `Click **Save**.` Also use bold for a term the reader must not miss, such as `**Never** commit your service role key.`
- _Italics_: Introduce a new term the first time you define it, or reference a title, such as a book or a third-party product name written in italics by convention. Use italics sparingly. Don't use italics for UI labels or for general emphasis.
- `Code`: Mark anything the reader types or copies verbatim, or anything the system reads literally. This includes filenames, paths, commands, flags, environment variables, function and parameter names, configuration keys, and literal values. For example, `` Set `SUPABASE_URL` in your `.env` file. ``
If a phrase fits more than one category, pick the most specific one. A command name is `code`, not **bold**, even though the reader also interacts with it.
### Content listings
Overview and index pages use a single `<ContentListings id="..." />` component for curated link sections such as "Get started", "Next steps", "Examples", or "Resources". Refer to [`storage.data.ts`](data/content-listings/storage.data.ts) and [`storage.mdx`](content/guides/storage.mdx) for a full example.
@@ -218,7 +250,7 @@ Overview and index pages use a single `<ContentListings id="..." />` component f
**Prompt to add content listings:**
```text
Add a content listing block for [TOPIC] / [SECTION] (for example, Storage / Examples).
Add a content listing block for [TOPIC] / [SECTION]. For example, use Storage / Examples.
Follow CONTRIBUTING § Content listings in apps/docs.
Copy structure from `storageGetStarted` in apps/docs/data/content-listings/storage.data.ts.
Pick a globally-unique kebab-case id like `[topic]-[section]`.
@@ -227,11 +259,11 @@ Run `pnpm test:local lib/content-listings.test.ts` from apps/docs.
**Manually add content listings:**
1. Add or update a `ContentListingGroup` export in [`data/content-listings/[topic].data.ts`](data/content-listings/). The `id` field must be globally unique across all listing groups (e.g. `storage-get-started`, not just `get-started`) — it is used both as the lookup key and as the telemetry `listingId`.
2. Place the component inline in guide MDX, for example `<ContentListings id="storage-get-started" />`. Use a partial only when the block is reused or gated with `$Show` at the partial level.
1. Add or update a `ContentListingGroup` export in [`data/content-listings/[topic].data.ts`](data/content-listings/). The `id` field must be globally unique across all listing groups. For example, use `storage-get-started` rather than `get-started`. The ID is both the lookup key and the telemetry `listingId`.
2. Place the component inline in guide MDX, for example `<ContentListings id="storage-get-started" />`. Use a partial only when the block is reused or gated with `$Show` at the partial level. For individual items that depend on a feature flag (for example `sdk:dart`), set `feature` on the item instead of wrapping the whole listing.
3. Run `pnpm test:local lib/content-listings.test.ts` from `apps/docs`.
Code snippets for manually adding content listings are available in [`.vscode/content-listing.code-snippets`](../../.vscode/content-listing.code-snippets): `cl-data` (data export with namespaced id) and `cl-inline` (MDX component).
Code snippets for manually adding content listings are available in [`.vscode/content-listing.code-snippets`](../../.vscode/content-listing.code-snippets). Use `cl-data` for a data export with a namespaced ID. Use `cl-inline` for an MDX component.
### Footnotes
@@ -240,7 +272,7 @@ Don't use footnotes.
### Graphs
Render diagrams (flowcharts, sequence diagrams, entity-relationship diagrams, etc.) by writing a fenced code block with `mermaid` as the language. The MDX renderer routes these blocks through the shared `Mermaid` component, so theming follows light/dark mode automatically.
Render diagrams, including flowcharts, sequence diagrams, and entity-relationship diagrams, by writing a fenced code block with `mermaid` as the language. The MDX renderer routes these blocks through the shared `Mermaid` component, so theming follows light and dark mode automatically.
For the full list of supported diagram types and their syntax, see the [official Mermaid diagram reference](https://mermaid.js.org/intro/syntax-reference.html).
@@ -259,7 +291,7 @@ sequenceDiagram
```
````
Flowchart (`flowchart` accepts a direction like `LR`, `TD`, etc.):
The `flowchart` keyword accepts a direction such as `LR` or `TD`:
````mdx
```mermaid
@@ -273,9 +305,9 @@ flowchart LR
A few tips:
- Use the standard Mermaid diagram keywords (`sequenceDiagram`, `flowchart`, `erDiagram`, etc.) on the first line of the block.
- Use a standard Mermaid diagram keyword, such as `sequenceDiagram`, `flowchart`, or `erDiagram`, on the first line of the block.
- Keep diagrams focused on a single flow or concept. If a diagram gets too dense, split it into multiple smaller diagrams.
- Wrap node labels that contain special characters (`*`, `/`, spaces, punctuation) in double quotes, for example `A["content/**/*.md"]`.
- Wrap node labels that contain special characters in double quotes. Special characters include `*`, `/`, spaces, and punctuation. For example, use `A["content/**/*.md"]`.
- Don't hardcode colors. The component themes the diagram automatically so it matches both light and dark mode.
- Use diagrams to support the prose, not replace it. Explain the key takeaway in text near the diagram.
@@ -283,17 +315,34 @@ A few tips:
Images are uploaded in the `apps/docs/public/img` folder.
For vector illustrations, use `svg`. For screenshots and non-vector graphics, use `png`. (These are automatically converted to `webp` for supported browsers.)
For vector illustrations, use `.svg` files. For screenshots and non-vector graphics, use `.png` files. Supported browsers receive `.webp` versions automatically.
Redact any sensitive information, such as API keys.
### Links
Link text should be descriptive. The reader should understand where the link goes from reading the link text alone. This is important for accessibility. For example, don't use `here` as link text.
Use descriptive link text that tells the reader where the link goes. This is important for accessibility. For example, don't use `here` as link text.
But link text shouldn't be too long. Use the shortest part of the link that is descriptive enough. For example, `see the [reference section](/link)` rather than `[see the reference section](/link)`.
Keep link text concise. Use the shortest part of the link that is descriptive enough. For example, `see the [reference section](/link)` rather than `[see the reference section](/link)`.
Use relative links when linking within the `supabase.com` domain. For example, `[link to another page in Supabase docs](/docs/guides/getting-started)`.
Don't include the `https://supabase.com` origin when linking to pages on `supabase.com`. Use a `/docs/...` path for a page in Supabase docs, such as `[getting started](/docs/guides/getting-started)`. Use a site-root path for a page outside docs, such as `[open the Supabase Dashboard](/dashboard)`.
### Procedures
Use a procedure when a human or agent must perform actions to reach an outcome. The procedural format makes that expectation explicit.
Write sequential actions as an ordered list. Begin each step with an imperative verb, and include one action or a closely related set of actions per step. Give the reader enough context to know where to act.
Apply the [Information Mapping chunking principle](https://informationmapping.com/blogs/news/writing-for-the-web-the-magical-number-seven-plus-or-minus-two) to procedures. Present 7 ± 2 related steps at a time. This gives readers a manageable chunk of five to nine actions. Aim for the lower end of the range when the task is complex or unfamiliar.
If a procedure has more than nine steps, group related steps into named phases or smaller procedures. If one step contains multiple distinct actions, split it into separate steps. Don't add steps to reach a minimum. The range is a guideline for organizing information, not a required procedure length.
An apparent one-step procedure can become two steps when there is a real orientation action. For example:
1. Open a terminal in your project directory.
2. Run `supabase start`.
The first step establishes the operating context for both readers and agents. Don't add a redundant orientation step to a genuinely atomic instruction. For example, write `Click **Save**.` instead of adding `Locate the **Save** button` as a separate step.
### Lists
@@ -319,7 +368,7 @@ Don't nest lists more than two deep.
Use tabs to provide alternative instructions for different platforms or languages.
The `queryGroup` param is optional. It lets you link directly to a tab by using the query group as a query param in the URL, for example: `https://supabase.com/docs/my-page?packagemanager=ts`
The optional `queryGroup` prop lets you link directly to a tab. For this example, use `/docs/my-page?packagemanager=npm`.
```
<Tabs
@@ -344,59 +393,38 @@ The `queryGroup` param is optional. It lets you link directly to a tab by using
### Videos
Include videos as TOC (Table of Contents) videos rather than putting them in the main text.
Include videos as table of contents (TOC) videos instead of placing them in the main text.
You can define a TOC video in the page frontmatter:
```yaml
---
tocVideo: 'rzglqRdZUQE',
tocVideo: 'rzglqRdZUQE'
---
```
## Styling, formatting, and grammar
Don't worry too much about grammar rules. Grammar is useful if, and only if, it makes your writing clearer. For example, you can use sentence fragments if they're self-explanatory.
Grammar is useful when it makes your writing clearer. Use complete sentences by default because they identify the actor and action. This reduces ambiguity for readers, translators, and agents. Use sentence fragments only where they improve scanning, such as headings, labels, or short list items.
Headings guide the reader's eye and organize the page, but they don't carry information by themselves. Make the content beneath a heading understandable without relying on the heading. The first sentence can restate the heading, even if it sounds redundant. Readers often skim headings and then return to the section that interests them, so use the opening sentence to confirm the context.
Don't use parentheses for asides or supplementary information. Rewrite that information as part of the sentence or as a separate sentence. Use parentheses to introduce an acronym after spelling out its meaning, such as full-text search (FTS), or to mark an item as `(Optional)`. Parentheses that are required by Markdown links or code syntax aren't prose parentheticals.
That said, a few rules help keep the docs concise, consistent, and clear:
- Format headings in sentence case. Capitalize the first word and any proper nouns. All other words are lowercase. For example, `Set up authentication` rather than `Set Up Authentication`.
- Use the Oxford comma (a comma before the `and` that marks the last item in a list). For example, `realtime, database, and authentication` rather than `realtime, database and authentication`.
- Use the Oxford comma. Place a comma before the `and` that marks the last item in a list. For example, use `functions, tables, and indexes` rather than `functions, tables and indexes`.
- Use the present tense as much as possible. For example, `the AI assistant answers your question` rather than `the AI assistant will answer your question`.
## Word usage and spelling
Use American English. If in doubt, consult the [Merriam-Webster dictionary](https://www.merriam-webster.com/).
Here are some exceptions and Supabase-specific guidelines.
### General word usage
- **Filler words**: You can often make your writing more concise by removing these words. (Some of these words can also sound patronizing.) Most filler, marketing, and vague-verb phrases are flagged by `supa-mdx-lint` as warnings, with suggested alternatives where a direct replacement exists. Run `pnpm lint:mdx` in `apps/docs` to check your changes.
- Actually
- Easy, easily
- Just
- Let's
- Please
- Simple, simply
- **UI elements**
- Buttons are `click`ed.
- Checkboxes are `select`ed.
- Toggles are `enable`d and `disable`d.
- Labels of UI elements are bolded. For example, `Click **Confirm**.`
### Word list
- `Frontend` isn't hyphenated (not `front-end`).
- `Backend` isn't hyphenated (not `back-end`).
- `Login` is a noun. `Log in` is a verb.
- `Postgres` is capitalized, except in code, and used instead of `PostgreSQL`.
- `Setup` is a noun. `Set up` is a verb.
- `Supabase` is capitalized (not `supabase`), except in code.
- `Supabase Platform` is in title case (not `Supabase platform`).
Follow the [Supabase documentation word list](./WORD_LIST.md) for preferred spelling, capitalization, and usage. The word list includes the terminology rules checked by `supa-mdx-lint`. Run `pnpm lint:mdx` in `apps/docs` to check your changes.
## Search
Search is handled using a Supabase instance. During CI, [a script](https://github.com/supabase/supabase/blob/master/apps/docs/scripts/search/generate-embeddings.ts) aggregates all content sources (for example, guides, reference docs, etc), indexes them using OpenAI embeddings, and stores them in a Supabase database.
Search uses a Supabase instance. During CI, [a script](https://github.com/supabase/supabase/blob/master/apps/docs/scripts/search/generate-embeddings.ts) collects guides, reference documentation, and other content. The script creates OpenAI embeddings and stores the search index in a Supabase database.
Search uses a hybrid of native Postgres FTS and embedding similarity search based on [`pgvector`](https://github.com/pgvector/pgvector). At runtime, a PostgREST call triggers the RPC that runs the weighted FTS search, and an [Edge Function](https://github.com/supabase/supabase/tree/master/supabase/functions) is executed to perform the embedding search.
Search combines native Postgres full-text search (FTS) with embedding similarity search based on [`pgvector`](https://github.com/pgvector/pgvector). At runtime, a PostgREST call invokes the weighted FTS RPC. An [Edge Function](https://github.com/supabase/supabase/tree/master/supabase/functions) runs the embedding search.
+1 -1
View File
@@ -17,7 +17,7 @@ For a complete run-down on how all of our tools work together, see the main DEVE
[supabase.com/docs](https://supabase.com/docs) is a Next.js site. You can get setup by following the same steps for all of our other Next.js projects:
1. Follow the steps outlined in the Local Development section of the main [DEVELOPERS.md](https://github.com/supabase/supabase/blob/master/DEVELOPERS.md)
2. If you work at Supabase, run `dev:secrets:pull` to pull down the internal environment variables. If you're a community member, create a `.env` file and add this line to it: `NEXT_PUBLIC_IS_PLATFORM=false`
2. If you work at Supabase, from `apps/docs` run `pnpm run dev:secrets:pull` to write internal env vars to `.env.local`. If you're a community member, create `apps/docs/.env.local` and add this line: `NEXT_PUBLIC_IS_PLATFORM=false`
3. Start the local docs site by navigating to `/apps/docs` and running `pnpm run dev`
4. Visit http://localhost:3001/docs in your browser - don't forget to append the `/docs` to the end
5. Your local site should look exactly like [https://supabase.com/docs](https://supabase.com/docs)
+920
View File
@@ -0,0 +1,920 @@
# Supabase documentation word list
Use this list when you write or review Supabase documentation. It records preferred
spelling, capitalization, and usage for terms that commonly appear in developer
documentation.
This list supplements [CONTRIBUTING.md](./CONTRIBUTING.md). If the two documents
conflict, follow `CONTRIBUTING.md`. Match literal code, API names, UI labels, and
third-party product names even when they differ from this guidance, and format them
as code or UI text as appropriate.
Many unambiguous rules in this list are checked by `supa-mdx-lint`. Run
`pnpm lint:mdx` from `apps/docs` after editing MDX. A lint warning still requires
judgment: rewrite the sentence instead of applying a replacement that changes its
meaning.
## Numbers and symbols
### `+`
Don't use `+` to mean _or later_.
- Recommended: Postgres 15 or later
- Not recommended: Postgres 15+
### `&`
Use _and_ instead of `&` in prose, headings, navigation, and tables of contents.
Keep `&` when it is part of a UI label, code, or a space-constrained table or
diagram label.
## A
### abbreviations
Spell out an unfamiliar abbreviation on first use. Don't expand familiar technical
abbreviations such as API, CPU, HTML, HTTP, or SQL unless the audience needs it.
Use `for example` instead of `e.g.` when practical. If space is constrained, write
`e.g.` with both periods. Use `that is` instead of `i.e.`.
The linter warns about malformed forms of `e.g.` and about `i.e.`.
### abort
Use _stop_, _exit_, _cancel_, or _end_ in general prose. Use `abort` when it is the
name of a command, signal, API, or operation.
### above
Don't use _above_ to refer to a location in a document or UI. Link to or name the
section or control. For versions, use _later_.
### access
When possible, use a more specific verb such as _view_, _find_, _edit_, _open_, or
_use_. Keep _access_ when it accurately describes authorization or connectivity.
### admin
Use _administrator_ in prose. Use _admin_ when it is part of a product name, API,
role, command, or UI label.
### AI
You can use _AI_ without spelling out _artificial intelligence_ when the audience
is familiar with the term.
### allowlist and denylist
Use _allowlist_ and _denylist_ as nouns. Prefer a precise verb that describes the
action instead of using either term as a verb.
- Recommended: Allow requests from the IP address.
- Recommended: Add the IP address to the allowlist.
- Not recommended: Allowlist the IP address.
Don't use _blacklist_ or _whitelist_. The linter reports these terms as errors.
When a literal code item contains one of them, format the item as code and explain
what it does.
### allows you to
Use _lets you_, or make the reader the subject of the sentence.
- Recommended: You can query the table.
- Recommended: The API lets you query the table.
- Not recommended: The API allows you to query the table.
### alpha and beta
Use lowercase when describing a release stage. Preserve capitalization when it is
part of an official product name.
### among and between
Use _between_ for distinct items, even when there are more than two. Use _among_
for members of a group or items that aren't distinct.
### and/or
Rewrite to use _and_, _or_, or explicitly state that either or both apply.
### API
Use _API_ for a web API or a language-specific API. Don't use _API_ to mean an
individual method, function, class, or endpoint.
### app and application
Use _app_ for web and mobile software intended for end users. Use _application_
when it is part of an established term, such as _application programming
interface_, or when the distinction is technically useful.
### as and since
Use _because_ when you mean causation. _As_ and _since_ can be mistaken for
references to time.
### authentication and authorization
Authentication verifies an identity. Authorization determines what an
authenticated identity can access or do. Don't use the terms interchangeably.
Avoid _authN_ and _authZ_ in prose. Use _authentication_ and _authorization_.
### auto-
Follow the spelling established by the relevant technology. Common closed forms
include _autoscaling_, _autofill_, and _autogenerate_. Don't invent a hyphenated
variation when an established form exists.
## B
### backend
Write _backend_, not _back-end_ or _back end_.
### base64
Use _base64_ in general prose. Use the capitalization required by a formal name or
literal code item.
### below
Don't use _below_ to refer to a location in a document or UI. Link to or name the
section or control. For versions, use _earlier_.
### black-box, gray-box, and white-box
Prefer a description of what the monitoring or testing method can observe. If the
established term is necessary, define it on first use.
### boolean
Use the spelling and capitalization of the programming-language type when
referring to code. Use lowercase _boolean_ for the abstract data type and uppercase
_Boolean_ for Boolean logic.
### button
Use _button_ only for an element that is actually a button. In desktop
instructions, users _click_ a button. Preserve the exact button label and format
it in bold.
## C
### can, may, might, must, and should
- Use _can_ for ability, permission, or an optional action.
- Use _might_ for possibility or an uncertain outcome.
- Reserve _may_ for policy or legal guidance when possible.
- Use _must_ or _need to_ for a requirement.
- Avoid ambiguous _should_. State whether an action is required, recommended, or
optional.
### checkboxes
Users _select_ and _clear_ checkboxes. Don't use _check_, _uncheck_, or _deselect_
for these actions.
### click
Use _click_ for buttons, links, and other controls in a desktop interface. Don't
write _click on_. Use _tap_ when the environment is specifically a touch
interface.
### click here
Don't use _click here_ or _here_ as link text. Describe the destination or action.
### client
In API documentation, a _client_ is usually an app that sends requests. Don't use
_client_ as an abbreviation for _client library_ when that could be ambiguous.
Use _concurrent connections_, not _concurrent clients_, when discussing database
connections. The linter checks this usage.
### codebase
Write _codebase_, not _code base_.
### command-line interface
Name the specific interface, such as _Supabase CLI_. Use _CLI_ after the name is
clear.
### config
Use _configuration_ in general prose. Keep _config_ when referring to a literal
file, command, property, or established technical name.
### console and dashboard
Use the product's official name. Don't use _console_ and _dashboard_
interchangeably, and don't call a UI a dashboard unless it presents a dashboard.
Use _Supabase Dashboard_ for the Supabase product.
### currently
Avoid _currently_ when the sentence describes the product's present behavior.
State the behavior directly.
## D
### data
Treat _data_ as a singular mass noun: _the data is_ and _less data_.
### data center
Write _data center_, not _datacenter_.
### data source
Use _data source_ in prose. Preserve `datasource` when it is a code item or
official product term.
### data type
Write _data type_, not _datatype_.
### deprecate
Use _deprecated_ when use is discouraged, usually because support will end. Don't
use it to mean _removed_, _deleted_, or _unavailable_.
### dialog
Use _dialog_ for a UI element that presents information or asks for input. Don't
use _dialogue_ or _popup_.
### directory and folder
Use _directory_ in command-line contexts and _folder_ in graphical interfaces.
Match the product UI when it uses a specific term.
### disable
Use _disable_ or _turn off_ for an available feature or option. Don't use
_disabled_ to mean that something is broken or unavailable.
### display
_Display_ is a transitive verb and requires an object.
- Recommended: The Dashboard displays the query results.
- Recommended: The query results appear.
- Not recommended: The query results display.
### docs
Use _documentation_ in prose. Use _docs_ in informal contributor instructions,
repository paths, URLs, or established product names.
### dropdown
Prefer the specific control name, such as _list_ or _menu_. Use _dropdown_ only
when the distinction matters, and don't use _drop-down_.
### dummy
Don't use _dummy_ for placeholders or sample values. Use _placeholder_, _sample_,
or a name that describes the value's role. For the statistical concept commonly
called a dummy variable, use _indicator variable_ or another established,
context-appropriate term.
## E
### easy, quick, and simple
Avoid claiming that a task is _easy_, _quick_, or _simple_. These words can be
subjective and usually add no information. The linter warns about _easy_,
_easily_, _quickly_, _simple_, and _simply_.
### email
Write _email_, not _e-mail_. Don't use _email_ as a verb; use _send email_.
### enable
Use _enable_ or _turn on_ consistently for activating a feature. When describing
capability, prefer _lets you_ over _enables you_.
### endpoint
Write _endpoint_, not _end point_. Don't use _endpoint_ when the more specific
term is _function_, _method_, or _route_.
### enter
Use _enter_ for adding text to a field. Use _type_ only when the physical act of
typing matters.
### etc.
Avoid _etc._, _and so on_, and _and more_. Introduce a non-exhaustive list with
_including_, _such as_, or _for example_.
### execute
Use _run_ when it has the same meaning. Keep _execute_ when it is the precise
technical term, such as an execute permission or query execution plan.
### extract
Use _extract_ instead of _unarchive_, _uncompress_, _untar_, or _unzip_ in prose.
Preserve literal command names.
## F
### fail over and failover
Use _fail over_ as a verb. Use _failover_ as a noun or adjective.
### filename
Write _filename_, not _file name_.
### file system
Write _file system_, not _filesystem_, unless the latter is part of a code item or
official name.
### fill in and fill out
Users _fill in_ individual fields and _fill out_ an entire form.
### first person
Address the reader as _you_. Don't use singular first person (_I_, _me_, _my_, or
_mine_); the linter reports it as an error.
Use _we_ only when it clearly refers to Supabase, not when it means the writer and
reader together.
### foo, bar, and baz
Use meaningful placeholder names that help explain the example. Keep conventional
placeholder names only when the convention itself is relevant.
### frontend
Write _frontend_, not _front-end_ or _front end_.
## H
### hardcode and hardcoded
Write _hardcode_ and _hardcoded_ without a hyphen.
### health and healthy
When possible, state the observable condition, such as _responding_, _available_,
or _passing its health check_. Don't use _healthy_ when it could be ambiguous or
anthropomorphic.
### higher and lower
For version ranges, use _later_ and _earlier_, not _higher_ and _lower_.
### hover
Use _hold the pointer over_ when the reader must wait for the interface to react.
Use _point to_ when no waiting is required.
### HTTPS
Write _HTTPS_, not _HTTPs_.
## I
### ID
Write _ID_, not _Id_ or _id_, except when matching code. Use _identifier_ when it
is clearer.
### impact
Use _impact_ as a noun. Prefer _affect_ as the verb.
- Recommended: The change affects performance.
- Not recommended: The change impacts performance.
### index
Use _indexes_ as the plural in database documentation. Use _indices_ only in
domains where it is the established term.
### ingest
Use _import_, _load_, or _copy_ for simple data movement. Use _ingest_ when the
operation also performs substantial processing.
### in order to
Use _to_ unless _in order to_ is necessary to prevent ambiguity. The linter warns
about _in order to_.
### inline
Write _inline_, not _in-line_.
### internet
Use lowercase _internet_ except at the beginning of a sentence.
## J
### just
Remove _just_ when it is filler. If it means _only_ or _previously_, use the more
specific word. The linter warns about _just_.
## K
### key
Don't use _key_ to mean _important_. When referring to a technical key, identify
the kind of key on first use.
### key-value pair
Write _key-value pair_, not _key/value pair_ or _key value pair_.
### kill
Use _stop_, _exit_, _cancel_, or _end_ in general prose. Preserve _kill_ for
literal commands, signals, and established technical operations.
## L
### later and earlier
Use _later_ and _earlier_ for version ranges.
- Recommended: Version 2.2 or later
- Not recommended: Version 2.2 or higher
### latest, new, and soon
Avoid time-relative descriptions that become stale. Provide a version, date, or
specific product state instead.
### leverage
Use _use_ or a more specific verb. The linter warns about _leverage_.
### lifecycle
Write _lifecycle_, not _life cycle_ or _life-cycle_.
### login and log in
Use _login_ as a noun or adjective and _log in_ as a verb. Follow the terminology
in the product UI when it uses _sign in_.
- Recommended: Open the login page, and then log in.
- Not recommended: Login to the Dashboard.
## M
### marketing language
Describe measurable behavior instead of making promotional claims. The linter
warns about:
- _best in class_ and _best-in-class_
- _cutting edge_ and _cutting-edge_
- _effortlessly_
- _game changer_ and _game-changer_
- _hassle free_ and _hassle-free_
- _powerful_
- _seamlessly_
### master and slave
Don't use _master_ and _slave_ together. Prefer terms that describe the
relationship accurately, such as _primary and replica_, _controller and worker_,
or _publisher and subscriber_.
When a literal code item uses either term, format it as code, explain it, and use
the preferred term afterward.
### media type
Use _media type_ rather than _MIME type_. Use _content type_ when referring to the
`Content-Type` HTTP header or when it prevents ambiguity.
### microservices
Write _microservices_, not _micro-services_.
### might
Use _might_ for possibility or an uncertain outcome.
### must
Use _must_ or _need to_ for a requirement. Don't use _must_ for a recommendation.
## N
### native
Use a more precise term when possible, such as _built-in_,
_platform-specific_, or _compiled_. Don't use _native_ to describe people.
### numbers in product versions
Write an explicit comparison, such as _version 3.0 or later_. Don't use _newer_,
_older_, _higher_, _lower_, or a trailing `+`.
## O
### OAuth 2.0
Write _OAuth 2.0_, not _OAuth2_, _OAuth 2_, or _Oauth_.
### obviously and of course
Remove these phrases. They can sound dismissive and don't help the reader. The
linter warns about both.
### once
Use _after_ if that is what you mean. Use _once_ only to mean one time.
### on-premises
Write _on-premises_, not _on-premise_, _on premise_, or _on prem_.
## P
### performant
Use a measurable or specific description, such as _lower latency_, _uses less
memory_, or _handles more concurrent connections_.
### persist
Avoid using _persist_ as a transitive verb.
- Recommended: Store the session.
- Recommended: Make the session persistent.
- Not recommended: Persist the session.
### plain text and plaintext
Use _plain text_ in general contexts. Use _plaintext_ in cryptography.
### please
Don't use _please_ in normal instructions. Use it only when asking permission,
apologizing for an inconvenience, or requesting an action that primarily benefits
Supabase. The linter warns about _please_.
### plugin
Use _plugin_ as a noun and _plug in_ as a verb.
### popup
Use the specific UI element, such as _dialog_, _menu_, or _window_. Don't use
_popup_ or _pop-up_ as a generic noun.
### Postgres
Use _Postgres_, not _PostgreSQL_, outside code and literal third-party names. The
linter checks this usage.
### powered by
Prefer _with_, _by_, or _through_, depending on the relationship. The linter warns
about _powered by_.
### prior to and subsequent to
Use _before_ and _after_. The linter checks both phrases.
## R
### read-only
Always hyphenate _read-only_.
### Realtime
Capitalize _Realtime_ when referring to the Supabase product. Use lowercase
_real-time_ as an adjective with its ordinary meaning.
### repository
Prefer _repository_ in documentation prose. _Repo_ is acceptable in informal
contributor instructions and when space is constrained.
### retry
Use _retry_ as a verb or noun. Write around _retriable_, _retryable_, _triable_,
and _tryable_ when practical.
### run time and runtime
Use _runtime_ for an execution environment. Use _run time_ for the time when a
program runs or the duration of a run.
## S
### sanity check
Use _preliminary check_, _confidence check_, or a description of what the check
validates.
### screenshot
Use _screenshot_ as a noun. Use _take a screenshot_, not _screenshot_ as a verb.
Redact secrets and personal information from screenshots.
### select
Use _select_ for choosing an item, selecting text, or marking a checkbox. Preserve
the exact UI label in bold.
### sensitive and confidential
_Sensitive data_ is data whose disclosure might cause harm. _Confidential data_ is
protected against unauthorized access. Use the term that describes the relevant
risk or control.
### setup and set up
Use _setup_ as a noun or adjective and _set up_ as a verb.
- Recommended: Complete the setup to set up authentication.
- Not recommended: Setup authentication.
### singular they
Use _they_, _them_, and _their_ as gender-neutral singular pronouns. Don't use
_s/he_, _he/she_, _(s)he_, or _him/her_. The linter reports these forms as errors.
### slang abbreviations
Don't use internet slang in documentation. The linter warns about _tl;dr_, _ymmv_,
_rtfm_, _imo_, and _fwiw_.
### spin up
Use _create_ or _start_ unless you are literally describing a spinning disk.
### SQL
Write _a SQL query_, not _an SQL query_. Use lowercase SQL keywords in code
examples unless uppercase is required by the surrounding convention.
### SSH
Don't use _SSH_ or `ssh` as a verb.
- Recommended: Connect to the server by using SSH.
- Recommended: Use the `ssh` command.
- Not recommended: SSH into the server.
### startup and start up
Use _startup_ as a noun or adjective and _start up_ as a verb.
### Supabase
Capitalize _Supabase_ outside code. Use _Supabase Platform_ with both words
capitalized. Match literal package names, commands, URLs, and code.
## T
### table name
Write _table name_ as two words. Format a specific table name as code.
### target
Avoid using _target_ as a verb for people. Use _intended for_, _designed for_, or
another description of the audience.
### terminate
Use _stop_, _exit_, _cancel_, or _end_ unless _terminate_ has a specific technical
meaning in the documented context.
### third party and third-party
Use _third party_ as a noun and _third-party_ as an adjective. Don't abbreviate
either form with `3rd`.
### this and that
Add a noun after _this_ or _that_ when the reference could be unclear.
- Recommended: This setting controls connection pooling.
- Not recommended: This controls connection pooling.
### timeout and time out
Use _timeout_ as a noun or adjective and _time out_ as a verb.
### timestamp
Write _timestamp_, not _time stamp_.
### time zone and time-zone
Use _time zone_ as a noun and _time-zone_ as an adjective.
### toggles
Users _enable_ and _disable_ features with toggles. Match and bold the visible
label. Don't instruct the reader to _click the toggle_ when the intended state can
be stated directly.
## U
### UI
Use the specific interface or page name when possible. Use _UI_ only when
discussing a user interface as a general concept.
Match visible UI labels exactly and format them in bold. Describe the element with
the correct noun when it improves clarity, such as _the **Connect** button_ or
_the **Database password** field_.
### URL
Use _URL_, not _web address_, when writing for developers. Use descriptive link
text rather than exposing a URL unless the URL itself is the subject.
### user
Address the reader as _you_. Use _user_ for a person who uses the software that
the reader is building or administering.
### utilize
Use _use_. Use _utilization_ only when referring to the measured proportion of a
resource in use. The linter warns about forms of _utilize_ and _utilise_.
## V
### vague verbs
Describe the concrete action. The linter suggests:
- _view and resolve errors_ instead of _handle errors_
- _create, edit, or delete tables_ instead of _manage tables_
- _query and update data_ instead of _work with data_
Choose a different precise verb if the suggested replacement doesn't match the
actual operation.
### versus
Write _versus_ in prose, not _vs._ Use `vs` only when it is part of a literal name
or when space is constrained.
## W
### web
Use lowercase _web_. Use the capitalization established by formal names such as
_WebAssembly_.
### we
Don't use _we_ to mean the writer and reader together. Use _you_ for the reader.
_We_ is acceptable when it unambiguously means Supabase.
### while
Use _while_ for events that occur at the same time. Use _although_ or _whereas_
for contrast. Use _while_, not _whilst_; the linter checks _whilst_.
### will and would
Use present tense for current product behavior. Use _will_ for an actual future
event, not a predictable result. Replace _would_ with _can_ when describing
capability.
### workload
Use a more specific term, such as _app_, _service_, _database_, or _job_, when the
meaning is known. If _workload_ is the established technical term, define its
scope on first use.
## Y
### you
Address the reader as _you_. Use _user_ only for a person who uses the software
that the reader is developing or administering.
## Lint-enforced phrase groups
The alphabetical entries explain the intent behind the rules. This section mirrors
the exact terminology checks configured in
`supa-mdx-lint/Rule004ExcludeWords`. Update this section when those rules change.
### Filler
The linter warns about _actually_, _easily_, _easy_, _just_, _let's_,
_obviously_, _of course_, _please_, _quickly_, _simple_, _simply_, and
_that's it_. Remove the term or state the intended meaning directly.
### Marketing language
The linter warns about _best in class_, _best-in-class_, _cutting edge_,
_cutting-edge_, _effortlessly_, _game changer_, _game-changer_, _hassle free_,
_hassle-free_, _powerful_, and _seamlessly_. Describe specific behavior or
measurable results instead.
### Vague verbs
The linter suggests _view and resolve errors_ for _handle errors_, _create, edit,
or delete tables_ for _manage tables_, and _query and update data_ for _work with
data_. Use a different precise replacement when the suggestion doesn't match the
operation.
### Apologies
The linter warns about _oops_ and _sorry_. State what happened directly. Apologize
only when an apology is genuinely useful to the reader.
### First person
The linter reports _I_, _I'm_, _me_, _my_, and _mine_ as errors. Address the
reader as _you_ and use an explicit noun for other actors.
### Gender-neutral pronouns
The linter reports _s/he_, _he/she_, _(s)he_, and _him/her_ as errors. Use the
singular _they_ or rewrite the sentence.
### Inclusive language
The linter reports these terms as errors:
- _mankind_: use _humankind_ or _people_
- _manmade_: use _manufactured_, _artificial_, or _synthetic_
- _middleman_: use _intermediary_
- _blacklist_: use _denylist_ or a more precise term
- _whitelist_: use _allowlist_ or a more precise term
### Abbreviations
The linter corrects _eg._ and _eg_ to _e.g._. It replaces _i.e._, _ie._, and
_ie_ with _that is_. Prefer _for example_ and _that is_ in prose when space
allows.
### Powered by
The linter warns about _powered by_. Use _with_, _by_, or _through_, depending on
the relationship.
### Preferred usage
The linter suggests:
- _Postgres_ for _PostgreSQL_
- _concurrent connections_ for _concurrent clients_
- _use_ for _utilize_ and _utilise_
- _uses_ for _utilizes_ and _utilises_
- _using_ for _utilizing_ and _utilising_
### Direct, concise language
The linter warns about these phrases:
- _aforementioned_: name the item
- _amongst_: use _among_
- _endeavor_ or _endeavour_: use _try_
- _facilitate_: use _help_ or describe the action
- _for the purpose of_: use _to_
- _in order to_: use _to_
- _leverage_: use _use_ or a more precise verb
- _prior to_: use _before_
- _subsequent to_: use _after_
- _whilst_: use _while_
### Internet slang
The linter warns about _tl;dr_, _ymmv_, _rtfm_, _imo_, and _fwiw_. Write out the
meaning or remove the aside.
## Attribution
Portions of this word list are modifications based on work created and shared by
Google and used according to the terms of the
[Creative Commons Attribution 4.0 License](https://creativecommons.org/licenses/by/4.0/).
See the
[Google developer documentation style guide word list](https://developers.google.com/style/word-list)
for the original work. Supabase-specific guidance and adaptations are maintained
in this repository.
+5
View File
@@ -0,0 +1,5 @@
export const corsHeaders = {
'Access-Control-Allow-Origin': '*',
'Access-Control-Allow-Methods': 'POST, OPTIONS',
'Access-Control-Allow-Headers': 'content-type',
}
Loaded 100 of 1894 files, more files were not shown because too many files have changed in this diff. Show more