chore: make agent instructions agent-agnostic (#49941)

Makes the repo's AI-agent setup tool-agnostic: instructions live in
`AGENTS.md` files, skills live in `.agents/skills/`, and Claude Code,
Codex, Cursor, and Copilot all read the same sources. Also sweeps the
skills for stale and duplicated content while everything was being
moved.

**Changed:**
- Every `CLAUDE.md` (root, `apps/studio`, `apps/docs`, `apps/kb`) is now
a one-line `@AGENTS.md` import; the content moved verbatim into an
`AGENTS.md` beside it. The root one moved from `.claude/CLAUDE.md` to
the repo root for consistency.
- All skills now live in `.agents/skills/`; `.claude/skills` is a single
symlink to it (replacing the old mix of real dirs and per-skill
symlinks). Path references in `.coderabbit.yaml`, code comments, and
docs updated to match.
- `.github/copilot-instructions.md` keeps only the review policy and
points at `AGENTS.md` + `.agents/skills/`. Copilot code review reads
those natively now, so the per-topic
`.github/instructions/*.instructions.md` files were duplicates of the
skills.
- Stale skill content fixed: `studio-queries` imported a toast library
Studio doesn't use, `telemetry-standards` and `studio-testing` used
import paths that don't resolve, `safe-sql-execution` cited a boundary
test that doesn't exist, the ask-the-docs references described an
`AiPrompt` mechanism that was replaced by the ID-keyed registry, plus a
handful of wrong paths, a self-contradicting `waitForTimeout` rule, an
invalid Playwright signature, and a ConfigCat flag described as PostHog.
- `studio-error-handling` now explains when to use `AlertError` (the
default) vs `ErrorMatcher`.

**Added:**
- `apps/docs/AGENTS.md` (docs test requirements, from the old Cursor
rule)
- `studio-shortcuts` skill (from the old Copilot instruction file,
verified against the current registry)
- `ask-the-docs/reference/graphql-endpoint.md` and
`search-embeddings.md` (from the old Cursor rules, with the missing
resolver/registration/codegen steps filled in)
- Feature-flag measurement section in `telemetry-standards`

**Removed:**
- `.cursor/` (rules folded in as above; skill symlinks no longer needed)
and `.cursorignore`
- `.github/instructions/` (8 files)
- `vercel-composition-patterns/AGENTS.md` – a 946-line verbatim
concatenation of its own `rules/` directory, and a nested `AGENTS.md`
that agents could auto-load as repo instructions
- `edit-the-docs/reference/structure-and-flow.md` – word-for-word copy
of the skill's own Phase 2 text

## To test

- `readlink .claude/skills` → `../.agents/skills`, and `ls
.claude/skills/copywriting/SKILL.md` resolves
- Open a Claude Code session at the repo root and in `apps/studio` – the
imported `AGENTS.md` content should load as before
- `git diff master --stat -M` shows the skill moves as 100% renames
(content unchanged except the listed fixes)
- Spot-check a fixed claim, e.g. `import { toast } from 'sonner'` in
`studio-queries`, or the `logs.all` ESLint rule cited in
`clickhouse-logs-queries/references/codebase-integration.md`

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

- **Documentation**
- Expanded guidance for documentation workflows, GraphQL resources,
search, ClickHouse logs, React forms, Studio testing, shortcuts,
telemetry, accessibility, copywriting, and composition patterns.
- Clarified local testing, linting, build workflows, error handling, and
AI coding agent usage.
- Added contributor guidance for the knowledge base, documentation, and
Studio areas.

- **Chores**
  - Consolidated agent instructions and skill references.
- Removed obsolete editor-specific guidance, duplicate links, and
superseded documentation.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->

---------

Co-authored-by: Alaister Young <10985857+alaister@users.noreply.github.com>
This commit is contained in:
Alaister YoungandAlaister Young authored and GitHub committed 2026-09-03 21:58:29 +08:00
1 parent 6181e27b93
commit f125126aec
75 files changed
+656 -1805

No files matched your search

+17 -15
View File
@@ -64,8 +64,8 @@ about:
- Component / data-registry relationships.
- Management API OpenAPI → codegen → reference page flow.
Mermaid fences (`` ```mermaid `````) render natively on GitHub, Cursor,
and most Markdown previewers. Several reference files already embed
Mermaid fences (`` ```mermaid `````) render natively on GitHub and
most Markdown previewers. Several reference files already embed
Mermaid; reuse or adapt them rather than re-deriving.
Keep diagrams **small and one-topic**. If a diagram needs more than a
@@ -76,19 +76,21 @@ dozen nodes, split it.
Short, focused docs under `reference/`. Read whichever apply to the task
at hand — they cite each other where context matters.
| File | What's inside |
| ---------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------- |
| [`reference/adding-features.md`](./reference/adding-features.md) | Best-practices guidance for adding features to `apps/docs`. Inventory existing code first, pick the smallest viable shape, reuse pipelines. |
| [`reference/docs-app-direction.md`](./reference/docs-app-direction.md) | Refactoring vision and working norms — what new work should align with. |
| [`reference/known-issues.md`](./reference/known-issues.md) | Living list of broken, fragile, or in-flux systems. Check before depending on anything (federated docs, search, Sentry, reference-page architecture). |
| [`reference/app-map.md`](./reference/app-map.md) | Architecture cheat sheet — directories, the two-pipeline (MDX runtime + markdown export) model, heading/typography contract, telemetry, lint entries. |
| [`reference/build-pipeline.md`](./reference/build-pipeline.md) | Turborepo + pnpm lifecycle steps for building `apps/docs` — codegen, prebuild, postbuild, Vercel deploy. Mermaid diagram included. |
| [`reference/llm-agent-surface.md`](./reference/llm-agent-surface.md) | Audience routing, `llms.txt`, content negotiation, bulk exports. |
| [`reference/llm-agent-parity.md`](./reference/llm-agent-parity.md) | HTML↔markdown fidelity (e.g. AI prompts), search caveat, agent onboarding guides, in-flux wiring. |
| [`reference/federated-docs.md`](./reference/federated-docs.md) | How docs pulls markdown from external repos at build time. Routes, `pageMap`, remark/rehype plugins, link transforms, known failure modes. |
| [`reference/ci-and-lint.md`](./reference/ci-and-lint.md) | GitHub Actions on every PR — `docs_lint`, `Docs Tests`, typecheck, prettier, Vercel preview gate. Where to add a check before creating a new one. |
| [`reference/management-api-reference.md`](./reference/management-api-reference.md) | Management API OpenAPI → reference generation, including scoped PAT permission tables; why not to swap in Scalar/Redoc. |
| [`reference/gotchas.md`](./reference/gotchas.md) | Specific traps to watch for. One-liner per item. |
| File | What's inside |
| ---------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| [`reference/adding-features.md`](./reference/adding-features.md) | Best-practices guidance for adding features to `apps/docs`. Inventory existing code first, pick the smallest viable shape, reuse pipelines. |
| [`reference/docs-app-direction.md`](./reference/docs-app-direction.md) | Refactoring vision and working norms — what new work should align with. |
| [`reference/known-issues.md`](./reference/known-issues.md) | Living list of broken, fragile, or in-flux systems. Check before depending on anything (federated docs, search, Sentry, reference-page architecture). |
| [`reference/app-map.md`](./reference/app-map.md) | Architecture cheat sheet — directories, the two-pipeline (MDX runtime + markdown export) model, heading/typography contract, telemetry, lint entries. |
| [`reference/build-pipeline.md`](./reference/build-pipeline.md) | Turborepo + pnpm lifecycle steps for building `apps/docs` — codegen, prebuild, postbuild, Vercel deploy. Mermaid diagram included. |
| [`reference/llm-agent-surface.md`](./reference/llm-agent-surface.md) | Audience routing, `llms.txt`, content negotiation, bulk exports. |
| [`reference/llm-agent-parity.md`](./reference/llm-agent-parity.md) | HTML↔markdown fidelity (e.g. AI prompts), search caveat, agent onboarding guides, in-flux wiring. |
| [`reference/federated-docs.md`](./reference/federated-docs.md) | How docs pulls markdown from external repos at build time. Routes, `pageMap`, remark/rehype plugins, link transforms, known failure modes. |
| [`reference/ci-and-lint.md`](./reference/ci-and-lint.md) | GitHub Actions on every PR — `docs_lint`, `Docs Tests`, typecheck, prettier, Vercel preview gate. Where to add a check before creating a new one. |
| [`reference/management-api-reference.md`](./reference/management-api-reference.md) | Management API OpenAPI → reference generation, including scoped PAT permission tables; why not to swap in Scalar/Redoc. |
| [`reference/graphql-endpoint.md`](./reference/graphql-endpoint.md) | The `/api/graphql` endpoint under `apps/docs/resources/` — per-query folder layout, `rootSchema.ts`, connection/field utils, and the steps to add a new top-level query. |
| [`reference/search-embeddings.md`](./reference/search-embeddings.md) | The `scripts/search/` embeddings pipeline behind `searchDocs` — content sources, processing flow, change detection, and the `page` / `page_section` tables. |
| [`reference/gotchas.md`](./reference/gotchas.md) | Specific traps to watch for. One-liner per item. |
## How to use during a chat
@@ -49,28 +49,28 @@ flowchart TB
## Top-level directory layout
| Path | Purpose | Notes |
| -------------------------------------- | ---------------------------------------------------------------------------------- | -------------------------------------------------------------------------- |
| `app/` | Next.js App Router — thin route files that delegate to feature modules | Slug-based catch-alls per section, e.g. `guides/auth/[[...slug]]/page.tsx` |
| `apps/docs/content/guides/` | Source MDX for `/docs/guides/...` pages | One file per page; `_partials/` for shared blocks |
| `apps/docs/content/_partials/` | Reusable MDX snippets included via `<$Partial path="..." />` | Recursion supported |
| `apps/docs/content/troubleshooting/` | Troubleshooting articles | Some synced from GitHub issues via `Troubleshooting.script.mjs` |
| `apps/docs/components/` | React components used inside MDX | One folder per component or component family |
| `apps/docs/data/` | Typed data modules consumed by components | `.data.ts` suffix; lookup helpers live in `.utils.ts`, not here |
| `apps/docs/lib/` | Pure library code shared across pipelines | Schemas (zod), helpers (`.utils.ts`), tests |
| `apps/docs/features/docs/` | Page-level skeletons and the MDX renderer (`MdxBase`) | Shared `<Heading>` lives in `MdxBase.shared.tsx` |
| `apps/docs/features/` | Domain logic — docs rendering, auth, search/command menu, telemetry, app providers | `app.providers.tsx` wires React Query, theme, dev toolbar, command menu |
| `apps/docs/spec/` | Source-of-truth specs for reference generation | OpenAPI, SDK YAML, CLI config |
| `apps/docs/generator/` | Codegen templates for reference docs | |
| `apps/docs/resources/` | Data loaders (guide, reference, search, errors) | |
| `apps/docs/internals/` | Build-time markdown generation | **Do not import from runtime/client code** |
| `apps/docs/internals/markdown-schema/` | Per-component handlers: JSX → markdown string | File name matches the component name |
| Path | Purpose | Notes |
| -------------------------------------- | -------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------- |
| `app/` | Next.js App Router — thin route files that delegate to feature modules | Slug-based catch-alls per section, e.g. `guides/auth/[[...slug]]/page.tsx` |
| `apps/docs/content/guides/` | Source MDX for `/docs/guides/...` pages | One file per page; `_partials/` for shared blocks |
| `apps/docs/content/_partials/` | Reusable MDX snippets included via `<$Partial path="..." />` | Recursion supported |
| `apps/docs/content/troubleshooting/` | Troubleshooting articles | Some synced from GitHub issues via `Troubleshooting.script.mjs` |
| `apps/docs/components/` | React components used inside MDX | One folder per component or component family |
| `apps/docs/data/` | Typed data modules consumed by components | `.data.ts` suffix; lookup helpers live in `.utils.ts`, not here |
| `apps/docs/lib/` | Pure library code shared across pipelines | Schemas (zod), helpers (`.utils.ts`), tests |
| `apps/docs/features/docs/` | Page-level skeletons and the MDX renderer (`MdxBase`) | Shared `<Heading>` lives in `MdxBase.shared.tsx` |
| `apps/docs/features/` | Domain logic — docs rendering, auth, search/command menu, telemetry, app providers | `app.providers.tsx` wires React Query, theme, dev toolbar, command menu |
| `apps/docs/spec/` | Source-of-truth specs for reference generation | OpenAPI, SDK YAML, CLI config |
| `apps/docs/generator/` | Codegen templates for reference docs | |
| `apps/docs/resources/` | GraphQL endpoint (`/api/graphql`): per-query schema, model, resolver | See [`graphql-endpoint.md`](./graphql-endpoint.md) |
| `apps/docs/internals/` | Build-time markdown generation | **Do not import from runtime/client code** |
| `apps/docs/internals/markdown-schema/` | Per-component handlers: JSX → markdown string | File name matches the component name |
| `apps/docs/public/markdown/guides/` | Generated `.md` output; served via `/docs/guides/<path>.md` or `Accept: text/markdown` | Built by `generate-guides-markdown.ts`; see [`llm-agent-surface.md`](./llm-agent-surface.md) |
| `apps/docs/public/markdown/reference/` | Generated reference `.md` files | Built by `generate-reference-markdown.ts` |
| `apps/docs/middleware.ts` | Content negotiation for guides; bot rewrite for reference deep links | Uses `packages/common/markdown-negotiation.ts` |
| `apps/docs/app/api/guides-md/` | Serves pre-generated guide markdown to agents | Rewritten from `/docs/guides/<path>.md` |
| `apps/docs/examples/` | Copied from repo root `examples/` at build time | `codegen:examples` |
| `apps/docs/scripts/` | Build-time scripts (sitemap, markdown export, embeddings) | |
| `apps/docs/public/markdown/reference/` | Generated reference `.md` files | Built by `generate-reference-markdown.ts` |
| `apps/docs/middleware.ts` | Content negotiation for guides; bot rewrite for reference deep links | Uses `packages/common/markdown-negotiation.ts` |
| `apps/docs/app/api/guides-md/` | Serves pre-generated guide markdown to agents | Rewritten from `/docs/guides/<path>.md` |
| `apps/docs/examples/` | Copied from repo root `examples/` at build time | `codegen:examples` |
| `apps/docs/scripts/` | Build-time scripts (sitemap, markdown export, embeddings) | |
Published guide sections: `ai`, `api`, `auth`, `cron`, `database`, `deployment`,
`functions`, `getting-started`, `integrations`, `local-development`, `platform`,
@@ -79,12 +79,12 @@ Published guide sections: `ai`, `api`, `auth`, `cron`, `database`, `deployment`,
## Content types
| Type | Location | Notes |
| ---------------------- | ----------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| **Guides / tutorials** | `content/guides/` | Hand-written MDX; goal-oriented |
| **Troubleshooting** | `content/troubleshooting/` | Partly synced from GitHub issues |
| Type | Location | Notes |
| ---------------------- | ----------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| **Guides / tutorials** | `content/guides/` | Hand-written MDX; goal-oriented |
| **Troubleshooting** | `content/troubleshooting/` | Partly synced from GitHub issues |
| **Reference** | Generated from `spec/` → `features/docs/generated/**` | Spec-driven (OpenAPI, SDKSpec, ConfigSpec, CLISpec). Reference pages do **not** use the standard MDX path — see [`docs-app-direction.md`](./docs-app-direction.md) for why. Management API OpenAPI path: [`management-api-reference.md`](./management-api-reference.md). |
| **Federated** | External repos at build time | Pulled via GitHub App. See [`federated-docs.md`](./federated-docs.md) |
| **Federated** | External repos at build time | Pulled via GitHub App. See [`federated-docs.md`](./federated-docs.md) |
## Routing model
@@ -128,12 +128,12 @@ outputs in sync without a parallel data shape.
**Reference implementation: `ContentListings`** — the ID-keyed two-pipeline
pattern in production:
| Layer | Path |
| ----- | ---- |
| Data registry | `apps/docs/data/content-listings/` (`.data.ts` per topic + `index.ts`) |
| Runtime component | `apps/docs/components/ContentListings/` |
| Markdown handler | `apps/docs/internals/markdown-schema/ContentListings.ts` |
| MDX registration | `apps/docs/features/docs/MdxBase.shared.tsx` |
| Layer | Path |
| ----------------- | ---------------------------------------------------------------------- |
| Data registry | `apps/docs/data/content-listings/` (`.data.ts` per topic + `index.ts`) |
| Runtime component | `apps/docs/components/ContentListings/` |
| Markdown handler | `apps/docs/internals/markdown-schema/ContentListings.ts` |
| MDX registration | `apps/docs/features/docs/MdxBase.shared.tsx` |
MDX usage: `<ContentListings id="storage-get-started" />`. For batch
overview-page migration, see the `audit-content-listings` skill in
@@ -194,14 +194,15 @@ markdown string to substitute.
See [`ci-and-lint.md`](./ci-and-lint.md) for the full CI surface. Local
commands:
| Tool | Where | What it catches |
| ------------------------------------------- | ----------- | --------------------------------------------------------- |
| `pnpm test:local <path>` (from `apps/docs`) | per-test | Vitest suite for `lib/` and `data/` schemas |
| `pnpm format` | repo root | Prettier — run before opening a PR |
| `pnpm lint --filter=docs` | repo root | ESLint over `apps/docs` |
| `pnpm typecheck` | repo root | TS across packages |
| `pnpm build --filter=docs` | repo root | Includes markdown generation; failures here block release |
| `pnpm run supa-mdx-lint` | `apps/docs` | MDX content lint |
| Tool | Where | What it catches |
| --------------------------------------------------- | ----------- | -------------------------------------------------------------------------------------------------------------- |
| `pnpm test:local:unwatch <path>` (from `apps/docs`) | per-test | Vitest suite for `lib/` and `data/` schemas; needs local Supabase + DB reset first — see `apps/docs/AGENTS.md` |
| `pnpm format` | repo root | Prettier — run before opening a PR |
| `pnpm lint --filter=docs` | repo root | ESLint over `apps/docs` |
| `pnpm typecheck` | repo root | TS across packages |
| `pnpm build --filter=docs` | repo root | Includes markdown generation; failures here block release |
| `pnpm lint:mdx` | `apps/docs` | MDX content lint (whole `content/` tree) |
| Typos check (`.github/workflows/avoid-typos.yml`) | CI only | `runner / misspell` job at error severity — no local command; fix flagged words before merge |
Before adding a custom lint job, check whether the existing one can absorb
the check (see [`adding-features.md`](./adding-features.md) "Reuse
@@ -81,9 +81,9 @@ Production deployment is handled by **Vercel**. Supabase employees branch the
repo directly rather than fork it, so CI checks auto-run and Vercel deploys
can be authorized without the external PR security gate.
Key deployment detail: the project generates markdown files for each page
under `/docs/guides/..` as a **prebuild task**. This lets Vercel bundle these
files with middleware and functions at build time.
Markdown for every guide is generated as a **prebuild task** so Vercel can
bundle it with middleware and functions — see
[`build-pipeline.md`](./build-pipeline.md) for the lifecycle.
The **Authorize Vercel Deploys** workflow is the glue between GitHub Actions
and Vercel: it runs first to approve the deploy, then Vercel picks it up and
@@ -13,10 +13,10 @@ Specific traps to watch for. One-liner per item.
decisions.
- **`mdxFlowExpression` / `mdxTextExpression` / `mdxjsEsm` are skipped.**
Anything wrapped in `{...}` won't appear in markdown output.
- **`AiPrompt` prompts must be a `prompt={...}` prop, not children.** The
schema handler decodes the raw JS string literal from `propsFrom()`
(Prettier may emit single-quoted multiline forms). See
`internals/markdown-schema/AiPrompt.ts`.
- **`<AiPrompt id="…" />` carries only an ID.** Prompt text lives in
`data/ai-prompts.data.ts`; don't inline prompt strings in MDX. Markdown
export handles the `PromptPanel` parts via
`internals/markdown-schema/PromptPanel.ts` (drops `PromptCopy`).
- **`<$Partial>` recursion is silent.** A missing or unreadable partial is
dropped without error. Check `partials/` paths when content seems missing
from generated `.md`.
@@ -1,19 +1,20 @@
---
description: "Docs: GraphQL architecture for apps/docs/resources"
globs:
- apps/docs/resources/**/*.ts
alwaysApply: false
---
# Docs GraphQL Architecture
**Verify against live code** before depending on any path here — check
`apps/docs/resources/` and `rootSchema.ts` for the current query list.
## Overview
The `apps/docs/resources` folder contains the GraphQL endpoint architecture for the docs GraphQL endpoint at `/api/graphql`. It follows a modular pattern where each top-level query is organized into its own folder with consistent file structure.
## Architecture Pattern
Each GraphQL query follows this structure:
Each top-level query lives in its own folder. `error/` is the fullest
example; `*Types.ts` and `*Sync.ts` are optional, and `globalSearch/`
uses a `*Interface.ts` for its polymorphic result type instead. `guide/`,
`reference/`, and `troubleshooting/` hold only model + schema files: they
are result types surfaced through `searchDocs`, registered under `types`
in `rootSchema.ts`, not top-level queries.
```
resources/
@@ -30,11 +31,16 @@ resources/
└── rootSync.ts # Root sync script for syncing to database
```
## Example queries
## Folders and top-level queries
1. **searchDocs** (`globalSearch/`) - Vector-based search across all docs content
2. **error** (`error/`) - Error code lookup for Supabase services
3. **schema** - GraphQL schema introspection
| Folder | Exposes |
| ------------------ | ------------------------------------------------------------------------------------ |
| `globalSearch/` | **searchDocs** — vector search across all docs content |
| `error/` | **error** (single code lookup) and **errors** (paginated collection) |
| `guide/` | `Guide` result type (search result), no top-level query |
| `reference/` | `ReferenceCLICommand`, `ReferenceManagementApi`, `ReferenceSDKFunction` result types |
| `troubleshooting/` | `Troubleshooting` result type |
| `rootSchema.ts` | **schema** — introspection, plus the root that spreads the query objects above |
## Key Files
@@ -97,7 +103,7 @@ export const GraphQLObjectTypeNewQuery = new GraphQLObjectType({
> [!TIP]
> The types in `~/__generated__/graphql` for a new endpoint will not exist
> until the code generation is run in the next step.
> until the code generation in step 6 has run.
```typescript
import { type RootQueryTypeNewQueryArgs } from '~/__generated__/graphql'
@@ -119,9 +125,7 @@ export class NewQueryModel {
): Promise<Result<NewQueryModel[], ApiErrorGeneric>> {
// Implement data fetching logic
const result = new Result(
await supabase()
.from('your_table')
.select('*')
await supabase().from('your_table').select('*')
// Add filters based on args
)
.map((data) => data.map((item) => new NewQueryModel(item)))
@@ -130,3 +134,52 @@ export class NewQueryModel {
}
}
```
### 4. Write the resolver (`newQueryResolver.ts`)
Mirror `error/errorResolver.ts`: wrap the model call in
`Result.tryCatchFlat(..., convertUnknownToApiError, args)`, log and
`Sentry.captureException` non-user errors, and return a `GraphQLError`
whose message is `'Internal Server Error'` when `error.isPrivate()`. For
paginated results use `paginationArgs`, `createCollectionType()`, and
`GraphQLCollectionBuilder.create()` from `utils/connections.ts`. Export a
root-field object keyed by the field constant:
```typescript
export const newQueryRoot = {
[GRAPHQL_FIELD_NEW_QUERY]: {
description: 'What this query returns',
args: { id: { type: new GraphQLNonNull(GraphQLString) } },
type: GraphQLObjectTypeNewQuery,
resolve: resolveNewQuery,
},
}
```
### 5. Register it in `rootSchema.ts`
Spread the root-field object into `RootQueryType.fields` next to
`...errorRoot`. Object types that are only reachable through an
interface (search results) go in the `types` array instead.
### 6. Run codegen
```bash
cd apps/docs && pnpm run codegen:graphql
```
This prints the schema to `__generated__/schema.graphql`
(`scripts/graphqlSchema.ts`) and runs `graphql-codegen` (`codegen.ts`) to
produce `~/__generated__/graphql` — the `RootQueryType*Args` and resolver
types the model and resolver import. `predev` and `prebuild` run this
automatically.
## Related
- [`app-map.md`](./app-map.md) — where `resources/` sits in the app layout.
- [`llm-agent-surface.md`](./llm-agent-surface.md) — `searchDocs` as an
agent entry point.
- [`search-embeddings.md`](./search-embeddings.md) — the offline pipeline
that populates what `searchDocs` queries.
- [`build-pipeline.md`](./build-pipeline.md) — where `codegen:graphql` runs
in `predev` / `prebuild`.
@@ -12,38 +12,29 @@ Content that renders on the HTML page is **not** automatically present
in the `.md` export. Both pipelines must implement the same semantics.
Example: framework quickstart **AI prompt blocks** (see
supabase/supabase#47543). Each quickstart includes a bare partial:
supabase/supabase#47543). Each quickstart embeds one by ID:
```mdx
<$Partial path="ai/quickstart_prompt_nextjs.mdx" />
<AiPrompt id="flask" />
```
The partial is a self-closing `<AiPrompt>` with the prompt as a **prop**
(not children — expression children are skipped by the guides markdown
pipeline):
The prompt text lives in `data/ai-prompts.data.ts` (`aiPrompts`, keyed by
`id`); the MDX carries only the ID, following the registry pattern in
[`app-map.md`](./app-map.md). An unknown ID throws at render time.
```mdx
<AiPrompt
prompt={
'Help me add Supabase to my Next.js project. Create a Supabase project at\ndatabase.new and run the instruments table SQL. Then:\n1. …'
}
/>
```
| Pipeline | Path |
| ------------ | --------------------------------------------------------------------------------------------------------------------------------------- |
| **HTML** | `features/ui/AiPrompt.tsx` looks up the prompt and renders `PromptPanel` (copy + expand) |
| **Markdown** | `internals/markdown-schema/PromptPanel.ts` — handlers for the `PromptPanel` compound parts, registered in `generate-guides-markdown.ts` |
| Pipeline | Path |
| ------------ | ------------------------------------------------------------ |
| **HTML** | `features/ui/AiPrompt.tsx` → `PromptPanel` (copy + expand) |
| **Markdown** | `internals/markdown-schema/AiPrompt.ts` reads `props.prompt` |
`propsFrom()` stores the raw JS expression source. Prettier formats the
prop as a multiline **single-quoted** string; the schema handler must
decode that literal (trim + escapes), not only `JSON.parse` double-quoted
JSON. Symptom if decoding is wrong: generated `.md` keeps surrounding
quotes and literal `\n`.
The markdown handlers keep `PromptTitle` (bold) and `PromptContent`, and
drop `PromptCopy` (clipboard-only duplicate). `AiPrompt` itself has no
markdown handler: the HTML wrapper composes the compound children on the
client, so the `.md` export intentionally omits the copy panel.
`$Partial` variable substitution (`lib/partials.utils.ts`, wired in both
`partialsRemark` and `inlinePartials`) remains required for other nested
partials — it is no longer the AI-prompt path.
`partialsRemark` and `inlinePartials`) is still required for nested
partials; it is not involved in the AI-prompt path.
When adding content aimed at both humans and agents, always verify:
@@ -1,12 +1,10 @@
---
description: "Docs: embeddings generation pipeline (apps/docs/scripts/search)"
globs:
- apps/docs/scripts/search/**/*.ts
alwaysApply: false
---
# Documentation Embeddings Generation System
**Verify against live code** before depending on any path here. Search
infrastructure is on the [`known-issues.md`](./known-issues.md) list
(decoupled from the markdown export pipeline and considered fragile) —
check there before building on it.
## Overview
The documentation embeddings generation system processes various documentation sources and uploads their metadata to a database for semantic search functionality. The system is located in `apps/docs/scripts/search/` and works by:
@@ -66,3 +64,14 @@ The documentation embeddings generation system processes various documentation s
- **`page`** table: Stores page metadata, content, checksum, version
- **`page_section`** table: Stores individual sections with embeddings, token counts
## Related
- [`known-issues.md`](./known-issues.md) — search infrastructure fragility.
- [`llm-agent-parity.md`](./llm-agent-parity.md) — `searchDocs` caveats for
agents.
- [`graphql-endpoint.md`](./graphql-endpoint.md) — the `searchDocs` query
that reads these embeddings.
- [`build-pipeline.md`](./build-pipeline.md) — embeddings are generated
offline (`pnpm run embeddings`, run by `.github/workflows/search.yml`),
not as part of the site build.
@@ -8,8 +8,13 @@ just writing a query in the Logs Explorer UI.
## Branded SQL: never concatenate user input
All analytics log SQL must be a `SafeLogSqlFragment`, built with the helpers in
`apps/studio/data/logs/safe-analytics-sql.ts`. This is enforced by eslint, and the
branding is what keeps interpolated values from becoming injection. The key
`apps/studio/data/logs/safe-analytics-sql.ts`. Two things enforce this: the
`sql` parameter of `executeAnalyticsSql` (`apps/studio/data/logs/execute-analytics-sql.ts`)
is typed `SafeLogSqlFragment`, and an eslint `no-restricted-syntax` rule in
`apps/studio/eslint.config.cjs` blocks direct `post()`/`get()` calls to the
`logs.all` / `logs.all.otel` endpoints from any file other than
`execute-analytics-sql.ts`. The branding is what keeps interpolated values from
becoming injection. The key
exports:
- `safeSql\`...\``— a tagged template that only accepts`SafeLogSqlFragment`interpolations. Plain strings (and Postgres-branded`SafeSqlFragment`) are
@@ -25,11 +30,19 @@ exports:
- `quotedIdent(value)` — backtick-quotes a dotted identifier path after validating
each segment.
There is intentionally no exported "raw" escape hatch. Compose with `safeSql` plus
these helpers.
Compose with `safeSql` plus these helpers. The only way to run SQL that was not
built from them is the user-authored path: `untrustedLogSql(text)` marks editor
text as `UntrustedLogSqlFragment` (displayable, storable, never executable), and
`acceptUntrustedLogsSql(fragment)` promotes it to `SafeLogSqlFragment`. That
promotion is a security boundary — call it only from a run gesture (Run
button click, Cmd+Enter) or an approval-gated tool call (the AI notebook
tools). Never from render, `useEffect`, or any automatic path. The notebook
persist path also promotes cells because the writable notebook type requires
`SafeLogSqlFragment`; that is storage typing, not execution approval, and is
not precedent for promoting anywhere else.
```ts
import { analyticsLiteral, safeSql } from 'data/logs/safe-analytics-sql'
import { analyticsLiteral, safeSql } from '@/data/logs/safe-analytics-sql'
const source = 'edge_logs'
const sql = safeSql`
@@ -43,8 +56,8 @@ const sql = safeSql`
## Pick the endpoint and builder by flag
The ClickHouse path is gated by the `otelLegacyLogs` PostHog flag
(`useFlag('otelLegacyLogs')` from `common`). Keep the BigQuery path working when
The ClickHouse path is gated by the `otelLegacyLogs` ConfigCat flag
(`useFlag('otelLegacyLogs')` from `common` — ConfigCat, not PostHog). Keep the BigQuery path working when
the flag is off. Two helpers in `apps/studio/data/logs/logs-endpoint.ts` express
the split:
@@ -61,7 +74,35 @@ const endpoint = logsAllEndpointUrl(useOtel)
```
Run the fragment through `executeAnalyticsSql` (`apps/studio/data/logs/execute-analytics-sql.ts`)
against that endpoint.
against that endpoint:
```ts
import { executeAnalyticsSql } from '@/data/logs/execute-analytics-sql'
import { analyticsLiteral, quotedIdent, safeSql } from '@/data/logs/safe-analytics-sql'
// ✅ GOOD: every interpolation is sanitized.
const sql = safeSql`
SELECT timestamp, event_message
FROM ${quotedIdent(table)}
WHERE id = ${analyticsLiteral(id)}
`
await executeAnalyticsSql({
projectRef,
endpoint,
sql,
iso_timestamp_start,
iso_timestamp_end,
})
```
```ts
// 🛑 BAD: raw string interpolation. This fails to type-check at the
// executeAnalyticsSql boundary because the result is `string`, not
// `SafeLogSqlFragment`.
const sql = `SELECT * FROM ${table} WHERE id = '${id}'`
await executeAnalyticsSql({ projectRef, endpoint, sql, iso_timestamp_start, iso_timestamp_end })
```
## Follow the existing OTEL builders
File renamed without changes.
@@ -10,7 +10,7 @@ description: Safety rules for the dev toolbar, PostHog client, and feature flags
Review checklist for PRs touching the dev toolbar (`packages/dev-tools/`) and its
integration points in `packages/common/`. The toolbar surfaces telemetry events and
allows feature flag overrides during local development (expanding to staging/preview).
allows feature flag overrides in local and staging environments only.
## When This Applies
@@ -32,12 +32,12 @@ so PRs touching only those files won't auto-request review. Watch for these in t
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.
- **Build-time tree-shaking** in `index.ts`: the bundler inlines `process.env.NEXT_PUBLIC_ENVIRONMENT`, so `isToolbarEnabled = env === 'local' || env === 'staging'` makes the export ternaries static. Outside those two environments every export is a stub (`DevToolbar` and `DevToolbarTrigger` render `null`, `DevToolbarProvider` passes children through, `useDevToolbar` returns a no-op context) and the implementation modules are eliminated from the bundle. The same literal `process.env` check is duplicated in `DevToolbarContext.tsx`, `DevToolbar.tsx`, `DevToolbarTrigger.tsx`, and `feature-flags.tsx` because the bundler must see it directly — keep them in sync.
- **Runtime guards** inside the implementation: `IS_LOCAL_DEV = env === 'local'` gates the local-only pieces — the SSE event stream in `DevToolbarContext.tsx` and local-only UI in `DevToolbar.tsx` — so staging gets the toolbar without them.
**Check for:**
- Guards being removed or broadened. The toolbar is expanding to staging and preview deploys but must remain invisible in production.
- Guards being removed or broadened. The toolbar is enabled only for `local` and `staging`; preview and production builds must keep getting the stubs.
- 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.
+2 -2
View File
@@ -33,7 +33,7 @@ and clarity. Distinct from [`write-the-docs`](../write-the-docs/SKILL.md)
## Phase 2 — Restructure
Apply [reference/structure-and-flow.md](reference/structure-and-flow.md):
Apply the **Mixed information types**, **Navigation**, and **Cross-references and glue** guidance in [`apps/docs/CONTRIBUTING.md`](../../../apps/docs/CONTRIBUTING.md) (Guides section), summarized here:
1. 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.
2. 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.
@@ -66,7 +66,7 @@ Then run [`review-the-docs`](../review-the-docs/SKILL.md) local self-review (`pn
## Additional resources
- Structure SoT: [`apps/docs/CONTRIBUTING.md`](../../../apps/docs/CONTRIBUTING.md) (mixed types, navigation, glue)
- Structure ops: [reference/structure-and-flow.md](reference/structure-and-flow.md)
- Structure ops: [`apps/docs/CONTRIBUTING.md`](../../../apps/docs/CONTRIBUTING.md) — Guides: Mixed information types, Navigation, Cross-references and glue
- Pitfalls: [`write-the-docs/reference/common-pitfalls.md`](../write-the-docs/reference/common-pitfalls.md)
- Mechanics: [`write-the-docs/reference/drafting-mechanics.md`](../write-the-docs/reference/drafting-mechanics.md)
- Architecture/IA: [`ask-the-docs`](../ask-the-docs/SKILL.md)
@@ -1,29 +0,0 @@
# Structure and flow
Operational guidance for restructuring existing docs pages. The human-facing
source of truth is [`apps/docs/CONTRIBUTING.md`](../../../../apps/docs/CONTRIBUTING.md)
under Guides: **Mixed information types**, **Navigation**, and
**Cross-references and glue**. Keep this file aligned with that section.
## Classify and group
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.
## Introduction navigation
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.
## Connective text
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.
## Voice and procedure shape
Use second person, present tense, short paragraphs, and ordered steps for
sequential actions.
@@ -16,7 +16,7 @@ A practical six-stage checklist and quality standard for planning, drafting, and
## 1. Frame
_Skills:_ `/ask-the-docs` to see how the surface works today; `/pm-the-docs` for audience, stage, and cross-cutting scope calls.
_Skills:_ `ask-the-docs` to see how the surface works today; `pm-the-docs` for audience, stage, and cross-cutting scope calls.
- [ ] P: State the product stage (private/public alpha, beta, GA)
- [ ] P: Name the audience and the job they are trying to do
@@ -24,7 +24,7 @@ _Skills:_ `/ask-the-docs` to see how the surface works today; `/pm-the-docs` for
## 2. Shape
_Skill:_ `/ask-the-docs` for IA placement, architecture, and where content lives.
_Skill:_ `ask-the-docs` for IA placement, architecture, and where content lives.
- [ ] P: Pick the content type(s): tutorial (learning), how-to (a task), reference (lookup), explanation (the why). Do not mix types on one page (refer to [Diátaxis](https://diataxis.fr/))
- [ ] P: Decide where the page lives in the existing IA and what links in and out (avoid orphan pages)
@@ -32,25 +32,25 @@ _Skill:_ `/ask-the-docs` for IA placement, architecture, and where content lives
## 3. Draft
_Skill:_ `/write-the-docs` to draft net-new content grounded in Linear and the code.
_Skill:_ `write-the-docs` to draft net-new content grounded in Linear and the code.
- [ ] P: Lead with the why and the outcome, then the how/what (product story first)
- [ ] P: Include at least one runnable, copy-pasteable example that you have actually run
- [ ] E: Contribute technical depth and verify accuracy (APIs, limits, edge cases)
- [ ] P: Call out the current stage inline and any known limitations
When the work is improving an existing page (restructure, reorder, connective text, brevity) rather than authoring net-new content, use `/edit-the-docs` instead of `/write-the-docs`.
When the work is improving an existing page (restructure, reorder, connective text, brevity) rather than authoring net-new content, use `edit-the-docs` instead of `write-the-docs`.
## 4. Self-review against the bar
_Skill:_ `/review-the-docs` — [Local self-review](../review-the-docs/SKILL.md#local-self-review-no-open-pr) on your own branch before opening the PR.
_Skill:_ `review-the-docs` — [Local self-review](../../review-the-docs/SKILL.md#local-self-review-no-open-pr) on your own branch before opening the PR.
- [ ] P/E: Check the draft against "What good looks like" above before opening the PR
- [ ] P/E: Follow authoring-experience standards and tooling when available
## 5. PR review
_Skill:_ `/review-the-docs` to triage, classify, verify the build, and report.
_Skill:_ `review-the-docs` to triage, classify, verify the build, and report.
- [ ] P/E: Open the PR and request review per the rules of engagement
- [ ] Docs: Review against the published bar
@@ -162,7 +162,13 @@ buttons.
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:
clobber the user's in-progress edits. `keepDirtyValues` preserves whatever is
in `formState.dirtyFields`; typed edits and `setValue(…, { shouldDirty: true })`
populate it regardless of subscriptions, but `useFieldArray` operations
(append/remove/move) only mark fields dirty while `dirtyFields` or `isDirty`
is subscribed — so a form that combines `keepDirtyValues` with a field array
must read one of them in the owner, or the next refetch will discard array
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
+35 -34
View File
@@ -1,7 +1,7 @@
---
name: review-the-docs
description: >-
Review Supabase docs changes locally in ~/GitHub/supabase/supabase —
Review Supabase docs changes locally in your `supabase/supabase` checkout —
either an open PR (triage, classify, verify) or your own branch before
opening a PR (local self-review). Covers markdown pipeline, MDX content,
tutorials, examples, Studio links, and docs tooling. Use when asked to
@@ -31,24 +31,24 @@ For **implementing** docs fixes (Linear tickets, worktrees, platform E2E), use [
## Repository layout
| Path | Purpose |
| -------------------------------------- | --------------------------------------------------------- |
| `~/GitHub/supabase/supabase` | Main clone for review checkouts |
| `apps/docs/content/guides/` | Source MDX |
| `apps/docs/internals/` | Markdown pipeline (`generate-guides-markdown.ts`, etc.) |
| `apps/docs/internals/markdown-schema/` | Component handlers → plain markdown strings |
| `apps/docs/public/markdown/guides/` | Generated output (produced by build) |
| `apps/docs/components/` | React MDX components |
| `examples/` | Tutorial/quickstart apps referenced via `$CodeSample` |
| `apps/studio/` | Dashboard UI; may link to hosted docs |
| `.agents/skills/` | In-repo agent skills (symlinked from `.claude`/`.cursor`) |
| Path | Purpose |
| --------------------------------------- | ---------------------------------------------------------------- |
| your local `supabase/supabase` checkout | Main clone for review checkouts |
| `apps/docs/content/guides/` | Source MDX |
| `apps/docs/internals/` | Markdown pipeline (`generate-guides-markdown.ts`, etc.) |
| `apps/docs/internals/markdown-schema/` | Component handlers → plain markdown strings |
| `apps/docs/public/markdown/guides/` | Generated output (produced by build) |
| `apps/docs/components/` | React MDX components |
| `examples/` | Tutorial/quickstart apps referenced via `$CodeSample` |
| `apps/studio/` | Dashboard UI; may link to hosted docs |
| `.agents/skills/` | In-repo agent skills (canonical; `.claude/skills` symlinks here) |
## Local self-review (no open PR)
Use this on your own branch **before** opening a PR (checklist Stage 4). No `gh pr` required.
```bash
cd ~/GitHub/supabase/supabase
cd <your supabase/supabase checkout>
# Ensure you're on the feature branch, not master
git branch --show-current
git diff --name-only master...HEAD
@@ -59,8 +59,8 @@ git diff --name-only master...HEAD
3. **Run type-specific checks** from the matching sections below on the current branch (no checkout step). Typical commands:
```bash
# Content / tutorial MDX
cd apps/docs && pnpm lint:mdx -- <changed-paths>
# Content / tutorial MDX (lints the whole content/ tree; no per-file scoping)
cd apps/docs && pnpm lint:mdx
# Pipeline / schema handler
cd apps/docs && pnpm build:guides-markdown
@@ -108,17 +108,17 @@ Inspect changed files from `gh pr view` or:
gh pr diff <number> --repo supabase/supabase --name-only
```
| PR type | Path signals | Primary skill section |
| --------------------------- | --------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------- |
| **Markdown-schema handler** | `apps/docs/internals/markdown-schema/`, `generate-guides-markdown.ts` | [Schema handler review](#schema-handler-review) |
| **Pipeline / internals** | `apps/docs/internals/` (not just one new handler) | [Pipeline review](#pipeline-review) |
| **Content-only MDX** | `apps/docs/content/**` only | [Content review](#content-review) |
| **Tutorial / quickstart** | `apps/docs/content/guides/**/tutorials/`, `quickstarts/`, plus `examples/` | [Tutorial review](#tutorial-review) → also `work-linear-issue` |
| **Example app only** | `examples/**` without matching MDX | [Example review](#example-review) |
| **Studio ↔ docs links** | `apps/studio/**` | [Studio review](#studio-review) |
| **Docs UI / components** | `apps/docs/components/`, `apps/docs/features/` (no pipeline) | [Component review](#component-review) |
| **Docs tooling** | `.agents/skills/`, `.claude/skills/`, `.cursor/skills/`, `apps/docs/CONTRIBUTING.md`, `apps/docs/DEVELOPERS.md` | [Docs tooling review](#docs-tooling-review) |
| **Mixed** | Multiple path groups above | Run each applicable section; note overlap |
| PR type | Path signals | Primary skill section |
| --------------------------- | ------------------------------------------------------------------------------------------------ | -------------------------------------------------------------- |
| **Markdown-schema handler** | `apps/docs/internals/markdown-schema/`, `generate-guides-markdown.ts` | [Schema handler review](#schema-handler-review) |
| **Pipeline / internals** | `apps/docs/internals/` (not just one new handler) | [Pipeline review](#pipeline-review) |
| **Content-only MDX** | `apps/docs/content/**` only | [Content review](#content-review) |
| **Tutorial / quickstart** | `apps/docs/content/guides/**/tutorials/`, `quickstarts/`, plus `examples/` | [Tutorial review](#tutorial-review) → also `work-linear-issue` |
| **Example app only** | `examples/**` without matching MDX | [Example review](#example-review) |
| **Studio ↔ docs links** | `apps/studio/**` | [Studio review](#studio-review) |
| **Docs UI / components** | `apps/docs/components/`, `apps/docs/features/` (no pipeline) | [Component review](#component-review) |
| **Docs tooling** | `.agents/skills/`, `apps/docs/AGENTS.md`, `apps/docs/CONTRIBUTING.md`, `apps/docs/DEVELOPERS.md` | [Docs tooling review](#docs-tooling-review) |
| **Mixed** | Multiple path groups above | Run each applicable section; note overlap |
When a PR spans types (e.g. schema handler + component refactor), run **all** matching sections.
@@ -131,7 +131,7 @@ Repeat for **each PR** (bottom of stack first).
**Checkout and install:**
```bash
cd ~/GitHub/supabase/supabase
cd <your supabase/supabase checkout>
gh pr checkout <number> --repo supabase/supabase
pnpm install --filter docs... # when node_modules missing or deps changed
```
@@ -174,7 +174,7 @@ Find usages: `rg '<ComponentName' apps/docs/content/`
```bash
cd apps/docs && pnpm build:guides-markdown
# Expect: Generated 546 markdown files under public/markdown/guides/
# Expect: "Generated N markdown files" where N roughly matches the .mdx count under content/guides/
```
Inspect `public/markdown/guides/` for affected pages:
@@ -193,7 +193,7 @@ For AST refactors, link rewriting, reference markdown generation, etc.
cd apps/docs
pnpm build:guides-markdown
pnpm build:reference-markdown # when reference pipeline changed
pnpm test internals/internal-links.test.ts # when link handling changed
pnpm test:local:unwatch internals/internal-links.test.ts # when link handling changed (needs local Supabase — see apps/docs/AGENTS.md)
```
Verify both guides and reference output when `generate-reference-markdown.ts` or `internal-links.ts` changed.
@@ -206,7 +206,7 @@ MDX prose, partials, navigation — no pipeline or example changes.
```bash
cd apps/docs
pnpm lint:mdx -- <changed-paths> # or monorepo equivalent on changed files
pnpm lint:mdx # lints the whole content/ tree; filter the output to your changed paths
```
Checklist:
@@ -227,7 +227,7 @@ Tutorial MDX plus matching example app. **Read [`work-linear-issue`](https://git
```bash
# MDX lint
cd apps/docs && pnpm lint:mdx -- content/guides/getting-started/tutorials/<path>
cd apps/docs && pnpm lint:mdx # then check output for content/guides/getting-started/tutorials/<path>
# Example build (from work-linear-issue)
cd examples/<example-dir>
@@ -291,7 +291,8 @@ Agent skills, contributor docs, or skill symlink wiring — no MDX/pipeline chan
Checklist:
- [ ] Symlinks under `.claude/skills/` and `.cursor/skills/` resolve to `.agents/skills/...` (same pattern as `vitest`)
- [ ] `.agents/skills/` is the canonical location — no skill content added anywhere else
- [ ] `.claude/skills` is still a single Git symlink to `../.agents/skills` — no per-skill symlinks or copies under `.claude/`
- [ ] Cross-skill links resolve: relative for in-repo skills; absolute `docs-agent-skills` URLs only for skills that remain in that private repo
- [ ] No personal vault paths, Obsidian references, or private-process-only instructions
- [ ] `apps/docs/CONTRIBUTING.md` / `DEVELOPERS.md` pointers match skill names and checklist stages
@@ -299,7 +300,7 @@ Checklist:
```bash
# Symlink smoke check
ls -la .claude/skills/<skill-name> .cursor/skills/<skill-name>
test "$(readlink .claude/skills)" = "../.agents/skills"
test -f .claude/skills/<skill-name>/SKILL.md
# Leftover internal refs
@@ -317,7 +318,7 @@ One consolidated report after all PRs are reviewed.
```markdown
# PR review report — <author, label, or topic>
Reviewed locally at `~/GitHub/supabase/supabase`.
Reviewed locally in a `supabase/supabase` checkout.
**Stack order:** master → #NNN → … (if applicable)
@@ -125,8 +125,8 @@ These are valid ways to generate a `SafeSqlFragment`:
- `literal`
- `keyword`
- Using the safe SQL manipulation utilities:
- `joinSqlFragments`
- `trimSafeSqlFragment`
- `joinSqlFragments` (from `pg-meta`)
- `trimSafeSqlFragment` (from `apps/studio/lib/sql.ts`)
`UntrustedSqlFragments` can be generated from raw strings using
`untrustedSql()`.
@@ -399,32 +399,31 @@ or ClickHouse via the
Filter keys and values from URL parameters and UI inputs are spliced into SQL
that runs against the project's logs, so the same injection risk exists.
The brand and helpers live in `apps/studio/data/logs/safe-analytics-sql.ts`,
intentionally **disjoint** from the pg-meta `SafeSqlFragment` brand:
Analytics SQL uses its own `SafeLogSqlFragment` brand
(`apps/studio/data/logs/safe-analytics-sql.ts`), intentionally **disjoint**
from the pg-meta `SafeSqlFragment` brand. The brands are kept separate because
escape semantics differ — Postgres-safe `E'…'` strings, `::jsonb` casts, and
double-quoted identifiers are unsafe for BigQuery and/or ClickHouse, and vice
versa. Crossing the brands would silently emit unsafe SQL.
The wire boundary is `executeAnalyticsSql` in
`apps/studio/data/logs/execute-analytics-sql.ts`, analogous to pg-meta's
`executeSql`; it accepts only `SafeLogSqlFragment`, and an eslint
`no-restricted-syntax` rule in `apps/studio/eslint.config.cjs` blocks direct
`post()`/`get()` calls to the `logs.all` endpoints from any other file.
Build fragments with the helpers in `safe-analytics-sql.ts`:
- `SafeLogSqlFragment` — branded type for analytics SQL.
- `safeSql` — template tag that only accepts `SafeLogSqlFragment`
interpolations.
interpolations; plain strings and Postgres `SafeSqlFragment`s are rejected at
compile time.
- `analyticsLiteral(value)` — sanitizes string/number/boolean literals.
- `quotedIdent(name)` — validates and backtick-quotes dotted identifiers.
- `keyword(value, allowed)` — validates against an allow-list of operators.
- `keyword(value, allowed)` — resolves a value against an allow-list of
fragments (e.g. `AND`/`OR`); never returns the raw input.
- `joinSqlFragments(fragments, separator)` — composes already-branded
fragments.
The brands are kept separate because escape semantics differ — Postgres-safe
`E'…'` strings, `::jsonb` casts, and double-quoted identifiers are unsafe for
BigQuery and/or ClickHouse, and vice versa. Crossing the brands would silently
emit unsafe SQL.
The wire-boundary wrapper is `executeAnalyticsSql` in
`apps/studio/data/logs/execute-analytics-sql.ts`, analogous to pg-meta's
`executeSql`. It accepts only `SafeLogSqlFragment` for its `sql` parameter, so
raw strings are rejected at compile time. A grep-based vitest
(`apps/studio/tests/unit/lints/analytics-sql-boundary.test.ts`) prevents
regressions by failing the build if any file outside
`execute-analytics-sql.ts` calls `post()` or `get()` directly against
`logs.all` or `logs.all.otel`.
```ts
import { executeAnalyticsSql } from '@/data/logs/execute-analytics-sql'
import { analyticsLiteral, quotedIdent, safeSql } from '@/data/logs/safe-analytics-sql'
@@ -436,13 +435,7 @@ const sql = safeSql`
WHERE id = ${analyticsLiteral(id)}
`
await executeAnalyticsSql({
projectRef,
endpoint: '/platform/projects/{ref}/analytics/endpoints/logs.all',
sql,
iso_timestamp_start,
iso_timestamp_end,
})
await executeAnalyticsSql({ projectRef, endpoint, sql, iso_timestamp_start, iso_timestamp_end })
```
```ts
@@ -450,5 +443,20 @@ await executeAnalyticsSql({
// executeAnalyticsSql boundary because the result is `string`, not
// `SafeLogSqlFragment`.
const sql = `SELECT * FROM ${table} WHERE id = '${id}'`
await executeAnalyticsSql({ projectRef, endpoint, sql, ... })
await executeAnalyticsSql({ projectRef, endpoint, sql, iso_timestamp_start, iso_timestamp_end })
```
The only path that runs SQL not built from these helpers is user-authored
editor text: `untrustedLogSql(text)` marks it `UntrustedLogSqlFragment`
(displayable and storable, never executable), and `acceptUntrustedLogsSql`
promotes it to `SafeLogSqlFragment`. That promotion is a **security boundary**
— call it only from a run gesture (Run button click, Cmd+Enter) or an
approval-gated tool call (the AI notebook tools). Never from render,
`useEffect`, or any automatic path. The notebook persist path also promotes
cells because the writable notebook type requires the safe brand; that is
storage typing, not execution approval, and is not precedent for promoting
anywhere else. The same rule as `acceptUntrustedSql` on the Postgres side.
Endpoint selection, the OTEL query builders, and the rest of the Studio
wiring live in the `clickhouse-logs-queries` skill
(`references/codebase-integration.md`).
@@ -56,12 +56,6 @@ Wait for elements with generous timeouts:
await expect(locator).toBeVisible({ timeout: 30000 })
```
Add messages to expects for debugging:
```typescript
await expect(locator).toBeVisible({ timeout: 30000 }, 'Element should be visible after page load')
```
Use serial mode for tests sharing database state:
```typescript
@@ -213,7 +207,7 @@ await page.waitForLoadState('networkidle')
await waitForApiResponse(page, 'pg-meta', ref, 'tables')
```
Timeouts are acceptable only for client-side debounces:
The only acceptable use of `waitForTimeout` is a client-side debounce:
```ts
await page.getByRole('textbox').fill('search term')
@@ -222,7 +216,7 @@ await page.waitForTimeout(300) // allow debounce
## Avoiding `waitForTimeout`
Never use `waitForTimeout` - always wait for something specific:
Never use `waitForTimeout` to wait for UI or network — always wait for something specific (the debounce case above is the sole exception):
```typescript
// BAD
@@ -30,7 +30,32 @@ handleError() → throws ConnectionTimeoutError → React Query catches → Erro
| `TroubleshootingSections.tsx` | Reusable accordion section components |
| `TroubleshootingAccordion.tsx` | Accordion wrapper with telemetry |
## Usage
## Which component
| Situation | Use |
| --------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------ |
| A query failed and the page/section can't render its data (the default case — most of Studio) | `AlertError` from `components/ui/AlertError` |
| The error may be a **classified** type with its own troubleshooting steps (e.g. connection timeout) | `ErrorMatcher` from `components/interfaces/ErrorHandling/ErrorMatcher` — pass a `fallback` for the unclassified case |
| A mutation failed | The mutation hook's default `onError` toast (`toast.error` from `sonner`) — don't render an alert (see `studio-queries`) |
### `AlertError` (default)
Renders a warning `Admonition` with the error message, generic "try refreshing / contact support" instructions, and a **Contact support** button pre-filled with `projectRef`, `subject`, and the error message.
```tsx
if (isError) return <AlertError error={error} subject="Failed to retrieve invoices" />
```
- `subject` is the human-readable title, phrased `Failed to <verb> <thing>`. Pass `projectRef` when in a project context so the support form is pre-filled.
- `error` is the React Query error object (anything with `message`); `503` responses are reworded automatically.
- Use `additionalActions` for a retry or navigate button; `hideContactSupport` only when support genuinely can't help (e.g. a user-input error).
- Prefer the early-return form for the page/section's primary data; use inline `{isError && <AlertError … />}` for secondary panels that shouldn't block the rest of the page.
### `ErrorMatcher` (classified errors)
Use when the data layer may have classified the error into a `KnownErrorType` with dedicated troubleshooting UI. It reads `errorType` from the error instance and renders the mapped `Troubleshooting` component, or `fallback` when there is no mapping. Today this is wired for the table editor sidebar; reach for it when adding troubleshooting for a new error type rather than as a general replacement for `AlertError`.
## `ErrorMatcher` usage
Pass the **full error object** from React Query — not `error.message`:
@@ -27,16 +27,18 @@ don't need MSW; render and assert directly.
## The template
```tsx
import { fireEvent, screen, waitFor } from '@testing-library/react'
import userEvent from '@testing-library/user-event'
import { screen } from '@testing-library/react'
import { platformComponents as components } from 'api-types'
import { mockAnimationsApi } from 'jsdom-testing-mocks'
import { HttpResponse } from 'msw'
import { describe, expect, test, vi } from 'vitest'
import { describe, expect, test } from 'vitest'
import { MyComponent } from './MyComponent'
import { customRender } from '@/tests/lib/custom-render'
import { addAPIMock } from '@/tests/lib/msw'
type OrganizationResponse = components['schemas']['OrganizationResponse']
// Needed if the component renders inside a Sheet, Modal, Popover, or
// anything else built on Radix that uses Web Animations.
mockAnimationsApi()
@@ -61,7 +63,8 @@ describe('MyComponent', () => {
})
```
That's the whole pattern. Server lifecycle (`listen`/`resetHandlers`/
That's the whole pattern (add `fireEvent`, `waitFor`, or `userEvent` as the
interactions need them — see the gotchas below). Server lifecycle (`listen`/`resetHandlers`/
`close`) is handled by `apps/studio/tests/vitestSetup.ts` — handlers
registered via `addAPIMock` are scoped to the current test.
@@ -111,7 +111,7 @@ const handleClick = useCallback(
```ts
import { useMutation, UseMutationOptions, useQueryClient } from '@tanstack/react-query'
import toast from 'react-hot-toast'
import { toast } from 'sonner'
import { xKeys } from './keys'
+63
View File
@@ -0,0 +1,63 @@
---
name: studio-shortcuts
description: Keyboard shortcut conventions for Supabase Studio. Use when adding a
repeated user action, toolbar action, list/table operation, or sub-page navigation
that should have shortcut coverage, when registering or changing a shortcut, or when
adding a search/filter Input (which needs the staged-Escape handler). Covers the
shortcut registry, useShortcut, ShortcutTooltip/ShortcutBadge, the reference sheet,
and collision rules.
---
# Studio Keyboard Shortcuts
When Studio UI changes introduce or materially alter repeated user actions, consider whether keyboard shortcut coverage should be added or updated. Shortcuts use the shared Studio shortcut system and must be discoverable from the visible UI.
## Rules
- Never add a one-off `keydown` listener for a normal Studio action — register it through the shortcut registry and `useShortcut`.
- Every registered shortcut is exposed where the action is visible, via `ShortcutTooltip`, `ShortcutBadge`, or a command-menu badge.
- `G then …` chords are reserved for navigation.
- Avoid broad `Mod+letter` shortcuts that overlap common browser, editor, system, copy/save/search, or devtools behavior.
- Before adding a shortcut, check the registry and any remaining non-registry listeners for collisions.
- Every search/filter `<Input>` gets `onKeyDown={onSearchInputEscape(...)}` — see [Search inputs](#search-inputs).
## Preferred pattern
- Add definitions in `apps/studio/state/shortcuts/registry.ts` or `apps/studio/state/shortcuts/registry/*`.
- Register with `useShortcut`.
- Gate availability with `enabled`.
- Surface visible actions with `ShortcutTooltip` or `ShortcutBadge`.
- Prefer scoped, mnemonic sequential chords over global modifier chords.
- Set `showInSettings: false` on contextual shortcuts (scoped to a specific page state, sheet, or panel).
- When a shortcut group should appear in the reference sheet (`Shift+?`), add the group key to `SHORTCUT_REFERENCE_GROUP_ORDER` in `apps/studio/state/shortcuts/referenceGroups.ts` and a human label to `GROUP_LABELS` in `ShortcutsReferenceSheet.tsx`.
- For sheet-scoped shortcuts (active only while a `<Sheet>` is open), mount `useShortcut` inside the sheet component gated by the `open` prop (`{ enabled: open }`) — `apps/studio/components/interfaces/Platform/Webhooks/PlatformWebhooksDeliveryDetailsSheet.tsx` is the canonical example. A shortcut that _opens_ a sheet from anywhere is global instead, gated by whatever makes the action valid (e.g. `useConnectSheetShortcut` checks project health).
## Search inputs
Every `<Input>` used as a search or filter field must include the staged-Escape handler from `apps/studio/lib/keyboard.ts`:
```tsx
import { onSearchInputEscape } from '@/lib/keyboard'
;<Input
value={query}
onChange={(e) => setQuery(e.target.value)}
onKeyDown={onSearchInputEscape(query, setQuery)}
/>
```
Behavior:
- **Escape while the input has a value** → clears the value, keeps focus (so a second Escape then blurs)
- **Escape while the input is empty** → blurs the input
- Stops propagation on Escape so the keystroke does not accidentally close a parent dialog or sheet
When pairing with `useShortcut(LIST_PAGE_FOCUS_SEARCH, ...)` to focus a search input via keyboard, always also add `onSearchInputEscape` on the same input — focus and escape-to-blur are always a pair.
## Key files
`apps/studio/state/shortcuts/registry.ts`, `apps/studio/state/shortcuts/useShortcut.tsx`, `apps/studio/components/ui/Shortcut*.tsx`, `apps/studio/lib/keyboard.ts`.
## Tests
E2E tests for a feature with shortcuts cover both click interactions and the keyboard path — see `studio-e2e-tests`.
@@ -143,14 +143,31 @@ popover open/close with keyboard/mouse, multi-step form transitions.
**Not valid:** testing a calculation or transformation that happens to live
in a component — extract to `.utils.ts` and unit test instead.
Studio component test conventions:
```tsx
// Studio component test conventions
import { fireEvent } from '@testing-library/react'
import userEvent from '@testing-library/user-event'
import { customRender } from 'tests/lib/custom-render' // always use customRender, not raw render
import { addAPIMock } from 'tests/lib/msw' // API mocking in beforeEach
import { screen } from '@testing-library/react'
import { platformComponents as components } from 'api-types'
import { HttpResponse } from 'msw'
import { customRender } from '@/tests/lib/custom-render'
import { addAPIMock } from '@/tests/lib/msw'
type OrganizationResponse = components['schemas']['OrganizationResponse']
addAPIMock({
method: 'get',
path: '/platform/organizations',
response: () => HttpResponse.json<OrganizationResponse[]>([]),
})
customRender(<MyComponent />)
expect(await screen.findByText('No organizations')).toBeInTheDocument()
```
- Mock API requests at the network layer with `addAPIMock` (MSW) — unhandled requests fail the test. Don't `vi.mock('@/data/...')`. Always pass the OpenAPI body type to `HttpResponse.json<…>`.
- `customRender` wraps the component in the providers Studio needs (React Query, router, etc.).
- The full template, path-param syntax, and the jsdom/MSW gotchas are in the `studio-mock-api-tests` skill.
## 4. E2E Tests for Shared Features (HIGH)
If a feature exists in both self-hosted and platform, create an E2E test.
@@ -46,7 +46,7 @@ Layout selection:
Dirty state / submit:
- Destructure `isDirty` from `form.formState` to show Cancel and disable Save
- Use `isDirty` to show Cancel and disable Save. Destructure it from `form.formState` only in the component that owns `useForm`; anywhere else subscribe with `useFormState({ control })` (see the `react-hook-form` skill)
- Show loading on submit button via `loading` prop
- If submit button is outside `<form>`, set a stable `formId` and use `form` prop on the button
@@ -76,7 +76,7 @@ Docs: `apps/design-system/content/docs/ui-patterns/charts.mdx`
- Use `useChart` context flags for loading/disabled states
- Keep composition straightforward — avoid over-abstraction
Demos (in `apps/design-system/__registry__/default/block/`): `chart-composed-demo.tsx`, `chart-composed-basic.tsx`, `chart-composed-states.tsx`, `chart-composed-metrics.tsx`, `chart-composed-actions.tsx`, `chart-composed-table.tsx`
Demos (in `apps/design-system/registry/default/block/`): `chart-composed-demo.tsx`, `chart-composed-basic.tsx`, `chart-composed-states.tsx`, `chart-composed-metrics.tsx`, `chart-composed-actions.tsx`, `chart-composed-table.tsx`
## Empty States
@@ -69,10 +69,10 @@ enabled, disabled, copied, exposed, failed, converted, closed, completed, applie
## Required Pattern
Import `useTrack` from `lib/telemetry/track` (within `apps/studio/`). Never use `useSendEventMutation` (deprecated).
Import `useTrack` from `@/lib/telemetry/track` (within `apps/studio/`).
```typescript
import { useTrack } from 'lib/telemetry/track'
import { useTrack } from '@/lib/telemetry/track'
const MyComponent = () => {
const track = useTrack()
@@ -89,6 +89,30 @@ const MyComponent = () => {
}
```
## Feature Flag Measurement
A feature flag that gates behavior needs telemetry on both the flag state and how users respond to the new behavior (toggle clicks, opt-in actions), so the rollout can be measured.
- **PostHog flags** (`usePHFlag`, or PostHog-backed hooks such as `useDataApiRevokeOnCreateDefaultEnabled`): capture the flag value in a relevant `track()` call.
- **ConfigCat flags** (`useFlag` from `common`) are a different system — this pattern does not apply to them.
`usePHFlag` returns `undefined` while the PostHog store is still loading. Read the raw flag via `usePHFlag('flagName')`, **not** through wrapper hooks that coerce `undefined` to `false`, and use a conditional spread so the property is omitted (not `false`) until the flag has resolved:
As always, `track()` runs inside the user-action handler — never in the component body or an effect:
```typescript
const track = useTrack()
const flagValue = usePHFlag<boolean>('myBooleanFlag') // for boolean flags
const handleSubmit = () => {
track('event_name', {
...(flagValue !== undefined && { myFlagEnabled: flagValue }),
})
}
```
For string-valued flags (e.g. experiment variants), use `usePHFlag<string>('flagName')`; a flag that may be migrated from boolean to multivariate is typed `usePHFlag<boolean | string>`. `ProjectCreationForm.tsx` (`dataApiRevokeOnCreateDefault`) is the canonical example.
## Event Definitions
All events must be defined as TypeScript interfaces in `packages/common/telemetry-constants.ts`:
@@ -111,7 +135,7 @@ export interface MyFeatureClickedEvent {
```
Add the new interface to the `TelemetryEvent` union type so `useTrack` picks it up.
`@group Events` and `@source` must be accurate.
`@group Events` and `@source` are required on every event; add `@page` when the event fires from a specific page. All three must be accurate.
## Review Rules
@@ -119,9 +143,9 @@ When reviewing a PR, flag these as **required changes:**
1. **Naming violations** — event not following `[object]_[verb]` snake_case, or using an unapproved verb
2. **Property violations** — not camelCase, generic names, or inconsistent with similar events
3. **Deprecated hook** — any usage of `useSendEventMutation` instead of `useTrack`
4. **Unnecessary view tracking** — events that fire on page load without user interaction
5. **Inaccurate docs** — `@page`/`@source` descriptions that don't match the actual implementation
3. **Unnecessary view tracking** — events that fire on page load without user interaction
4. **Inaccurate docs** — `@source`/`@page` descriptions that don't match the actual implementation
5. **Unmeasured feature flags** — a PostHog flag gates new behavior but its value is not captured in any `track()` call, or there is no outcome tracking for the gated behavior
When a PR adds user-facing interactions (buttons, forms, toggles, modals) **without** tracking, suggest:
@@ -163,16 +187,16 @@ To add tracking for a user action:
1. **Name the event** — `[object]_[verb]` using approved verbs only
2. **Choose properties** — camelCase preferred for new events; check `packages/common/telemetry-constants.ts` for similar events and match their property names and casing
3. **Add interface to telemetry-constants.ts** — with `@group Events` and `@source` JSDoc, add to the `TelemetryEvent` union type
4. **Add to component** — `import { useTrack } from 'lib/telemetry/track'`, call `track('event_name', { properties })`
3. **Add interface to telemetry-constants.ts** — with `@group Events` and `@source` JSDoc (plus `@page` when page-specific), add to the `TelemetryEvent` union type
4. **Add to component** — `import { useTrack } from '@/lib/telemetry/track'`, call `track('event_name', { properties })`
### Verification checklist
- [ ] Event name follows `[object]_[verb]` with approved verb
- [ ] Event name is snake_case
- [ ] Properties are camelCase and self-explanatory
- [ ] Event defined in telemetry-constants.ts with accurate `@page`/`@source`
- [ ] Using `useTrack` hook (not `useSendEventMutation`)
- [ ] Event defined in telemetry-constants.ts with accurate `@group Events`, `@source`, and (if page-specific) `@page`
- [ ] Using the `useTrack` hook
- [ ] Not tracking passive views/appearances
- [ ] No PII in event properties (emails, names, IPs, etc.)
- [ ] Property names consistent with similar events
@@ -85,4 +85,4 @@ Each rule file contains:
## Full Compiled Document
For the complete guide with all rules expanded: `AGENTS.md`
For the complete guide with all rules expanded, read the files under `rules/`
+1 -1
View File
@@ -50,7 +50,7 @@ When in doubt, ask `ask-the-docs` rather than guessing — this classification i
- **Wire it into navigation, not just onto disk.** Placement (which section) and nav enablement (whether it actually shows up) are separate — confirm the current nav-registration mechanism via `ask-the-docs`/`audit-docs-ia` rather than assuming a page is discoverable just because the file exists in the right folder.
- Ground every behavior claim in Phase 1's code read (the linked PR when there is one); ground every "why this matters" framing in Linear/PM context or prior Frame/Shape output; mark inferred material inline (e.g. an HTML comment or a flagged line in the handoff summary) so a reviewer can find it fast.
- **Write for timelessness.** Prefer documenting what exists now over promising future features. See [reference/common-pitfalls.md](reference/common-pitfalls.md#2-timeless-documentation).
- **Keep it concise and avoid redundancy.** See [reference/common-pitfalls.md](reference/common-pitfalls.md#4-redundancy).
- **Keep it concise and avoid redundancy.** See [reference/common-pitfalls.md](reference/common-pitfalls.md#4-redundancy-and-over-explanation).
- **Prefer paragraphs over single-item lists.** See [reference/common-pitfalls.md](reference/common-pitfalls.md#5-single-item-lists).
- **Strip internal business context before the final draft.** HTML comments flagging PRD intent, roadmap speculation, internal ticket discussions, or "gap-fill" notes must be removed from MDX before handoff. Open-source docs shouldn't expose internal planning. Flag assumptions and open questions for reviewers in the PR description instead, not in the shipped content.
- Search [`apps/docs/WORD_LIST.md`](../../../apps/docs/WORD_LIST.md) when introducing or reviewing technical terms, UI actions, abbreviations, and potentially ambiguous language during drafting. This targeted search supplements, but does not replace, the full-file compliance check in Phase 2.5.
+1
View File
@@ -0,0 +1 @@
../.agents/skills
-1
View File
@@ -1 +0,0 @@
../../.agents/skills/ask-the-docs
-1
View File
@@ -1 +0,0 @@
../../.agents/skills/edit-the-docs
-1
View File
@@ -1 +0,0 @@
../../.agents/skills/pm-the-docs
-1
View File
@@ -1 +0,0 @@
../../.agents/skills/review-the-docs
@@ -1,946 +0,0 @@
# React Composition Patterns
**Version 1.0.0**
Engineering
January 2026
> **Note:**
> This document is mainly for agents and LLMs to follow when maintaining,
> generating, or refactoring React codebases using composition. Humans
> may also find it useful, but guidance here is optimized for automation
> and consistency by AI-assisted workflows.
---
## Abstract
Composition patterns for building flexible, maintainable React components. Avoid boolean prop proliferation by using compound components, lifting state, and composing internals. These patterns make codebases easier for both humans and AI agents to work with as they scale.
---
## Table of Contents
1. [Component Architecture](#1-component-architecture) — **HIGH**
- 1.1 [Avoid Boolean Prop Proliferation](#11-avoid-boolean-prop-proliferation)
- 1.2 [Use Compound Components](#12-use-compound-components)
2. [State Management](#2-state-management) — **MEDIUM**
- 2.1 [Decouple State Management from UI](#21-decouple-state-management-from-ui)
- 2.2 [Define Generic Context Interfaces for Dependency Injection](#22-define-generic-context-interfaces-for-dependency-injection)
- 2.3 [Lift State into Provider Components](#23-lift-state-into-provider-components)
3. [Implementation Patterns](#3-implementation-patterns) — **MEDIUM**
- 3.1 [Create Explicit Component Variants](#31-create-explicit-component-variants)
- 3.2 [Prefer Composing Children Over Render Props](#32-prefer-composing-children-over-render-props)
4. [React 19 APIs](#4-react-19-apis) — **MEDIUM**
- 4.1 [React 19 API Changes](#41-react-19-api-changes)
---
## 1. Component Architecture
**Impact: HIGH**
Fundamental patterns for structuring components to avoid prop
proliferation and enable flexible composition.
### 1.1 Avoid Boolean Prop Proliferation
**Impact: CRITICAL (prevents unmaintainable component variants)**
Don't add boolean props like `isThread`, `isEditing`, `isDMThread` to customize
component behavior. Each boolean doubles possible states and creates
unmaintainable conditional logic. Use composition instead.
**Incorrect: boolean props create exponential complexity**
```tsx
function Composer({
onSubmit,
isThread,
channelId,
isDMThread,
dmId,
isEditing,
isForwarding,
}: Props) {
return (
<form>
<Header />
<Input />
{isDMThread ? (
<AlsoSendToDMField id={dmId} />
) : isThread ? (
<AlsoSendToChannelField id={channelId} />
) : null}
{isEditing ? (
<EditActions />
) : isForwarding ? (
<ForwardActions />
) : (
<DefaultActions />
)}
<Footer onSubmit={onSubmit} />
</form>
)
}
```
**Correct: composition eliminates conditionals**
```tsx
// Channel composer
function ChannelComposer() {
return (
<Composer.Frame>
<Composer.Header />
<Composer.Input />
<Composer.Footer>
<Composer.Attachments />
<Composer.Formatting />
<Composer.Emojis />
<Composer.Submit />
</Composer.Footer>
</Composer.Frame>
)
}
// Thread composer - adds "also send to channel" field
function ThreadComposer({ channelId }: { channelId: string }) {
return (
<Composer.Frame>
<Composer.Header />
<Composer.Input />
<AlsoSendToChannelField id={channelId} />
<Composer.Footer>
<Composer.Formatting />
<Composer.Emojis />
<Composer.Submit />
</Composer.Footer>
</Composer.Frame>
)
}
// Edit composer - different footer actions
function EditComposer() {
return (
<Composer.Frame>
<Composer.Input />
<Composer.Footer>
<Composer.Formatting />
<Composer.Emojis />
<Composer.CancelEdit />
<Composer.SaveEdit />
</Composer.Footer>
</Composer.Frame>
)
}
```
Each variant is explicit about what it renders. We can share internals without
sharing a single monolithic parent.
### 1.2 Use Compound Components
**Impact: HIGH (enables flexible composition without prop drilling)**
Structure complex components as compound components with a shared context. Each
subcomponent accesses shared state via context, not props. Consumers compose the
pieces they need.
**Incorrect: monolithic component with render props**
```tsx
function Composer({
renderHeader,
renderFooter,
renderActions,
showAttachments,
showFormatting,
showEmojis,
}: Props) {
return (
<form>
{renderHeader?.()}
<Input />
{showAttachments && <Attachments />}
{renderFooter ? (
renderFooter()
) : (
<Footer>
{showFormatting && <Formatting />}
{showEmojis && <Emojis />}
{renderActions?.()}
</Footer>
)}
</form>
)
}
```
**Correct: compound components with shared context**
```tsx
const ComposerContext = createContext<ComposerContextValue | null>(null)
function ComposerProvider({ children, state, actions, meta }: ProviderProps) {
return (
<ComposerContext value={{ state, actions, meta }}>
{children}
</ComposerContext>
)
}
function ComposerFrame({ children }: { children: React.ReactNode }) {
return <form>{children}</form>
}
function ComposerInput() {
const {
state,
actions: { update },
meta: { inputRef },
} = use(ComposerContext)
return (
<TextInput
ref={inputRef}
value={state.input}
onChangeText={(text) => update((s) => ({ ...s, input: text }))}
/>
)
}
function ComposerSubmit() {
const {
actions: { submit },
} = use(ComposerContext)
return <Button onPress={submit}>Send</Button>
}
// Export as compound component
const Composer = {
Provider: ComposerProvider,
Frame: ComposerFrame,
Input: ComposerInput,
Submit: ComposerSubmit,
Header: ComposerHeader,
Footer: ComposerFooter,
Attachments: ComposerAttachments,
Formatting: ComposerFormatting,
Emojis: ComposerEmojis,
}
```
**Usage:**
```tsx
<Composer.Provider state={state} actions={actions} meta={meta}>
<Composer.Frame>
<Composer.Header />
<Composer.Input />
<Composer.Footer>
<Composer.Formatting />
<Composer.Submit />
</Composer.Footer>
</Composer.Frame>
</Composer.Provider>
```
Consumers explicitly compose exactly what they need. No hidden conditionals. And the state, actions and meta are dependency-injected by a parent provider, allowing multiple usages of the same component structure.
---
## 2. State Management
**Impact: MEDIUM**
Patterns for lifting state and managing shared context across
composed components.
### 2.1 Decouple State Management from UI
**Impact: MEDIUM (enables swapping state implementations without changing UI)**
The provider component should be the only place that knows how state is managed.
UI components consume the context interface—they don't know if state comes from
useState, Zustand, or a server sync.
**Incorrect: UI coupled to state implementation**
```tsx
function ChannelComposer({ channelId }: { channelId: string }) {
// UI component knows about global state implementation
const state = useGlobalChannelState(channelId)
const { submit, updateInput } = useChannelSync(channelId)
return (
<Composer.Frame>
<Composer.Input
value={state.input}
onChange={(text) => sync.updateInput(text)}
/>
<Composer.Submit onPress={() => sync.submit()} />
</Composer.Frame>
)
}
```
**Correct: state management isolated in provider**
```tsx
// Provider handles all state management details
function ChannelProvider({
channelId,
children,
}: {
channelId: string
children: React.ReactNode
}) {
const { state, update, submit } = useGlobalChannel(channelId)
const inputRef = useRef(null)
return (
<Composer.Provider
state={state}
actions={{ update, submit }}
meta={{ inputRef }}
>
{children}
</Composer.Provider>
)
}
// UI component only knows about the context interface
function ChannelComposer() {
return (
<Composer.Frame>
<Composer.Header />
<Composer.Input />
<Composer.Footer>
<Composer.Submit />
</Composer.Footer>
</Composer.Frame>
)
}
// Usage
function Channel({ channelId }: { channelId: string }) {
return (
<ChannelProvider channelId={channelId}>
<ChannelComposer />
</ChannelProvider>
)
}
```
**Different providers, same UI:**
```tsx
// Local state for ephemeral forms
function ForwardMessageProvider({ children }) {
const [state, setState] = useState(initialState)
const forwardMessage = useForwardMessage()
return (
<Composer.Provider
state={state}
actions={{ update: setState, submit: forwardMessage }}
>
{children}
</Composer.Provider>
)
}
// Global synced state for channels
function ChannelProvider({ channelId, children }) {
const { state, update, submit } = useGlobalChannel(channelId)
return (
<Composer.Provider state={state} actions={{ update, submit }}>
{children}
</Composer.Provider>
)
}
```
The same `Composer.Input` component works with both providers because it only
depends on the context interface, not the implementation.
### 2.2 Define Generic Context Interfaces for Dependency Injection
**Impact: HIGH (enables dependency-injectable state across use-cases)**
Define a **generic interface** for your component context with three parts:
`state`, `actions`, and `meta`. This interface is a contract that any provider
can implement—enabling the same UI components to work with completely different
state implementations.
**Core principle:** Lift state, compose internals, make state
dependency-injectable.
**Incorrect: UI coupled to specific state implementation**
```tsx
function ComposerInput() {
// Tightly coupled to a specific hook
const { input, setInput } = useChannelComposerState()
return <TextInput value={input} onChangeText={setInput} />
}
```
**Correct: generic interface enables dependency injection**
```tsx
// Define a GENERIC interface that any provider can implement
interface ComposerState {
input: string
attachments: Attachment[]
isSubmitting: boolean
}
interface ComposerActions {
update: (updater: (state: ComposerState) => ComposerState) => void
submit: () => void
}
interface ComposerMeta {
inputRef: React.RefObject<TextInput>
}
interface ComposerContextValue {
state: ComposerState
actions: ComposerActions
meta: ComposerMeta
}
const ComposerContext = createContext<ComposerContextValue | null>(null)
```
**UI components consume the interface, not the implementation:**
```tsx
function ComposerInput() {
const {
state,
actions: { update },
meta,
} = use(ComposerContext)
// This component works with ANY provider that implements the interface
return (
<TextInput
ref={meta.inputRef}
value={state.input}
onChangeText={(text) => update((s) => ({ ...s, input: text }))}
/>
)
}
```
**Different providers implement the same interface:**
```tsx
// Provider A: Local state for ephemeral forms
function ForwardMessageProvider({ children }: { children: React.ReactNode }) {
const [state, setState] = useState(initialState)
const inputRef = useRef(null)
const submit = useForwardMessage()
return (
<ComposerContext
value={{
state,
actions: { update: setState, submit },
meta: { inputRef },
}}
>
{children}
</ComposerContext>
)
}
// Provider B: Global synced state for channels
function ChannelProvider({ channelId, children }: Props) {
const { state, update, submit } = useGlobalChannel(channelId)
const inputRef = useRef(null)
return (
<ComposerContext
value={{
state,
actions: { update, submit },
meta: { inputRef },
}}
>
{children}
</ComposerContext>
)
}
```
**The same composed UI works with both:**
```tsx
// Works with ForwardMessageProvider (local state)
<ForwardMessageProvider>
<Composer.Frame>
<Composer.Input />
<Composer.Submit />
</Composer.Frame>
</ForwardMessageProvider>
// Works with ChannelProvider (global synced state)
<ChannelProvider channelId="abc">
<Composer.Frame>
<Composer.Input />
<Composer.Submit />
</Composer.Frame>
</ChannelProvider>
```
**Custom UI outside the component can access state and actions:**
```tsx
function ForwardMessageDialog() {
return (
<ForwardMessageProvider>
<Dialog>
{/* The composer UI */}
<Composer.Frame>
<Composer.Input placeholder="Add a message, if you'd like." />
<Composer.Footer>
<Composer.Formatting />
<Composer.Emojis />
</Composer.Footer>
</Composer.Frame>
{/* Custom UI OUTSIDE the composer, but INSIDE the provider */}
<MessagePreview />
{/* Actions at the bottom of the dialog */}
<DialogActions>
<CancelButton />
<ForwardButton />
</DialogActions>
</Dialog>
</ForwardMessageProvider>
)
}
// This button lives OUTSIDE Composer.Frame but can still submit based on its context!
function ForwardButton() {
const {
actions: { submit },
} = use(ComposerContext)
return <Button onPress={submit}>Forward</Button>
}
// This preview lives OUTSIDE Composer.Frame but can read composer's state!
function MessagePreview() {
const { state } = use(ComposerContext)
return <Preview message={state.input} attachments={state.attachments} />
}
```
The provider boundary is what matters—not the visual nesting. Components that
need shared state don't have to be inside the `Composer.Frame`. They just need
to be within the provider.
The `ForwardButton` and `MessagePreview` are not visually inside the composer
box, but they can still access its state and actions. This is the power of
lifting state into providers.
The UI is reusable bits you compose together. The state is dependency-injected
by the provider. Swap the provider, keep the UI.
### 2.3 Lift State into Provider Components
**Impact: HIGH (enables state sharing outside component boundaries)**
Move state management into dedicated provider components. This allows sibling
components outside the main UI to access and modify state without prop drilling
or awkward refs.
**Incorrect: state trapped inside component**
```tsx
function ForwardMessageComposer() {
const [state, setState] = useState(initialState)
const forwardMessage = useForwardMessage()
return (
<Composer.Frame>
<Composer.Input />
<Composer.Footer />
</Composer.Frame>
)
}
// Problem: How does this button access composer state?
function ForwardMessageDialog() {
return (
<Dialog>
<ForwardMessageComposer />
<MessagePreview /> {/* Needs composer state */}
<DialogActions>
<CancelButton />
<ForwardButton /> {/* Needs to call submit */}
</DialogActions>
</Dialog>
)
}
```
**Incorrect: useEffect to sync state up**
```tsx
function ForwardMessageDialog() {
const [input, setInput] = useState('')
return (
<Dialog>
<ForwardMessageComposer onInputChange={setInput} />
<MessagePreview input={input} />
</Dialog>
)
}
function ForwardMessageComposer({ onInputChange }) {
const [state, setState] = useState(initialState)
useEffect(() => {
onInputChange(state.input) // Sync on every change 😬
}, [state.input])
}
```
**Incorrect: reading state from ref on submit**
```tsx
function ForwardMessageDialog() {
const stateRef = useRef(null)
return (
<Dialog>
<ForwardMessageComposer stateRef={stateRef} />
<ForwardButton onPress={() => submit(stateRef.current)} />
</Dialog>
)
}
```
**Correct: state lifted to provider**
```tsx
function ForwardMessageProvider({ children }: { children: React.ReactNode }) {
const [state, setState] = useState(initialState)
const forwardMessage = useForwardMessage()
const inputRef = useRef(null)
return (
<Composer.Provider
state={state}
actions={{ update: setState, submit: forwardMessage }}
meta={{ inputRef }}
>
{children}
</Composer.Provider>
)
}
function ForwardMessageDialog() {
return (
<ForwardMessageProvider>
<Dialog>
<ForwardMessageComposer />
<MessagePreview /> {/* Custom components can access state and actions */}
<DialogActions>
<CancelButton />
<ForwardButton /> {/* Custom components can access state and actions */}
</DialogActions>
</Dialog>
</ForwardMessageProvider>
)
}
function ForwardButton() {
const { actions } = use(Composer.Context)
return <Button onPress={actions.submit}>Forward</Button>
}
```
The ForwardButton lives outside the Composer.Frame but still has access to the
submit action because it's within the provider. Even though it's a one-off
component, it can still access the composer's state and actions from outside the
UI itself.
**Key insight:** Components that need shared state don't have to be visually
nested inside each other—they just need to be within the same provider.
---
## 3. Implementation Patterns
**Impact: MEDIUM**
Specific techniques for implementing compound components and
context providers.
### 3.1 Create Explicit Component Variants
**Impact: MEDIUM (self-documenting code, no hidden conditionals)**
Instead of one component with many boolean props, create explicit variant
components. Each variant composes the pieces it needs. The code documents
itself.
**Incorrect: one component, many modes**
```tsx
// What does this component actually render?
<Composer
isThread
isEditing={false}
channelId='abc'
showAttachments
showFormatting={false}
/>
```
**Correct: explicit variants**
```tsx
// Immediately clear what this renders
<ThreadComposer channelId="abc" />
// Or
<EditMessageComposer messageId="xyz" />
// Or
<ForwardMessageComposer messageId="123" />
```
Each implementation is unique, explicit and self-contained. Yet they can each
use shared parts.
**Implementation:**
```tsx
function ThreadComposer({ channelId }: { channelId: string }) {
return (
<ThreadProvider channelId={channelId}>
<Composer.Frame>
<Composer.Input />
<AlsoSendToChannelField channelId={channelId} />
<Composer.Footer>
<Composer.Formatting />
<Composer.Emojis />
<Composer.Submit />
</Composer.Footer>
</Composer.Frame>
</ThreadProvider>
)
}
function EditMessageComposer({ messageId }: { messageId: string }) {
return (
<EditMessageProvider messageId={messageId}>
<Composer.Frame>
<Composer.Input />
<Composer.Footer>
<Composer.Formatting />
<Composer.Emojis />
<Composer.CancelEdit />
<Composer.SaveEdit />
</Composer.Footer>
</Composer.Frame>
</EditMessageProvider>
)
}
function ForwardMessageComposer({ messageId }: { messageId: string }) {
return (
<ForwardMessageProvider messageId={messageId}>
<Composer.Frame>
<Composer.Input placeholder="Add a message, if you'd like." />
<Composer.Footer>
<Composer.Formatting />
<Composer.Emojis />
<Composer.Mentions />
</Composer.Footer>
</Composer.Frame>
</ForwardMessageProvider>
)
}
```
Each variant is explicit about:
- What provider/state it uses
- What UI elements it includes
- What actions are available
No boolean prop combinations to reason about. No impossible states.
### 3.2 Prefer Composing Children Over Render Props
**Impact: MEDIUM (cleaner composition, better readability)**
Use `children` for composition instead of `renderX` props. Children are more
readable, compose naturally, and don't require understanding callback
signatures.
**Incorrect: render props**
```tsx
function Composer({
renderHeader,
renderFooter,
renderActions,
}: {
renderHeader?: () => React.ReactNode
renderFooter?: () => React.ReactNode
renderActions?: () => React.ReactNode
}) {
return (
<form>
{renderHeader?.()}
<Input />
{renderFooter ? renderFooter() : <DefaultFooter />}
{renderActions?.()}
</form>
)
}
// Usage is awkward and inflexible
return (
<Composer
renderHeader={() => <CustomHeader />}
renderFooter={() => (
<>
<Formatting />
<Emojis />
</>
)}
renderActions={() => <SubmitButton />}
/>
)
```
**Correct: compound components with children**
```tsx
function ComposerFrame({ children }: { children: React.ReactNode }) {
return <form>{children}</form>
}
function ComposerFooter({ children }: { children: React.ReactNode }) {
return <footer className='flex'>{children}</footer>
}
// Usage is flexible
return (
<Composer.Frame>
<CustomHeader />
<Composer.Input />
<Composer.Footer>
<Composer.Formatting />
<Composer.Emojis />
<SubmitButton />
</Composer.Footer>
</Composer.Frame>
)
```
**When render props are appropriate:**
```tsx
// Render props work well when you need to pass data back
<List
data={items}
renderItem={({ item, index }) => <Item item={item} index={index} />}
/>
```
Use render props when the parent needs to provide data or state to the child.
Use children when composing static structure.
---
## 4. React 19 APIs
**Impact: MEDIUM**
React 19+ only. Don't use `forwardRef`; use `use()` instead of `useContext()`.
### 4.1 React 19 API Changes
**Impact: MEDIUM (cleaner component definitions and context usage)**
> **⚠️ React 19+ only.** Skip this if you're on React 18 or earlier.
In React 19, `ref` is now a regular prop (no `forwardRef` wrapper needed), and `use()` replaces `useContext()`.
**Incorrect: forwardRef in React 19**
```tsx
const ComposerInput = forwardRef<TextInput, Props>((props, ref) => {
return <TextInput ref={ref} {...props} />
})
```
**Correct: ref as a regular prop**
```tsx
function ComposerInput({ ref, ...props }: Props & { ref?: React.Ref<TextInput> }) {
return <TextInput ref={ref} {...props} />
}
```
**Incorrect: useContext in React 19**
```tsx
const value = useContext(MyContext)
```
**Correct: use instead of useContext**
```tsx
const value = use(MyContext)
```
`use()` can also be called conditionally, unlike `useContext()`.
---
## References
1. [https://react.dev](https://react.dev)
2. [https://react.dev/learn/passing-data-deeply-with-context](https://react.dev/learn/passing-data-deeply-with-context)
3. [https://react.dev/reference/react/use](https://react.dev/reference/react/use)
-1
View File
@@ -1 +0,0 @@
../../.agents/skills/vitest
-1
View File
@@ -1 +0,0 @@
../../.agents/skills/write-the-docs
+5 -5
View File
@@ -118,23 +118,23 @@ reviews:
destination. Skip if aria-label or wrapping context already names
where the link goes.
# Applies our internal engineering skills (.claude/skills/) as CodeRabbit review
# Applies our internal engineering skills (.agents/skills/) as CodeRabbit review
# guidelines. The skills are the single source of truth — they are consumed
# directly, with no copy of their content elsewhere.
#
# `applyTo` decouples where a guideline file lives from the code it governs.
# Without it, CodeRabbit scopes a guideline file to its own directory and below;
# our skills live in .claude/skills/, which contains no code, so they would never
# our skills live in .agents/skills/, which contains no code, so they would never
# reach apps/studio. `applyTo` points them at the right paths instead.
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,react-hook-form}/SKILL.md'
- files: '.agents/skills/{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'
- files: '.agents/skills/{studio-testing,studio-mock-api-tests}/SKILL.md'
applyTo: 'apps/studio/**/*.test.{ts,tsx}'
# Studio end-to-end (Playwright) test conventions
- files: '.claude/skills/studio-e2e-tests/SKILL.md'
- files: '.agents/skills/studio-e2e-tests/SKILL.md'
applyTo: 'e2e/studio/**/*.spec.ts'
@@ -1,25 +0,0 @@
---
description: "Docs: how to run tests locally (Supabase setup + correct commands)"
globs:
- apps/docs/**/*.{test,spec}.{ts,tsx}
alwaysApply: false
---
# Docs test requirements
Before running tests for `apps/docs`, ensure local Supabase is available and the DB is in a known state.
## Recommended sequence
```bash
pnpm supabase status
pnpm supabase start # if not running
pnpm supabase db reset --local
pnpm run -F docs test:local:unwatch
```
## Notes
- Always reset the local DB before running docs tests to avoid state leakage.
- Prefer `test:local:unwatch` for non-watch CI-like runs.
-1
View File
@@ -1 +0,0 @@
../../.agents/skills/ask-the-docs
-1
View File
@@ -1 +0,0 @@
../../.agents/skills/edit-the-docs
-1
View File
@@ -1 +0,0 @@
../../.agents/skills/pm-the-docs
-1
View File
@@ -1 +0,0 @@
../../.agents/skills/review-the-docs
-1
View File
@@ -1 +0,0 @@
../../.agents/skills/vitest
-1
View File
@@ -1 +0,0 @@
../../.agents/skills/write-the-docs
-1
View File
@@ -1 +0,0 @@
!.env.example
+37 -11
View File
@@ -19,7 +19,7 @@ Our CI pipeline already validates the following. **Never comment on these topics
- **Missing tests for trivial changes** — Handled by topic-specific test instructions
- **Import ordering or grouping** — Handled by linter
- **Naming style preferences** (camelCase vs snake_case debates) — Follow existing file conventions
- **Accessibility attributes on shadcn/Radix UI components** — See `studio-shadcn-components.instructions.md` for details
- **Accessibility attributes on shadcn/Radix UI components** — handled by the primitives; see [shadcn/Radix accessibility](#shadcnradix-accessibility) below
### What TO Comment On (Priority Order)
@@ -47,15 +47,41 @@ This is a TypeScript/Next.js/React monorepo:
## Topic-Specific Guidelines
Path-specific rules in `.github/instructions/`:
Coding conventions are not duplicated here. Read them from the shared agent instruction files, which apply to every AI tool working in this repo:
- **Telemetry**: `studio-telemetry.instructions.md` — event naming, property conventions, feature flag measurement
- **Testing**: `studio-testing.instructions.md` — test strategy, extraction patterns, coverage expectations
- **Error Handling**: `studio-error-handling.instructions.md` — error classification, `ErrorMatcher` usage
- **E2E Tests**: `studio-e2e-tests.instructions.md` — selector priority, anti-patterns (`waitForTimeout`, `force: true`)
- **Composition Patterns**: `studio-composition-patterns.instructions.md` — avoid boolean props, use compound components
- **UI Copy**: `studio-copy.instructions.md` → `apps/design-system/content/docs/copywriting.mdx`
- **shadcn/Radix Components**: `studio-shadcn-components.instructions.md` — accessibility handled by primitives, do not flag
- **Keyboard Shortcuts**: `studio-shortcuts.instructions.md` — shortcut registry pattern, search-input escape handler, when to flag missing coverage
- `AGENTS.md` (repo root) — monorepo structure, commands, CI, conventions
- `apps/studio/AGENTS.md` — Studio-specific rules and the task → skill map
- `.agents/skills/*/SKILL.md` — the source of truth per topic. Most relevant to review: `studio-testing`, `studio-mock-api-tests`, `studio-e2e-tests`, `studio-error-handling`, `studio-queries`, `studio-ui-patterns`, `react-hook-form`, `telemetry-standards`, `safe-sql-execution`, `vercel-composition-patterns`, `studio-shortcuts`, `copywriting`
These files are scoped to `apps/studio/` and applied automatically during reviews.
When a skill says to flag something, treat it as **advisory** here — the confidence threshold and comment style above still apply.
## shadcn/Radix accessibility
Studio uses **shadcn/ui** components built on **Radix UI** primitives (from `packages/ui/`), which provide ARIA roles, keyboard navigation, focus management, and screen-reader support automatically. **Do not flag missing accessibility attributes that the underlying primitive already handles.**
| Component | What Radix handles |
| ----------------------------- | -------------------------------------------------------------------------------- |
| `Dialog`, `AlertDialog` | `role="dialog"`, `aria-modal`, focus trapping, ESC to close |
| `DropdownMenu`, `ContextMenu` | `role="menu"` / `role="menuitem"`, arrow key navigation |
| `Select` | `role="combobox"`, `aria-expanded`, keyboard selection |
| `Tabs` | `role="tablist"` / `role="tab"` / `role="tabpanel"`, `aria-selected`, arrow keys |
| `Checkbox` | `role="checkbox"`, `aria-checked`, Space to toggle |
| `RadioGroup` | `role="radio"`, `aria-checked`, arrow key navigation |
| `Switch` | `role="switch"`, `aria-checked`, keyboard toggle |
| `Tooltip` | Trigger/content association, show/hide timing |
| `Accordion`, `Collapsible` | `aria-expanded`, Enter/Space to toggle |
| `Popover`, `HoverCard` | Focus management, dismiss on ESC |
| `Slider` | `role="slider"`, `aria-valuemin/max/now`, arrow keys |
| `Toggle`, `ToggleGroup` | `aria-pressed`, keyboard support |
| `ScrollArea` | Accessible scrollbar replacement |
| `NavigationMenu` | `role="navigation"`, keyboard navigation |
Specifically, never flag: missing `role` on Radix-based components; missing `aria-modal` on `Dialog`/`AlertDialog`; missing `aria-expanded` on `Accordion`, `Collapsible`, `Select`, or `DropdownMenu` triggers; missing `aria-selected` on `Tabs`; missing `aria-checked` on `Checkbox`, `RadioGroup`, or `Switch`; missing keyboard handlers on interactive Radix components; missing focus management in dialogs; missing `aria-label` on `DialogClose` (it renders `<span className="sr-only">Close</span>`).
**Do** flag accessibility issues on:
1. **Custom interactive elements** not using Radix primitives (e.g. a `<div onClick>` that should be a `<button>`)
2. **Icon-only buttons** missing an accessible label — `<Button>` alone does not add one; use `aria-label` or `<span className="sr-only">`
3. **Missing `Label` association** — form inputs should be paired with `<Label htmlFor="...">` or wrapped in a `<Field>` component
4. **Images missing `alt` text**
5. **Color-only state indicators**
@@ -1,93 +0,0 @@
---
applyTo: "apps/studio/**"
---
# React Composition Patterns Review Rules
All comments are **advisory**.
## Core Principle
Avoid boolean prop proliferation. Use composition (compound components, explicit variants, children) instead of boolean flags to customize behavior.
## When to Flag
### 1. Boolean Prop Proliferation (HIGH)
Flag components accumulating boolean props like `isThread`, `isEditing`, `showAttachments`. Each boolean doubles the state space.
```tsx
// BAD — unclear intent, combinatorial explosion
<Composer isThread isDMThread isEditing isForwarding={false} />
// GOOD — self-documenting variants
<ThreadComposer channelId="abc" />
<EditMessageComposer messageId="xyz" />
```
### 2. Render Props Instead of Children (MEDIUM)
Flag `renderX` callback props when `children` composition would work.
```tsx
// BAD — render prop for structure
<Composer renderFooter={() => <F />} />
// GOOD — compound component
<Composer.Footer>
<Composer.Formatting />
<Composer.Emojis />
</Composer.Footer>
```
### 3. UI Coupled to State Implementation (MEDIUM)
Flag UI components calling specific state hooks like `useGlobalChannelState()` directly. The provider should own the state implementation; UI should only use a generic context interface.
```tsx
// BAD — UI knows HOW state is managed
const state = useGlobalChannelState(channelId)
// GOOD — provider owns implementation, UI uses context
<ChannelProvider channelId={channelId}>
<Composer /> {/* reads from context */}
</ChannelProvider>
```
### 4. State Trapped in Child Components (MEDIUM)
Flag state that siblings or dialogs need but can't access without prop drilling or refs. Lift it into a provider.
```tsx
// BAD — sibling can't access state
function ForwardComposer() {
const [state, setState] = useState(init)
}
// ForwardButton is a sibling and can't reach state
// GOOD — provider at shared ancestor
<ForwardMessageProvider>
<Composer /> {/* can access state */}
<ForwardButton /> {/* can also access state */}
</ForwardMessageProvider>
```
### 5. React 19 API Updates
Flag `forwardRef` and `useContext` in new code — use `ref` as a regular prop and `use()` instead.
```tsx
// BAD
const Input = forwardRef((props, ref) => <input ref={ref} />)
const value = useContext(MyContext)
// GOOD
function Input({ ref, ...props }) { return <input ref={ref} /> }
const value = use(MyContext)
```
## Key Principle
Lift state → Compose UI → Inject via generic context → No boolean prop proliferation.
Canonical standard: `.claude/skills/vercel-composition-patterns/SKILL.md`
@@ -1,13 +0,0 @@
---
applyTo: 'apps/studio/**'
---
# Studio UI Copy
All comments are **advisory**.
**Source of truth:** `apps/design-system/content/docs/copywriting.mdx` — read it before writing or reviewing user-facing Studio strings.
## Agent checklist (not in the design doc)
- When changing visible copy, grep `e2e/studio/` and `.github/instructions/` for the old string.
@@ -1,86 +0,0 @@
---
applyTo: 'e2e/studio/**,apps/studio/**'
---
# Studio E2E Test Review Rules
All comments are **advisory**.
## Selector Priority (best to worst)
1. **`getByRole` with accessible name** — most robust, tests accessibility
```typescript
page.getByRole('button', { name: 'Save' })
```
2. **`getByTestId`** — stable, explicit test hooks
```typescript
page.getByTestId('table-editor-side-panel')
```
3. **`getByText` with exact match** — good for unique text
```typescript
page.getByText('Data API access', { exact: true })
```
4. **`locator` with CSS** — use sparingly, more fragile
```typescript
page.locator('[data-state="open"]')
```
## Patterns to Flag
- **XPath selectors** — fragile to DOM changes
```typescript
// BAD
locator('xpath=ancestor::div[contains(@class, "space-y")]')
```
- **Parent traversal with `locator('..')`** — breaks when structure changes
```typescript
// BAD
element.locator('..').getByRole('button')
```
- **`waitForTimeout`** — never use; wait for something specific instead
```typescript
// BAD
await page.waitForTimeout(1000)
// GOOD — wait for UI element
await expect(page.getByText('Success')).toBeVisible()
// GOOD — wait for API response
const apiPromise = waitForApiResponse(page, 'pg-meta', ref, 'query?key=table-create')
await saveButton.click()
await apiPromise
```
- **`force: true` on clicks** — make elements visible first instead
```typescript
// BAD
await menuButton.click({ force: true })
// GOOD — hover to reveal, then click
await tableRow.hover()
await expect(menuButton).toBeVisible()
await menuButton.click()
```
- **Broad `filter({ hasText })` on generic elements** — may match multiple elements; scope to specific containers instead
## Good Practices to Encourage
- Scope selectors to containers: `page.getByTestId('side-panel').getByRole('switch')`
- Add `aria-label` to icon-only buttons in source code for better test selectors
- Use `test.describe.configure({ mode: 'serial' })` for tests sharing database state
- Add messages to expects: `await expect(locator, 'why').toBeVisible({ timeout: 30000 })`
Canonical standard: `.claude/skills/studio-e2e-tests/SKILL.md`
@@ -1,43 +0,0 @@
---
applyTo: "apps/studio/**"
---
# Studio Error Handling Review Rules
All comments are **advisory**.
## Architecture
Errors flow: `handleError()` → throws typed subclass → React Query catches → `ErrorMatcher` reads `errorType` → renders troubleshooting. The component does an O(1) lookup — it never does regex matching.
## When to Flag
- PR passes `error.message` instead of the full `error` object to `ErrorMatcher` — the class type is lost
- PR puts regex patterns in `error-mappings.tsx` — they belong in `data/error-patterns.ts`
- PR uses `Object.assign` to stamp `errorType` on an error — should throw a proper subclass instead
- PR passes a raw URL string for support links — should use `supportFormParams={{ projectRef }}`
- PR puts the page title inside the error mapping — it belongs on the `<ErrorMatcher>` caller
- PR adds callback props (`onDebugWithAI`, `onRestartProject`) to troubleshooting components — use hooks inside them instead
## Correct Usage
```tsx
{isError && (
<ErrorMatcher
title="Failed to load tables"
error={error}
supportFormParams={{ projectRef }}
/>
)}
```
## Key Files
| File | Purpose |
|------|---------|
| `data/error-patterns.ts` | `{ pattern, ErrorClass }` array — regex lives here |
| `types/api-errors.ts` | Error classes, `KnownErrorType` union |
| `ErrorMatcher.tsx` | Reads `errorType`, looks up mapping, renders |
| `error-mappings.tsx` | `Record<KnownErrorType, { id, Troubleshooting }>` |
Canonical standard: `.claude/skills/studio-error-handling/SKILL.md`
@@ -1,53 +0,0 @@
---
applyTo: "apps/studio/**"
---
# shadcn/Radix UI Component Review Rules
All comments are **advisory**.
## Core Principle
This project uses **shadcn/ui** components built on **Radix UI** primitives (from `packages/ui/`). These components provide comprehensive accessibility out-of-the-box. **Do not flag missing accessibility attributes that are already handled by the underlying Radix primitives.**
## Components with Built-In Accessibility — Do NOT Flag
The following components (imported from `ui`) already handle ARIA roles, keyboard navigation, focus management, and screen reader support automatically via Radix UI primitives:
| Component | What Radix Handles |
|-----------|-------------------|
| `Dialog`, `AlertDialog` | `role="dialog"`, `aria-modal`, focus trapping, ESC to close |
| `DropdownMenu`, `ContextMenu` | `role="menu"` / `role="menuitem"`, arrow key navigation |
| `Select` | `role="combobox"`, `aria-expanded`, keyboard selection |
| `Tabs` | `role="tablist"` / `role="tab"` / `role="tabpanel"`, `aria-selected`, arrow keys |
| `Checkbox` | `role="checkbox"`, `aria-checked`, Space to toggle |
| `RadioGroup` | `role="radio"`, `aria-checked`, arrow key navigation |
| `Switch` | `role="switch"`, `aria-checked`, keyboard toggle |
| `Tooltip` | Trigger/content association, show/hide timing |
| `Accordion`, `Collapsible` | `aria-expanded`, Enter/Space to toggle |
| `Popover`, `HoverCard` | Focus management, dismiss on ESC |
| `Slider` | `role="slider"`, `aria-valuemin/max/now`, arrow keys |
| `Toggle`, `ToggleGroup` | `aria-pressed`, keyboard support |
| `ScrollArea` | Accessible scrollbar replacement |
| `NavigationMenu` | `role="navigation"`, keyboard navigation |
### Specifically, Never Flag These
- Missing `role` on `Dialog`, `AlertDialog`, `DropdownMenu`, `Select`, `Tabs`, `RadioGroup`, or other Radix-based components — roles are set by the primitive
- Missing `aria-modal` on `Dialog` or `AlertDialog` — set automatically
- Missing `aria-expanded` on `Accordion`, `Collapsible`, `Select`, or `DropdownMenu` triggers — managed by Radix state
- Missing `aria-selected` on `Tabs` — managed by `TabsPrimitive`
- Missing `aria-checked` on `Checkbox`, `RadioGroup`, or `Switch` — managed by Radix state
- Missing keyboard event handlers (`onKeyDown`, `onKeyUp`) on interactive Radix components — keyboard support is built-in
- Missing focus management in `Dialog` or `AlertDialog` — focus trapping is automatic
- Missing `aria-label` on `DialogClose` or `AlertDialogCancel` — these render a visible `<span className="sr-only">Close</span>`
## What TO Flag
Only flag accessibility issues for:
1. **Custom interactive elements** not using Radix primitives (e.g., a `<div onClick>` that should be a `<button>`)
2. **Icon-only buttons** missing an accessible label — `<Button>` alone does not add one; use `aria-label` or `<span className="sr-only">`
3. **Missing `Label` association** — form inputs should be paired with `<Label htmlFor="...">` or wrapped in a `<Field>` component
4. **Images missing `alt` text** — not handled by any component library
5. **Color-only state indicators** — state changes should not rely solely on color
@@ -1,56 +0,0 @@
---
applyTo: 'apps/studio/**'
---
# Studio Shortcut Review Rules
All comments are **advisory**.
## Core Principle
When Studio UI changes introduce or materially alter repeated user actions, consider whether keyboard shortcut coverage should be added or updated. Shortcuts should use the shared Studio shortcut system and be discoverable from the visible UI.
## When to Flag
- PR adds a primary repeated action, toolbar action, list/table operation, or sub-page navigation without considering shortcut coverage.
- PR adds a one-off `keydown` listener for a normal Studio action instead of using the shortcut registry and `useShortcut`.
- PR registers a shortcut but does not expose it via `ShortcutTooltip`, `ShortcutBadge`, or command-menu badge where the action is visible.
- PR uses `G then ...` for a non-navigation action.
- PR adds a broad `Mod+letter` shortcut that overlaps common browser, editor, system, copy/save/search, or devtools behaviour.
- PR adds a shortcut without checking existing registry and non-registry listeners for collisions.
- PR adds a search/filter `<Input>` without `onKeyDown={onSearchInputEscape(...)}` — see **Search Inputs** below.
## Preferred Pattern
- Add definitions in `apps/studio/state/shortcuts/registry.ts` or `apps/studio/state/shortcuts/registry/*`.
- Register with `useShortcut`.
- Gate availability with `enabled`.
- Surface visible actions with `ShortcutTooltip` or `ShortcutBadge`.
- Prefer scoped, mnemonic sequential chords over global modifier chords.
- Set `showInSettings: false` on contextual shortcuts (scoped to a specific page state, sheet, or panel).
- When a shortcut group should appear in the reference sheet (`Mod+/`), add the group key to `SHORTCUT_REFERENCE_GROUP_ORDER` in `apps/studio/state/shortcuts/referenceGroups.ts` and a human label to `GROUP_LABELS` in `ShortcutsReferenceSheet.tsx`.
- For sheet-scoped shortcuts (active only while a `<Sheet>` is open), mount `useShortcut` inside the sheet component gated by the `open` prop — see `apps/studio/components/interfaces/ConnectSheet/useConnectSheetShortcut.ts` as the canonical example.
## Search Inputs
Every `<Input>` used as a search or filter field must include the staged-Escape handler from `apps/studio/lib/keyboard.ts`:
```tsx
import { onSearchInputEscape } from '@/lib/keyboard'
;<Input
value={query}
onChange={(e) => setQuery(e.target.value)}
onKeyDown={onSearchInputEscape(query, setQuery)}
/>
```
Behaviour:
- **Escape while the input has a value** → clears the value, keeps focus (so a second Escape then blurs)
- **Escape while the input is empty** → blurs the input
- Stops propagation on Escape so the keystroke does not accidentally close a parent dialog or sheet
When pairing with `useShortcut(LIST_PAGE_FOCUS_SEARCH, ...)` to focus a search input via keyboard, always also add `onSearchInputEscape` on the same input — focus and escape-to-blur are always a pair.
Canonical implementation context: `apps/studio/state/shortcuts/registry.ts`, `apps/studio/state/shortcuts/useShortcut.tsx`, and `apps/studio/components/ui/Shortcut*.tsx`
@@ -1,60 +0,0 @@
---
applyTo: 'apps/studio/**,packages/common/telemetry*'
---
# Studio Telemetry Review Rules
All comments are **advisory** — suggest, do not request changes.
## When to Flag Missing Telemetry
Use judgment — not every PR needs telemetry. But **always flag** when:
1. **Changes to `packages/common/telemetry-constants.ts`** — validate event naming, property conventions, and JSDoc accuracy.
2. **PostHog feature flags without measurement.** If a PR uses `usePHFlag` or PostHog-backed hooks like `useDataApiRevokeOnCreateDefaultEnabled` to gate behavior, the flag state should be captured in a telemetry event so the rollout can be measured. Flag if the flag value isn't included in a relevant `track()` call. (Note: `useFlag` from `common` reads ConfigCat flags, not PostHog — different system, different guidance.)
3. **Feature-flagged rollouts without outcome tracking.** If a flag gates new behavior, there should be telemetry on both the flag state _and_ how users respond to the new behavior (e.g., toggle clicks, opt-in actions).
4. **Growth-oriented components adding user interactions without tracking** — onboarding flows, setup wizards, upgrade CTAs, A/B experiment variants.
When tracking is missing, comment: _"This adds a user interaction (or feature flag) that may benefit from tracking."_ Then propose an event name and `useTrack()` call.
## Feature Flag Telemetry Pattern
When capturing a PostHog flag value for telemetry, read the raw flag via `usePHFlag('flagName')` — **not** through wrapper hooks that coerce `undefined` to `false`. Use conditional spread so the property is omitted (not false) when the flag store hasn't loaded:
```typescript
const flagValue = usePHFlag<boolean>('myBooleanFlag') // for boolean flags
track('event_name', {
...(flagValue !== undefined && { myFlagEnabled: flagValue }),
})
```
For string-valued flags (e.g., experiment variants), use `usePHFlag<string>('flagName')` instead.
## Event Naming
Format: `[object]_[verb]` in snake_case.
Prefer verbs already in use in `packages/common/telemetry-constants.ts`: `opened`, `clicked`, `submitted`, `created`, `removed`, `updated`, `intended`, `evaluated`, `added`, `enabled`, `disabled`, `copied`, `exposed`, `failed`, `converted`, `closed`, `completed`, `applied`, `sent`, `moved`.
Flag: unapproved verbs (`saved`, `viewed`, `pressed`), wrong order (`click_product_card`), wrong casing (`productCardClicked`), passive view tracking on page load (exception: `_exposed` events for A/B experiments).
## Event Properties
- **camelCase** for new events; match existing convention when extending
- Self-explanatory names — flag generic (`label`, `value`, `name`, `data`)
- Check `telemetry-constants.ts` for consistency with similar events
- Never track PII
## Event Implementation
- Use `useTrack` from `lib/telemetry/track` — avoid introducing new `useSendEventMutation` usage
- New events need a TypeScript interface in `telemetry-constants.ts` with `@group Events` and `@source` JSDoc tags (add `@page` when applicable for page-specific events), added to the `TelemetryEvent` union
```typescript
import { useTrack } from 'lib/telemetry/track'
const track = useTrack()
track('product_card_clicked', { productType: 'database', planTier: 'pro' })
```
Canonical standards: `.claude/skills/telemetry-standards/SKILL.md`
@@ -1,29 +0,0 @@
---
applyTo: "apps/studio/**"
---
# Studio Testing Review Rules
All comments are **advisory**.
## Core Principle
Push logic out of React components into pure `.utils.ts` functions, then test those functions exhaustively. Only use component tests for complex UI interactions.
## When to Comment
- PR adds **business logic inline in a component** that could be extracted to a `ComponentName.utils.ts` file next to the component and unit tested at `tests/components/.../ComponentName.utils.test.ts`
- PR adds a **utility function without test coverage**
- PR uses **component tests for pure logic** that should be a unit test on a pure function
- PR adds a **feature used in both self-hosted and platform** without E2E test consideration
## Which Test Type to Suggest
- **Pure transformation** (parse, format, validate, compute) → extract to `.utils.ts` + unit test with vitest
- **Complex UI interaction** → component test with `customRender` (or E2E if shared with self-hosted)
- **E2E tests** should cover both click interactions AND keyboard shortcuts
- **No tests at all** for non-trivial changes → nudge to add coverage
## Reference
See `.claude/skills/studio-testing/SKILL.md` for the full testing standard.
+1 -3
View File
@@ -121,10 +121,8 @@ next-env.d.ts
.claude/*
!.claude/settings.json
!.claude/scripts/
!.claude/skills/
!.claude/skills
.agents/skills/me-*
.claude/skills/me-*
!.claude/CLAUDE.md
CLAUDE.local.md
#include template .env file for docker-compose
+21 -18
View File
@@ -4,22 +4,24 @@ pnpm 11 + Turborepo monorepo. Requires Node >= 22.13.
## Structure
| 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 |
| Directory | Purpose |
| ------------------------ | ------------------------------------------------------------------------------------------------------------------ |
| `apps/studio` | Supabase Studio/Dashboard — has its own `apps/studio/AGENTS.md` (see below) |
| `apps/docs` | Documentation site — Next.js app router, MDX (port 3001, served under `/docs`) — has its own `apps/docs/AGENTS.md` |
| `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 |
| `apps/kb` | Knowledge base — Astro — has its own `apps/kb/AGENTS.md` |
| `apps/learn` | Courses site — Next.js + Contentlayer (port 3007), early stage |
| `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
@@ -27,6 +29,7 @@ pnpm 11 + Turborepo monorepo. Requires Node >= 22.13.
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 dev:kb # run knowledge base dev server
pnpm test:studio # Studio unit tests (vitest)
pnpm e2e # Studio E2E tests (playwright)
pnpm build --filter=studio # build Studio
@@ -57,7 +60,7 @@ Never hand-edit generated files: `packages/api-types/types/**`, `**/routeTree.ge
## Skills
The skills in `.claude/skills/` are the source of truth for conventions — load the relevant ones before working, don't guess:
The skills in `.agents/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
- `pm-the-docs` / `write-the-docs` / `edit-the-docs` / `ask-the-docs` / `review-the-docs` — anything under `apps/docs` (see `apps/docs/CONTRIBUTING.md` for the authoring skill model)
@@ -69,4 +72,4 @@ The skills in `.claude/skills/` are the source of truth for conventions — load
## Studio
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.
Before working on anything in `apps/studio`, read `apps/studio/AGENTS.md` if it isn't already in context — it maps Studio tasks to required skills and covers the TanStack Start migration rules.
+1
View File
@@ -0,0 +1 @@
@AGENTS.md
+27
View File
@@ -0,0 +1,27 @@
# Supabase Docs
Next.js app router + MDX. Dev server: `pnpm dev:docs` → http://localhost:3001/docs (the bare `/` 404s).
## Skills — load before working
`pm-the-docs`, `write-the-docs`, `edit-the-docs`, `ask-the-docs`, and `review-the-docs` back the docs authoring process — see `CONTRIBUTING.md` for which stage each covers. For architecture questions (MDX pipeline, GraphQL endpoint, search embeddings, federated docs, build pipeline), `ask-the-docs` has the reference notes.
## Test requirements
Before running tests for `apps/docs`, ensure local Supabase is available and the DB is in a known state.
### Recommended sequence
```bash
pnpm supabase status
pnpm supabase start # if not running
pnpm supabase db reset --local
pnpm run -F docs test:local:unwatch
```
### Notes
- Always reset the local DB before running docs tests to avoid state leakage.
- Prefer `test:local:unwatch` for non-watch CI-like runs. Append a path to run a single file: `pnpm run -F docs test:local:unwatch internals/internal-links.test.ts`.
- `pnpm test` (from `apps/docs`) wraps `test:local` in `supabase start` / `supabase stop`, but does not reset the DB and runs in watch mode, so it is not a substitute for the sequence above.
- MDX content lint is `pnpm lint:mdx` (from `apps/docs`); it lints the whole `content/` tree and takes no path arguments.
+1
View File
@@ -0,0 +1 @@
@AGENTS.md
+3 -3
View File
@@ -21,9 +21,9 @@ To make docs as clear as possible:
## AI agent skills for docs authoring
If you're using Claude Code or Cursor, this repo ships skills that back the [Write the docs](../../.agents/skills/pm-the-docs/reference/write-the-docs-checklist.md) authoring checklist.
If you're using an AI coding agent (Claude Code, Codex, or anything else that reads `.agents/skills/`), this repo ships skills that back the [Write the docs](../../.agents/skills/pm-the-docs/reference/write-the-docs-checklist.md) authoring checklist.
Invoke a skill by name: `/write-the-docs`, `/edit-the-docs`, `/ask-the-docs`, `/pm-the-docs`, `/review-the-docs`.
Ask your agent for a skill by name (`write-the-docs`, `edit-the-docs`, `ask-the-docs`, `pm-the-docs`, `review-the-docs`); in Claude Code these are also available as `/name` slash commands.
| Skill | Checklist stage | Use for |
| --- | --- | --- |
@@ -33,7 +33,7 @@ Invoke a skill by name: `/write-the-docs`, `/edit-the-docs`, `/ask-the-docs`, `/
| [`edit-the-docs`](../../.agents/skills/edit-the-docs/SKILL.md) | Edit | Restructure and improve existing pages |
| [`review-the-docs`](../../.agents/skills/review-the-docs/SKILL.md) | Self-review / PR review | Checking a draft and PR triage/verification |
The canonical files live in `.agents/skills/`, with Git symlinks in `.claude/skills/` and `.cursor/skills/`.
The canonical files live in `.agents/skills/`; `.claude/skills` is a Git symlink to that directory so Claude Code discovers them too.
## Document types
-1
View File
@@ -1 +0,0 @@
AGENTS.md
+1
View File
@@ -0,0 +1 @@
@AGENTS.md
+89
View File
@@ -0,0 +1,89 @@
# Supabase Studio
Next.js pages router + TanStack Start (mid-migration, see below), React 19. Dev server: `pnpm dev:studio` → http://localhost:8082.
## Skills — load before working
Load the skills matching the task; stack them when a task spans areas:
| Task | Additional skills |
| ------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------- |
| Query/mutation hooks, query keys (`data/**`) | `studio-queries` |
| UI: pages, forms, tables, charts, sheets, empty states | `studio-ui-patterns` |
| Form logic: react-hook-form fields, watch/formState, reset, number inputs | `react-hook-form` |
| Displaying API errors | `studio-error-handling` |
| Tests (deciding, writing, reviewing) | `studio-testing`, then `studio-mock-api-tests` (component/MSW) or `studio-e2e-tests` (Playwright) |
| PostHog event tracking | `telemetry-standards` |
| SQL against user databases | `safe-sql-execution` |
| Logs Explorer SQL, `data/logs` | `clickhouse-logs-queries` |
| Component API design, boolean-prop refactors | `vercel-composition-patterns` |
| Keyboard shortcuts, search/filter inputs | `studio-shortcuts` |
| User-facing copy | `copywriting` |
## TanStack Start migration
Studio is migrating from the Next.js pages router (`pages/**`) to TanStack Start (`routes/**`). Both runtimes ship side-by-side; the `STUDIO_FRAMEWORK` env var selects which one `pnpm dev`/`build` runs (default: `next`, resolved in `scripts/dispatch.js`). Full route map and strategy: `TANSTACK_MIGRATION.md`.
- **Never delete a page file.** Most `routes/**` files are thin wrappers re-exporting the default export of their `pages/**` counterpart, so the Next file is load-bearing for both runtimes until the final cleanup pass.
- Pure page-body edits propagate to the route automatically. Mirror a change by hand into the corresponding `routes/**` file only when it touches what the route duplicates: `getLayout`/layout wrapping, page titles or other `staticData` (incl. `skip*Layout` flags), `withAuth`, or redirect paths.
- A new page under `pages/**` needs a matching route under `routes/**` plus a checklist entry in `TANSTACK_MIGRATION.md`.
- New code uses native TanStack APIs — no `next/router` or `next/link`. The `compat/next/` shims exist only for legacy re-exported pages.
- `routeTree.gen.ts` is generated by the Vite plugin — never hand-edit.
## Orientation
- **Data layer** — all platform API calls go through `data/fetchers.ts` (`openapi-fetch`, typed by the generated `api-types` package) with `handleError`; never raw `fetch`. One folder per resource in `data/`, most with a `keys.ts` query-key factory.
- **State** — valtio for global state (`state/`), nuqs for URL state, react-hook-form + zod for forms.
- **Platform vs self-hosted** — `IS_PLATFORM` gates platform-only behavior; `withAuth` is a no-op when self-hosted.
- **Telemetry** — `useTrack()` from `lib/telemetry/track`; event types live in `packages/common/telemetry-constants.ts`.
- **Tests** — default to including relevant tests with any change: a couple of unit tests for extracted logic, component tests for UI behavior, E2E only when the scope demands it (`studio-testing` has the decision tree). Not every PR needs them, but "no tests" should be a considered choice, not the default. Tooling: vitest + MSW; component tests use `customRender` + `addAPIMock` from `tests/lib/`; unhandled network requests fail tests. Don't `vi.mock('@/data/...')`.
- **Shortcuts** — use the registry in `state/shortcuts/` and `components/ui/Shortcut*.tsx`; keep `G then …` chords for navigation; no one-off keyboard listeners (`studio-shortcuts` has the full pattern, including the search-input Escape handler).
- **Scoped PAT catalog** — `packages/shared-data/scoped-access-token-permissions.ts` feeds Studio and the generated Personal Access Tokens guide. After changing it, run `make -C apps/docs/spec generate.partials.access-control`; Docs Tests rejects stale tables.
- **Reuse first** — before writing a new hook or helper, search for an existing one (`hooks/`, `lib/`, `packages/common`, `packages/ui-patterns`). If you do need a new one, make it as reusable as possible: general naming, no page-specific coupling, placed where other callers can find it.
- Co-locate sub-components with their parent; avoid barrel re-export files.
## Code style
Older Studio code predates some of these conventions. For new or modified code, follow them rather than mirroring nearby legacy patterns:
- **Booleans** read as `is`/`has`/`can`/`should`. Derive them from existing state (`const isFormValid = name.length > 0 && email.includes('@')`) — mirroring a derivable value into `useState` synced by `useEffect` is a bug pattern. Give multi-condition logic a name (`const canShowAddButton = !isSchemaLocked && canUpdateColumns && …`) instead of inlining the chain in JSX.
- **Ternaries**: one is fine for a binary choice; never nest them. Anything bigger flattens — early returns in statement position, sibling `&&` blocks in JSX.
- **Fetch states** render with early returns at the top level, or a flat `&&` chain with mutually exclusive guards inline — never a nested ternary:
```tsx
// Top level: early return per state
if (isLoading) return <GenericSkeletonLoader />
if (isError) return <AlertError error={error} subject="Failed to retrieve data" />
if (isSuccess && data.length === 0) return <EmptyState />
return <DataDisplay data={data} />
// Inline: flat `&&` blocks, mutually exclusive guards
<div>
{isLoading && <ShimmeringLoader />}
{isError && <AlertError error={error} />}
{isSuccess && data.length === 0 && <EmptyState />}
{isSuccess && data.length > 0 && <DataDisplay data={data} />}
</div>
```
- **`useEffect` is for synchronizing with external systems** (subscriptions, DOM, timers) — not for deriving data (compute it in render), reacting to user actions (do it in the handler), or fetching (React Query). Older code uses effects for all of these; don't copy it.
- **State** stays as local as possible — lift it only when it's actually shared. Related form fields belong in a single react-hook-form + zod form, not parallel `useState` calls.
- **Component size**: split at ~200–300 lines — or sooner when a component grows multiple distinct UI sections, tangled conditional rendering, or clusters of unrelated `useState`. Extract repeated JSX into small components, non-trivial pure logic into `.utils.ts` functions (which get unit tests), and reusable stateful logic into custom hooks.
- **Memoization is not the default**: `useMemo`/`useCallback` only for measured expense or referential stability a memoized child depends on.
- **TypeScript**: avoid `as` casts — where external data enters, parse it with zod (`schema.parse`/`safeParse`) instead. Model multi-state values as discriminated unions (`{ status: 'success'; data: T } | { status: 'error'; error: Error }`) rather than independent boolean flags.
- **Naming**: prop callbacks are `onX`, internal handlers are `handleX`. Custom hooks return objects, not tuples.
- **Refactoring**: when you move or extract code into a new module, update every importer to point at the new location directly — do **not** leave a re-export shim in the old file "for backward compatibility." It's a one-line import change per consumer, and keeping shims around makes the codebase messy and the true source of a symbol ambiguous.
## Defaults that differ here
- **ESLint warnings are ratcheted in CI**: the per-rule occurrence count must not increase, so a new `any`, unresolved `exhaustive-deps` warning, or default export fails the build even though it's "only a warning". Check locally with `pnpm --filter studio run lint:ratchet`.
- **Dead files and deps are gated in CI** by knip (`pnpm knip --workspace apps/studio` locally). Framework-convention files nothing imports (routes, Vercel functions, TanStack Start files) belong in `knip.jsonc` under `workspaces["apps/studio"].entry`, not `ignore` — an ignored file's imports aren't traced, so anything only it uses gets reported as dead. There's no inline `knip-ignore` comment; the only per-file opt-out is config. In a PR stack, a file whose first consumer lands in a later PR fails the gate on the earlier one — either add it in the same PR as its first use, or add it to `workspaces["apps/studio"].ignore` with a `used from #NNN` comment and remove it in that PR.
- **Clipboard**: `copyToClipboard` from `'ui'`, and never `await` anything before calling it (Safari requires the write inside the user gesture; lint-enforced) — pass a Promise as the argument instead.
- **`useParams()` comes from `'common'`**, not `next/navigation` — it camelCases keys and returns `string | undefined`.
- **Permissions**: `useAsyncCheckPermissions` from `hooks/misc/useCheckPermissions` (returns `can: true` when self-hosted).
- **Gating**: `useIsFeatureEnabled` for product features, `useFlag` from `'common'` for feature flags — two different systems.
- **Dates**: `dayjs` (plugins pre-loaded at both entries, `pages/_app.tsx` and `routes/__root.tsx`), not `date-fns`. **Toasts**: `toast` from `'sonner'`.
- **Import split**: `'ui'` = primitives, `'ui-patterns'` = composed patterns (`ConfirmationModal`, …), `@ui/*` = alias into `packages/ui/src`. Icons come from `lucide-react`.
- **New tables** use `@tanstack/react-table`; `react-data-grid` is banned for new code.
- **Ad-hoc SQL** against the user's database goes through `executeSql` / `useExecuteSqlMutation` (`data/sql/execute-sql-mutation`).
- **Confirmations**: `ConfirmationModal` / `TextConfirmModal` from `ui-patterns`, never `window.confirm`. Disabled buttons needing an explanation use `ButtonTooltip`; inline warnings use `Admonition`.
+1 -88
View File
@@ -1,88 +1 @@
# Supabase Studio
Next.js pages router + TanStack Start (mid-migration, see below), React 19. Dev server: `pnpm dev:studio` → http://localhost:8082.
## Skills — load before working
Load the skills matching the task; stack them when a task spans areas:
| Task | Additional skills |
| ------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------- |
| Query/mutation hooks, query keys (`data/**`) | `studio-queries` |
| UI: pages, forms, tables, charts, sheets, empty states | `studio-ui-patterns` |
| Form logic: react-hook-form fields, watch/formState, reset, number inputs | `react-hook-form` |
| Displaying API errors | `studio-error-handling` |
| Tests (deciding, writing, reviewing) | `studio-testing`, then `studio-mock-api-tests` (component/MSW) or `studio-e2e-tests` (Playwright) |
| PostHog event tracking | `telemetry-standards` |
| SQL against user databases | `safe-sql-execution` |
| Logs Explorer SQL, `data/logs` | `clickhouse-logs-queries` |
| Component API design, boolean-prop refactors | `vercel-composition-patterns` |
| User-facing copy | `copywriting` |
## TanStack Start migration
Studio is migrating from the Next.js pages router (`pages/**`) to TanStack Start (`routes/**`). Both runtimes ship side-by-side; the `STUDIO_FRAMEWORK` env var selects which one `pnpm dev`/`build` runs (default: `next`, resolved in `scripts/dispatch.js`). Full route map and strategy: `TANSTACK_MIGRATION.md`.
- **Never delete a page file.** Most `routes/**` files are thin wrappers re-exporting the default export of their `pages/**` counterpart, so the Next file is load-bearing for both runtimes until the final cleanup pass.
- Pure page-body edits propagate to the route automatically. Mirror a change by hand into the corresponding `routes/**` file only when it touches what the route duplicates: `getLayout`/layout wrapping, page titles or other `staticData` (incl. `skip*Layout` flags), `withAuth`, or redirect paths.
- A new page under `pages/**` needs a matching route under `routes/**` plus a checklist entry in `TANSTACK_MIGRATION.md`.
- New code uses native TanStack APIs — no `next/router` or `next/link`. The `compat/next/` shims exist only for legacy re-exported pages.
- `routeTree.gen.ts` is generated by the Vite plugin — never hand-edit.
## Orientation
- **Data layer** — all platform API calls go through `data/fetchers.ts` (`openapi-fetch`, typed by the generated `api-types` package) with `handleError`; never raw `fetch`. One folder per resource in `data/`, most with a `keys.ts` query-key factory.
- **State** — valtio for global state (`state/`), nuqs for URL state, react-hook-form + zod for forms.
- **Platform vs self-hosted** — `IS_PLATFORM` gates platform-only behavior; `withAuth` is a no-op when self-hosted.
- **Telemetry** — `useTrack()` from `lib/telemetry/track`; event types live in `packages/common/telemetry-constants.ts`.
- **Tests** — default to including relevant tests with any change: a couple of unit tests for extracted logic, component tests for UI behavior, E2E only when the scope demands it (`studio-testing` has the decision tree). Not every PR needs them, but "no tests" should be a considered choice, not the default. Tooling: vitest + MSW; component tests use `customRender` + `addAPIMock` from `tests/lib/`; unhandled network requests fail tests. Don't `vi.mock('@/data/...')`.
- **Shortcuts** — use the registry in `state/shortcuts/` and `components/ui/Shortcut*.tsx`; keep `G then …` chords for navigation; no one-off keyboard listeners.
- **Scoped PAT catalog** — `packages/shared-data/scoped-access-token-permissions.ts` feeds Studio and the generated Personal Access Tokens guide. After changing it, run `make -C apps/docs/spec generate.partials.access-control`; Docs Tests rejects stale tables.
- **Reuse first** — before writing a new hook or helper, search for an existing one (`hooks/`, `lib/`, `packages/common`, `packages/ui-patterns`). If you do need a new one, make it as reusable as possible: general naming, no page-specific coupling, placed where other callers can find it.
- Co-locate sub-components with their parent; avoid barrel re-export files.
## Code style
Older Studio code predates some of these conventions. For new or modified code, follow them rather than mirroring nearby legacy patterns:
- **Booleans** read as `is`/`has`/`can`/`should`. Derive them from existing state (`const isFormValid = name.length > 0 && email.includes('@')`) — mirroring a derivable value into `useState` synced by `useEffect` is a bug pattern. Give multi-condition logic a name (`const canShowAddButton = !isSchemaLocked && canUpdateColumns && …`) instead of inlining the chain in JSX.
- **Ternaries**: one is fine for a binary choice; never nest them. Anything bigger flattens — early returns in statement position, sibling `&&` blocks in JSX.
- **Fetch states** render with early returns at the top level, or a flat `&&` chain with mutually exclusive guards inline — never a nested ternary:
```tsx
// Top level: early return per state
if (isLoading) return <GenericSkeletonLoader />
if (isError) return <AlertError error={error} subject="Failed to retrieve data" />
if (isSuccess && data.length === 0) return <EmptyState />
return <DataDisplay data={data} />
// Inline: flat `&&` blocks, mutually exclusive guards
<div>
{isLoading && <ShimmeringLoader />}
{isError && <AlertError error={error} />}
{isSuccess && data.length === 0 && <EmptyState />}
{isSuccess && data.length > 0 && <DataDisplay data={data} />}
</div>
```
- **`useEffect` is for synchronizing with external systems** (subscriptions, DOM, timers) — not for deriving data (compute it in render), reacting to user actions (do it in the handler), or fetching (React Query). Older code uses effects for all of these; don't copy it.
- **State** stays as local as possible — lift it only when it's actually shared. Related form fields belong in a single react-hook-form + zod form, not parallel `useState` calls.
- **Component size**: split at ~200–300 lines — or sooner when a component grows multiple distinct UI sections, tangled conditional rendering, or clusters of unrelated `useState`. Extract repeated JSX into small components, non-trivial pure logic into `.utils.ts` functions (which get unit tests), and reusable stateful logic into custom hooks.
- **Memoization is not the default**: `useMemo`/`useCallback` only for measured expense or referential stability a memoized child depends on.
- **TypeScript**: avoid `as` casts — where external data enters, parse it with zod (`schema.parse`/`safeParse`) instead. Model multi-state values as discriminated unions (`{ status: 'success'; data: T } | { status: 'error'; error: Error }`) rather than independent boolean flags.
- **Naming**: prop callbacks are `onX`, internal handlers are `handleX`. Custom hooks return objects, not tuples.
- **Refactoring**: when you move or extract code into a new module, update every importer to point at the new location directly — do **not** leave a re-export shim in the old file "for backward compatibility." It's a one-line import change per consumer, and keeping shims around makes the codebase messy and the true source of a symbol ambiguous.
## Defaults that differ here
- **ESLint warnings are ratcheted in CI**: the per-rule occurrence count must not increase, so a new `any`, unresolved `exhaustive-deps` warning, or default export fails the build even though it's "only a warning". Check locally with `pnpm --filter studio run lint:ratchet`.
- **Dead files and deps are gated in CI** by knip (`pnpm knip --workspace apps/studio` locally). Framework-convention files nothing imports (routes, Vercel functions, TanStack Start files) belong in `knip.jsonc` under `workspaces["apps/studio"].entry`, not `ignore` — an ignored file's imports aren't traced, so anything only it uses gets reported as dead. There's no inline `knip-ignore` comment; the only per-file opt-out is config. In a PR stack, a file whose first consumer lands in a later PR fails the gate on the earlier one — either add it in the same PR as its first use, or add it to `workspaces["apps/studio"].ignore` with a `used from #NNN` comment and remove it in that PR.
- **Clipboard**: `copyToClipboard` from `'ui'`, and never `await` anything before calling it (Safari requires the write inside the user gesture; lint-enforced) — pass a Promise as the argument instead.
- **`useParams()` comes from `'common'`**, not `next/navigation` — it camelCases keys and returns `string | undefined`.
- **Permissions**: `useAsyncCheckPermissions` from `hooks/misc/useCheckPermissions` (returns `can: true` when self-hosted).
- **Gating**: `useIsFeatureEnabled` for product features, `useFlag` from `'common'` for feature flags — two different systems.
- **Dates**: `dayjs` (plugins pre-loaded at both entries, `pages/_app.tsx` and `routes/__root.tsx`), not `date-fns`. **Toasts**: `toast` from `'sonner'`.
- **Import split**: `'ui'` = primitives, `'ui-patterns'` = composed patterns (`ConfirmationModal`, …), `@ui/*` = alias into `packages/ui/src`. Icons come from `lucide-react`.
- **New tables** use `@tanstack/react-table`; `react-data-grid` is banned for new code.
- **Ad-hoc SQL** against the user's database goes through `executeSql` / `useExecuteSqlMutation` (`data/sql/execute-sql-mutation`).
- **Confirmations**: `ConfirmationModal` / `TextConfirmModal` from `ui-patterns`, never `window.confirm`. Disabled buttons needing an explanation use `ButtonTooltip`; inline warnings use `Admonition`.
@AGENTS.md
+1 -1
View File
@@ -620,5 +620,5 @@ for the Vite pipeline:
- Delete `pages/_app.tsx`, `pages/_document.tsx`, `pages/_error.jsx`, `pages/500.tsx`, `pages/404.tsx` (Next-only catch-alls; TanStack equivalents on `__root.tsx`).
- Drop the `dev:next` / `build:next` / `start:next` scripts from `apps/studio/package.json` once we're committed to TanStack.
- Remove the `apps/studio/pages/**` `path_instructions` guardrail entry from `.coderabbit.yaml` (added in FE-3423; remove it as part of this FE-3106 cleanup) — it's only useful while both runtimes coexist.
- Remove the "TanStack Start migration" section from `apps/studio/CLAUDE.md` — it only applies while both runtimes coexist.
- Remove the "TanStack Start migration" section from `apps/studio/AGENTS.md` — it only applies while both runtimes coexist.
- Delete this file.
@@ -6,7 +6,7 @@
* value flowing from URL parameters, UI inputs, or LLM output must pass through
* a sanitization helper in safe-analytics-sql.ts before reaching the wire.
*
* See .claude/skills/safe-sql-execution/SKILL.md for the full security model.
* See .agents/skills/safe-sql-execution/SKILL.md for the full security model.
*/
import type { SafeLogSqlFragment } from './safe-analytics-sql'
import { get, handleError, post } from '@/data/fetchers'
+1 -1
View File
@@ -5,7 +5,7 @@
// other fragments that originate from URL parameters, UI inputs, or LLM output
// can be spliced into SQL that is executed on behalf of the project. The pattern
// here mirrors the pg-meta safe-SQL model described in
// .claude/skills/safe-sql-execution/SKILL.md: every value that flows from an
// .agents/skills/safe-sql-execution/SKILL.md: every value that flows from an
// external source must pass through a sanitization helper before being
// interpolated, and the wire boundary (`executeAnalyticsSql`) refuses plain
// strings at compile time.
+1 -1
View File
@@ -99,7 +99,7 @@ module.exports = defineConfig([
// Analytics SQL wire boundary: every call to a SQL-bearing analytics
// endpoint (`logs.all` / `logs.all.otel`) must go through
// `executeAnalyticsSql` so the `SafeLogSqlFragment` brand is enforced at the
// type level. See .claude/skills/safe-sql-execution/SKILL.md.
// type level. See .agents/skills/safe-sql-execution/SKILL.md.
{
files: ['**/*.ts', '**/*.tsx'],
ignores: ['data/logs/execute-analytics-sql.ts'],