Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
15 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
70 changes: 49 additions & 21 deletions .claude/CLAUDE.md
Original file line number Diff line number Diff line change
@@ -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 install # install dependencies
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

## Studio
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
- `vitest` / `vercel-composition-patterns` — generic unit-testing and React composition references

Pages router. Co-locate sub-components with parent. Avoid barrel re-export files.
## Studio

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.
2 changes: 1 addition & 1 deletion .claude/skills/studio-ui-patterns/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -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:

Expand Down
2 changes: 1 addition & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -146,7 +146,7 @@ export default function Component() {
{['desktop', 'mobile'].map((key) => {
const chart = key as keyof typeof chartConfig
return (
<button
<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"
Expand Down
22 changes: 22 additions & 0 deletions apps/design-system/__registry__/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2634,6 +2634,28 @@ export const Index: Record<string, any> = {
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",
Expand Down
1 change: 1 addition & 0 deletions apps/design-system/components/click-counter.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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"
>
Expand Down
1 change: 1 addition & 0 deletions apps/design-system/components/color-palette.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@ const ColorPalette = () => {
const isCopied = copied === reference
return (
<button
tabIndex={0}
key={step * 100}
type="button"
onClick={() => handleCopy(reference)}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -81,10 +81,26 @@ project linking.

## Logos

Use `LogoPair` when the user is connecting two services, and `SupabaseLogo`
alone for first-party flows. `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`.
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"
Expand All @@ -96,25 +112,65 @@ Store new partner icons in `apps/studio/public/img/icons`.
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-single"
description="SupabaseLogo alone for first-party flows"
name="connect-interstitial-logo-unknown"
description="No trusted requester mark: Supabase alone"
align="start"
className="p-0"
padded={false}
peekCode
wide
/>

```tsx
const AwsLogo = () => (
<LogoBox className="border-[#232f3e] bg-[#232f3e]">
<img alt="AWS" src={`${BASE_PATH}/img/icons/aws-icon.svg`} className="w-8" />
</LogoBox>
)
**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.

<LogoPair left={<AwsLogo />} right={<SupabaseLogo />} />
```
<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 only,
not from self-asserted `name` or `website`. 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.

## Account row

Expand Down Expand Up @@ -150,6 +206,16 @@ 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
Expand Down
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
Original file line number Diff line number Diff line change
@@ -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>
)
}
Original file line number Diff line number Diff line change
@@ -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>
)
}
Original file line number Diff line number Diff line change
Expand Up @@ -45,9 +45,9 @@ export function StripeLogo() {
)
}

export function SupabaseLogo() {
export function SupabaseLogo({ forceLight = false }: { forceLight?: boolean } = {}) {
return (
<LogoBox className="bg-surface-75">
<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"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -316,6 +316,7 @@ 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"
Expand Down Expand Up @@ -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"
Expand Down
Loading
Loading