From f2de2abfe4d5dbf6050c6fe497aca4af02e48f76 Mon Sep 17 00:00:00 2001 From: Jesse <16379819+JesseVent@users.noreply.github.com> Date: Tue, 4 Aug 2026 00:57:50 +0930 Subject: [PATCH 1/2] feat(web): AgentPrism trace viewer for eval runs Adds an AgentPrism-based trace visualization UI (apps/web) and the core adapter that turns any EvalResult into a trace span tree. Rebased cleanly onto current main: - Kept main's regenerated regression-eval-results.json and eval-results.json instead of the stale snapshot this carried, since main has since refreshed both independently. - Dropped the "trigger" experiment-suite trace fixtures and the "trigger" entries in EXPERIMENT_SUITES/url-state, since that suite isn't merged into main's core schema yet and lands separately. - Merged the adjacent (non-conflicting) codex parser test additions from main's #130 alongside this branch's usage-tracking tests. Claude-Session: https://claude.ai/code/session_01WaER4ZeRLmDSwC5GFzkeFQ --- CHANGELOG.md | 18 + README.md | 8 + apps/framework/scripts/export-results.ts | 99 +- apps/web/package.json | 9 + apps/web/src/App.tsx | 28 +- .../web/src/components/agent-prism/Avatar.tsx | 145 +++ apps/web/src/components/agent-prism/Badge.tsx | 94 ++ .../src/components/agent-prism/BrandLogo.tsx | 102 ++ .../web/src/components/agent-prism/Button.tsx | 123 +++ .../agent-prism/CollapseAndExpandControls.tsx | 44 + .../agent-prism/CollapsibleSection.tsx | 124 +++ .../src/components/agent-prism/CopyButton.tsx | 63 ++ .../agent-prism/DetailsView/DetailsView.tsx | 141 +++ .../DetailsView/DetailsViewAttributesTab.tsx | 123 +++ .../DetailsView/DetailsViewContentViewer.tsx | 70 ++ .../DetailsView/DetailsViewHeader.tsx | 99 ++ .../DetailsView/DetailsViewHeaderActions.tsx | 21 + .../DetailsView/DetailsViewInputOutputTab.tsx | 135 +++ .../DetailsView/DetailsViewJsonOutput.tsx | 32 + .../DetailsView/DetailsViewMarkdown.tsx | 102 ++ .../DetailsView/DetailsViewPrettyOutput.tsx | 75 ++ .../DetailsView/DetailsViewRawDataTab.tsx | 31 + .../src/components/agent-prism/IconButton.tsx | 74 ++ .../src/components/agent-prism/PriceBadge.tsx | 14 + .../components/agent-prism/SearchInput.tsx | 17 + .../src/components/agent-prism/SpanBadge.tsx | 54 + .../agent-prism/SpanCard/SpanCard.tsx | 419 ++++++++ .../agent-prism/SpanCard/SpanCardBadges.tsx | 23 + .../SpanCard/SpanCardConnector.tsx | 36 + .../agent-prism/SpanCard/SpanCardTimeline.tsx | 59 ++ .../agent-prism/SpanCard/SpanCardToggle.tsx | 38 + .../src/components/agent-prism/SpanStatus.tsx | 79 ++ .../components/agent-prism/TabSelector.tsx | 35 + apps/web/src/components/agent-prism/Tabs.tsx | 139 +++ .../src/components/agent-prism/TextInput.tsx | 143 +++ .../components/agent-prism/TimestampBadge.tsx | 22 + .../components/agent-prism/TokensBadge.tsx | 28 + .../agent-prism/TraceList/TraceList.tsx | 87 ++ .../agent-prism/TraceList/TraceListItem.tsx | 104 ++ .../TraceList/TraceListItemHeader.tsx | 37 + .../agent-prism/TraceViewer/TraceViewer.tsx | 192 ++++ .../TraceViewer/TraceViewerDesktopLayout.tsx | 111 ++ .../TraceViewer/TraceViewerMobileLayout.tsx | 109 ++ .../TraceViewer/TraceViewerPlaceholder.tsx | 5 + .../TraceViewerSearchAndControls.tsx | 30 + .../TraceViewerTreeViewContainer.tsx | 76 ++ .../src/components/agent-prism/TreeView.tsx | 70 ++ .../src/components/agent-prism/shared.test.ts | 49 + apps/web/src/components/agent-prism/shared.ts | 213 ++++ .../src/components/agent-prism/theme/index.ts | 99 ++ .../components/agent-prism/theme/theme.css | 232 +++++ .../src/components/results/eval-details.tsx | 16 + apps/web/src/components/trace-panel.tsx | 141 +++ .../traces/build-cli-001-bootstrap-app.json | 343 +++++++ .../build-cli-002-declarative-schema.json | 442 ++++++++ .../build-cli-003-pg-cron-queue-workflow.json | 335 +++++++ ...base-001-migrate-postgres-to-supabase.json | 597 +++++++++++ .../traces/build-frontend-001-todos-app.json | 300 ++++++ .../build-functions-001-order-total.json | 230 +++++ .../build-functions-002-edge-auth-db.json | 211 ++++ .../build-functions-003-todos-crud-api.json | 334 ++++++ ...ild-functions-004-service-role-bypass.json | 315 ++++++ ...d-functions-005-dual-auth-user-secret.json | 701 +++++++++++++ ...d-functions-006-dual-auth-with-server.json | 773 ++++++++++++++ .../build-realtime-001-live-chat-updates.json | 258 +++++ .../build-rls-002-own-todos-client.json | 287 ++++++ .../build-rls-003-org-roles-permissions.json | 418 ++++++++ ...ild-storage-001-private-bucket-access.json | 311 ++++++ .../build-tests-001-rls-tenant-isolation.json | 443 ++++++++ ...uild-vectors-001-rag-with-permissions.json | 409 ++++++++ ...eploy-database-001-prometheus-metrics.json | 443 ++++++++ ...y-functions-001-edge-function-secrets.json | 454 +++++++++ ...eploy-self-hosting-001-docker-compose.json | 334 ++++++ ...estigate-auth-001-deleted-user-access.json | 397 ++++++++ .../investigate-db-001-table-row-counts.json | 197 ++++ ...gate-functions-001-546-resource-limit.json | 221 ++++ ...vestigate-logs-001-top-error-function.json | 152 +++ ...ate-realtime-001-subscribed-no-events.json | 290 ++++++ ...gate-reliability-001-error-rate-spike.json | 175 ++++ ...te-reliability-002-subtle-error-spike.json | 187 ++++ ...ity-003-edge-function-5xx-correlation.json | 236 +++++ ...investigate-security-001-public-table.json | 244 +++++ .../resolve-dataapi-001-empty-results.json | 433 ++++++++ ...lve-dataapi-002-secure-default-grants.json | 412 ++++++++ ...dataapi-002-update-zero-rows-affected.json | 328 ++++++ ...tabase-001-migration-history-mismatch.json | 428 ++++++++ ...-performance-001-slow-query-cpu-spike.json | 339 +++++++ ...bility-001-unhealthy-project-recovery.json | 166 +++ ...olve-security-001-rls-cross-user-leak.json | 279 ++++++ ...ve-security-002-rls-cross-tenant-leak.json | 383 +++++++ ...rage-001-upsert-missing-update-policy.json | 313 ++++++ apps/web/src/index.css | 86 ++ apps/web/src/lib/url-state.ts | 7 + packages/core/package.json | 1 + .../src/agents/claude-code/parser.test.ts | 77 +- .../core/src/agents/claude-code/parser.ts | 63 +- packages/core/src/agents/codex/parser.test.ts | 75 ++ packages/core/src/agents/codex/parser.ts | 67 +- .../core/src/agents/opencode/parser.test.ts | 86 ++ packages/core/src/agents/opencode/parser.ts | 75 +- packages/core/src/index.ts | 74 +- packages/core/src/parsers/adapt.ts | 10 +- packages/core/src/trace-viewer.test.ts | 529 ++++++++++ packages/core/src/trace-viewer.ts | 376 +++++++ packages/core/src/transcript/types.ts | 22 + pnpm-lock.yaml | 948 ++++++++++++++++++ 106 files changed, 20016 insertions(+), 35 deletions(-) create mode 100644 CHANGELOG.md create mode 100644 apps/web/src/components/agent-prism/Avatar.tsx create mode 100644 apps/web/src/components/agent-prism/Badge.tsx create mode 100644 apps/web/src/components/agent-prism/BrandLogo.tsx create mode 100644 apps/web/src/components/agent-prism/Button.tsx create mode 100644 apps/web/src/components/agent-prism/CollapseAndExpandControls.tsx create mode 100644 apps/web/src/components/agent-prism/CollapsibleSection.tsx create mode 100644 apps/web/src/components/agent-prism/CopyButton.tsx create mode 100644 apps/web/src/components/agent-prism/DetailsView/DetailsView.tsx create mode 100644 apps/web/src/components/agent-prism/DetailsView/DetailsViewAttributesTab.tsx create mode 100644 apps/web/src/components/agent-prism/DetailsView/DetailsViewContentViewer.tsx create mode 100644 apps/web/src/components/agent-prism/DetailsView/DetailsViewHeader.tsx create mode 100644 apps/web/src/components/agent-prism/DetailsView/DetailsViewHeaderActions.tsx create mode 100644 apps/web/src/components/agent-prism/DetailsView/DetailsViewInputOutputTab.tsx create mode 100644 apps/web/src/components/agent-prism/DetailsView/DetailsViewJsonOutput.tsx create mode 100644 apps/web/src/components/agent-prism/DetailsView/DetailsViewMarkdown.tsx create mode 100644 apps/web/src/components/agent-prism/DetailsView/DetailsViewPrettyOutput.tsx create mode 100644 apps/web/src/components/agent-prism/DetailsView/DetailsViewRawDataTab.tsx create mode 100644 apps/web/src/components/agent-prism/IconButton.tsx create mode 100644 apps/web/src/components/agent-prism/PriceBadge.tsx create mode 100644 apps/web/src/components/agent-prism/SearchInput.tsx create mode 100644 apps/web/src/components/agent-prism/SpanBadge.tsx create mode 100644 apps/web/src/components/agent-prism/SpanCard/SpanCard.tsx create mode 100644 apps/web/src/components/agent-prism/SpanCard/SpanCardBadges.tsx create mode 100644 apps/web/src/components/agent-prism/SpanCard/SpanCardConnector.tsx create mode 100644 apps/web/src/components/agent-prism/SpanCard/SpanCardTimeline.tsx create mode 100644 apps/web/src/components/agent-prism/SpanCard/SpanCardToggle.tsx create mode 100644 apps/web/src/components/agent-prism/SpanStatus.tsx create mode 100644 apps/web/src/components/agent-prism/TabSelector.tsx create mode 100644 apps/web/src/components/agent-prism/Tabs.tsx create mode 100644 apps/web/src/components/agent-prism/TextInput.tsx create mode 100644 apps/web/src/components/agent-prism/TimestampBadge.tsx create mode 100644 apps/web/src/components/agent-prism/TokensBadge.tsx create mode 100644 apps/web/src/components/agent-prism/TraceList/TraceList.tsx create mode 100644 apps/web/src/components/agent-prism/TraceList/TraceListItem.tsx create mode 100644 apps/web/src/components/agent-prism/TraceList/TraceListItemHeader.tsx create mode 100644 apps/web/src/components/agent-prism/TraceViewer/TraceViewer.tsx create mode 100644 apps/web/src/components/agent-prism/TraceViewer/TraceViewerDesktopLayout.tsx create mode 100644 apps/web/src/components/agent-prism/TraceViewer/TraceViewerMobileLayout.tsx create mode 100644 apps/web/src/components/agent-prism/TraceViewer/TraceViewerPlaceholder.tsx create mode 100644 apps/web/src/components/agent-prism/TraceViewer/TraceViewerSearchAndControls.tsx create mode 100644 apps/web/src/components/agent-prism/TraceViewer/TraceViewerTreeViewContainer.tsx create mode 100644 apps/web/src/components/agent-prism/TreeView.tsx create mode 100644 apps/web/src/components/agent-prism/shared.test.ts create mode 100644 apps/web/src/components/agent-prism/shared.ts create mode 100644 apps/web/src/components/agent-prism/theme/index.ts create mode 100644 apps/web/src/components/agent-prism/theme/theme.css create mode 100644 apps/web/src/components/trace-panel.tsx create mode 100644 apps/web/src/data/traces/build-cli-001-bootstrap-app.json create mode 100644 apps/web/src/data/traces/build-cli-002-declarative-schema.json create mode 100644 apps/web/src/data/traces/build-cli-003-pg-cron-queue-workflow.json create mode 100644 apps/web/src/data/traces/build-database-001-migrate-postgres-to-supabase.json create mode 100644 apps/web/src/data/traces/build-frontend-001-todos-app.json create mode 100644 apps/web/src/data/traces/build-functions-001-order-total.json create mode 100644 apps/web/src/data/traces/build-functions-002-edge-auth-db.json create mode 100644 apps/web/src/data/traces/build-functions-003-todos-crud-api.json create mode 100644 apps/web/src/data/traces/build-functions-004-service-role-bypass.json create mode 100644 apps/web/src/data/traces/build-functions-005-dual-auth-user-secret.json create mode 100644 apps/web/src/data/traces/build-functions-006-dual-auth-with-server.json create mode 100644 apps/web/src/data/traces/build-realtime-001-live-chat-updates.json create mode 100644 apps/web/src/data/traces/build-rls-002-own-todos-client.json create mode 100644 apps/web/src/data/traces/build-rls-003-org-roles-permissions.json create mode 100644 apps/web/src/data/traces/build-storage-001-private-bucket-access.json create mode 100644 apps/web/src/data/traces/build-tests-001-rls-tenant-isolation.json create mode 100644 apps/web/src/data/traces/build-vectors-001-rag-with-permissions.json create mode 100644 apps/web/src/data/traces/deploy-database-001-prometheus-metrics.json create mode 100644 apps/web/src/data/traces/deploy-functions-001-edge-function-secrets.json create mode 100644 apps/web/src/data/traces/deploy-self-hosting-001-docker-compose.json create mode 100644 apps/web/src/data/traces/investigate-auth-001-deleted-user-access.json create mode 100644 apps/web/src/data/traces/investigate-db-001-table-row-counts.json create mode 100644 apps/web/src/data/traces/investigate-functions-001-546-resource-limit.json create mode 100644 apps/web/src/data/traces/investigate-logs-001-top-error-function.json create mode 100644 apps/web/src/data/traces/investigate-realtime-001-subscribed-no-events.json create mode 100644 apps/web/src/data/traces/investigate-reliability-001-error-rate-spike.json create mode 100644 apps/web/src/data/traces/investigate-reliability-002-subtle-error-spike.json create mode 100644 apps/web/src/data/traces/investigate-reliability-003-edge-function-5xx-correlation.json create mode 100644 apps/web/src/data/traces/investigate-security-001-public-table.json create mode 100644 apps/web/src/data/traces/resolve-dataapi-001-empty-results.json create mode 100644 apps/web/src/data/traces/resolve-dataapi-002-secure-default-grants.json create mode 100644 apps/web/src/data/traces/resolve-dataapi-002-update-zero-rows-affected.json create mode 100644 apps/web/src/data/traces/resolve-database-001-migration-history-mismatch.json create mode 100644 apps/web/src/data/traces/resolve-performance-001-slow-query-cpu-spike.json create mode 100644 apps/web/src/data/traces/resolve-reliability-001-unhealthy-project-recovery.json create mode 100644 apps/web/src/data/traces/resolve-security-001-rls-cross-user-leak.json create mode 100644 apps/web/src/data/traces/resolve-security-002-rls-cross-tenant-leak.json create mode 100644 apps/web/src/data/traces/resolve-storage-001-upsert-missing-update-policy.json create mode 100644 packages/core/src/trace-viewer.test.ts create mode 100644 packages/core/src/trace-viewer.ts diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 00000000..9ed474f2 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,18 @@ +# Changelog + +Notable changes to this repo. Not every commit — see `git log` for that. + +## 2026-08-02 + +### Added + +- **AgentPrism trace viewer**: a "View span tree" link on every result row opens a full trace panel — span tree, per-span duration/token badges, tool call input/output, docs activity. Backed by a new `EvalResult → AgentPrism spans` adapter (`packages/core/src/trace-viewer.ts`) and lazy per-eval trace export from `pnpm export-results`. +- Real per-turn token usage surfaced as the trace viewer's token badges (previously only aggregate totals were available). +- `DetailsViewPrettyOutput`: a human-readable "Plain" view for tool call JSON — renders real line breaks instead of forcing multi-line shell commands/output onto one escaped-`\n` line. Now the default view for structured tool call data; exact "JSON" stays available alongside it. + +### Fixed + +- `--agentprism-secondary` (feeds Button, Avatar, Tabs, SpanCardConnector, TraceListItem, and the trace timeline's track background) inherited the app's own `--secondary` token, which lightens off `--background` — invisible in light mode, where the background is already near-max lightness. Rebased on a foreground-tinted overlay so it stays visible in both themes. +- The trace row's title/timeline split used a hardcoded 600px JS width tuned for a wider reference layout; replaced with a CSS grid (`minmax(0,1fr) auto`) so it adapts to whatever panel width actually exists instead of overflowing or wrapping. +- Per-span-type accent colors (`SPAN_ACCENT_COLORS`) were keyed by short names (`tool`, `agent`, `llm`) against `TraceSpanCategory`'s long-form values (`tool_execution`, `agent_invocation`, `llm_call`) — every span except `event` silently fell back to the same gray "unknown" accent. Since removed in favor of no per-row accent border (see below), but worth noting for anyone re-adding one. +- Removed the per-row `border-l-2` type-accent border (read as visual noise); kept the tree connector guide lines. diff --git a/README.md b/README.md index 31527982..46e8ee84 100644 --- a/README.md +++ b/README.md @@ -78,6 +78,14 @@ Start the web app development server: pnpm web ``` +### Trace viewer + +Every row in the results table has a "View span tree" link that opens an [AgentPrism](https://github.com/evilmartians/agent-prism)-based trace panel: the full span tree for that run — system/user messages, each tool call with its input/output, the assistant's turns, and check results — with per-span duration, token usage, and status. + +`pnpm export-results` writes these lazily: alongside the aggregate `eval-results.json`, it emits one `apps/web/src/data/traces/.json` per eval (via `evalResultToTraceSpans` in `packages/core/src/trace-viewer.ts`, which adapts a run's raw transcript into AgentPrism's span format). The web app fetches a trace only when its row is opened, so the aggregate bundle stays lean. Pass `--no-traces` to `export-results` to skip writing them. + +Tool call input/output defaults to a human-readable "Plain" view with real line breaks — multi-line shell commands and command output are unreadable as JSON's escaped `\n` form. Switch to the "JSON" tab for exact, copy-pasteable JSON. + ## Eval Shape Every eval contains: diff --git a/apps/framework/scripts/export-results.ts b/apps/framework/scripts/export-results.ts index 3b32923f..40919c09 100644 --- a/apps/framework/scripts/export-results.ts +++ b/apps/framework/scripts/export-results.ts @@ -7,7 +7,9 @@ import { fileURLToPath } from 'node:url'; import { parseEvalMarkdown } from '@supabase-evals/core/eval-markdown'; import { rawEvalResultSchema } from '@supabase-evals/core/eval-metadata'; import { + evalResultToTraceSpans, getExperimentDisplayMetadata, + type EvalResultTraceInput, type ExperimentConfig, type ExperimentDisplayMetadata, } from '@supabase-evals/core'; @@ -16,6 +18,7 @@ import type { EvalSuite, ExperimentSuite, } from '@supabase-evals/core/eval-metadata'; +import type { ToolCallRecord, TranscriptPart } from '@supabase-evals/core'; import { normalizeExperimentName, readExperimentSuiteFilters, @@ -36,6 +39,7 @@ const OUTPUT_PATH = join( 'data', 'eval-results.json' ); +const TRACES_DIR = join(ROOT, 'apps', 'web', 'src', 'data', 'traces'); type ExperimentExportMetadata = { display: ExperimentDisplayMetadata; @@ -66,6 +70,10 @@ const EVAL_FILTERS = readRepeatedFlag(rawArgs, 'eval'); const SUITE_FILTERS = readSuiteFilters(rawArgs); const EXPERIMENT_SUITE_FILTERS = readExperimentSuiteFilters(rawArgs); const MERGE = rawArgs.includes('--merge'); +// Per-eval trace JSON for the web viewer's TracePanel. On by default; flip with +// `--no-traces`. Written lazily (one file per evalId) so the aggregate bundle +// stays lean — the web app fetches a trace only when a row is selected. +const WRITE_TRACES = !rawArgs.includes('--no-traces'); const OUTPUT_FLAG = readRepeatedFlag(rawArgs, 'output')[0]; const outputPath = OUTPUT_FLAG ? resolve(ROOT, OUTPUT_FLAG) : OUTPUT_PATH; @@ -98,7 +106,7 @@ async function readResultFile( filePath: string, sourcePath: string, experimentMetadata: Map -): Promise { +): Promise<{ result: EvalResult; traceInput: EvalResultTraceInput } | null> { const parsed: unknown = JSON.parse(await readFile(filePath, 'utf8')); const result = rawEvalResultSchema.safeParse(parsed); if (!result.success) { @@ -113,7 +121,7 @@ async function readResultFile( parsedResult.profile ?? experimentData?.experimentSuite; - return { + const evalResult: EvalResult = { experiment: parsedResult.experiment, experimentSuite, experimentDisplay: @@ -134,6 +142,23 @@ async function readResultFile( attempts: parsedResult.attempts, sourcePath, }; + + // rawEvalResultSchema is loose, so the transcript/toolCalls/agentReport the + // strict evalResultSchema drops are still here — that's what the trace + // adapter consumes. + const traceInput: EvalResultTraceInput = { + evalId: parsedResult.eval, + passed: parsedResult.passed === true, + transcript: parsedResult.transcript as TranscriptPart[] | undefined, + toolCalls: parsedResult.toolCalls as ToolCallRecord[] | undefined, + agentReport: parsedResult.agentReport as string | undefined, + skills: parsedResult.skills, + checks: parsedResult.checks, + experimentDisplay: parsedResult.experimentDisplay, + attempts: parsedResult.attempts, + }; + + return { result: evalResult, traceInput }; } function shouldIncludeExperiment(experiment: string): boolean { @@ -173,15 +198,30 @@ function shouldIncludeExperimentSuite( ); } -async function loadEvalResults(): Promise { +async function loadEvalResults(): Promise<{ + results: EvalResult[]; + traceInputs: Map; +}> { + const traceInputs = new Map(); if (!existsSync(RESULTS_DIR)) { - return []; + return { results: [], traceInputs }; } const experimentMetadata = await loadExperimentMetadata(); const results: EvalResult[] = []; const experiments = await readdir(RESULTS_DIR); + // Same evalId may run under several experiments (different agents/models). + // Prefer a run that actually recorded a transcript over one that didn't, so + // the trace viewer shows a real span tree rather than an empty no-skills run. + const upsertTrace = (input: EvalResultTraceInput) => { + const existing = traceInputs.get(input.evalId); + const hasTranscript = (input.transcript?.length ?? 0) > 0; + if (!existing || (hasTranscript && !existing.transcript?.length)) { + traceInputs.set(input.evalId, input); + } + }; + for (const experiment of experiments) { if (experiment.startsWith('.') || experiment.startsWith('_')) { continue; @@ -209,17 +249,18 @@ async function loadEvalResults(): Promise { continue; } - const result = await readResultFile( + const read = await readResultFile( entryPath, relativeEntryPath, experimentMetadata ); if ( - result && - shouldIncludeSuite(result.suite) && - shouldIncludeExperimentSuite(result.experimentSuite) + read && + shouldIncludeSuite(read.result.suite) && + shouldIncludeExperimentSuite(read.result.experimentSuite) ) { - results.push(result); + results.push(read.result); + upsertTrace(read.traceInput); } continue; } @@ -237,29 +278,48 @@ async function loadEvalResults(): Promise { continue; } - const result = await readResultFile( + const read = await readResultFile( summaryPath, `${relativeEntryPath}/summary.json`, experimentMetadata ); if ( - result && - shouldIncludeSuite(result.suite) && - shouldIncludeExperimentSuite(result.experimentSuite) + read && + shouldIncludeSuite(read.result.suite) && + shouldIncludeExperimentSuite(read.result.experimentSuite) ) { - results.push(result); + results.push(read.result); + upsertTrace(read.traceInput); } } } - return results.sort( + results.sort( (a, b) => a.experiment.localeCompare(b.experiment) || a.eval.localeCompare(b.eval) ); + return { results, traceInputs }; +} + +async function writeTraces( + traceInputs: Map +): Promise { + if (!WRITE_TRACES) return 0; + await mkdir(TRACES_DIR, { recursive: true }); + let written = 0; + for (const [evalId, input] of traceInputs) { + const data = evalResultToTraceSpans(input); + await writeFile( + join(TRACES_DIR, `${evalId}.json`), + `${JSON.stringify(data, null, 2)}\n` + ); + written += 1; + } + return written; } async function main() { - const newResults = await loadEvalResults(); + const { results: newResults, traceInputs } = await loadEvalResults(); const hasFilters = EXPERIMENT_FILTERS.length > 0 || EVAL_FILTERS.length > 0 || @@ -295,6 +355,13 @@ async function main() { `Exported ${results.length} result(s) to ${relative(ROOT, outputPath)} ` + `(${passed} pass, ${results.length - passed} fail)` ); + + const tracesWritten = await writeTraces(traceInputs); + if (WRITE_TRACES) { + console.log( + `Exported ${tracesWritten} trace(s) to ${relative(ROOT, TRACES_DIR)}` + ); + } } main().catch((error: unknown) => { diff --git a/apps/web/package.json b/apps/web/package.json index 6f96dbcf..35b57c17 100644 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -14,19 +14,28 @@ "preview": "vite preview" }, "dependencies": { + "@evilmartians/agent-prism-data": "^0.0.9", + "@evilmartians/agent-prism-types": "^0.0.9", "@fontsource-variable/inter": "^5.2.8", "@fontsource-variable/manrope": "5.2.8", "@fontsource-variable/source-code-pro": "5.3.0", + "@radix-ui/react-collapsible": "^1.1.12", + "@radix-ui/react-tabs": "^1.1.13", "@supabase-evals/core": "workspace:*", "@tailwindcss/vite": "^4.2.1", "@vercel/analytics": "^2.0.1", "class-variance-authority": "^0.7.1", + "classnames": "^2.5.1", "clsx": "^2.1.1", "lucide-react": "^1.14.0", "nuqs": "^2.9.2", "radix-ui": "^1.4.3", "react": "^19.2.4", "react-dom": "^19.2.4", + "react-json-pretty": "^2.2.0", + "react-markdown": "^10.1.0", + "react-resizable-panels": "^4.11.2", + "remark-gfm": "^4.0.1", "shadcn": "^4.6.0", "tailwind-merge": "^3.5.0", "tailwindcss": "^4.2.1", diff --git a/apps/web/src/App.tsx b/apps/web/src/App.tsx index 6d17d678..85219843 100644 --- a/apps/web/src/App.tsx +++ b/apps/web/src/App.tsx @@ -1,4 +1,5 @@ -import { useQueryStates } from "nuqs" +import { lazy, Suspense } from "react" +import { useQueryState, useQueryStates } from "nuqs" import { EvalOverviewCards } from "@/components/eval-overview-cards" import { PageContainer } from "@/components/page-container" @@ -8,7 +9,18 @@ import { SiteHeader } from "@/components/site-header" import { SiteHero } from "@/components/site-hero" import { TooltipProvider } from "@/components/ui/tooltip" import { sortedResults } from "@/lib/eval-results" -import { resultsQueryKeys, resultsQueryParsers } from "@/lib/url-state" +import { + resultsQueryKeys, + resultsQueryParsers, + traceEvalParser, + TRACE_EVAL_QUERY_KEY, +} from "@/lib/url-state" + +// Lazy so the AgentPrism component tree stays out of the main bundle; the +// trace panel is only mounted when a run is opened. +const TracePanel = lazy(() => + import("@/components/trace-panel").then((m) => ({ default: m.TracePanel })) +) export function App() { const [{ groupBy, experimentSuite }, setResultsQuery] = useQueryStates( @@ -18,6 +30,10 @@ export function App() { clearOnDefault: false, } ) + const [traceEval, setTraceEval] = useQueryState( + TRACE_EVAL_QUERY_KEY, + traceEvalParser + ) const suiteResults = sortedResults.filter( (result) => result.experimentSuite === experimentSuite ) @@ -55,6 +71,14 @@ export function App() { No result files found in the repo results directory. )} + {traceEval ? ( + + void setTraceEval(null)} + /> + + ) : null} ) diff --git a/apps/web/src/components/agent-prism/Avatar.tsx b/apps/web/src/components/agent-prism/Avatar.tsx new file mode 100644 index 00000000..4a80f81d --- /dev/null +++ b/apps/web/src/components/agent-prism/Avatar.tsx @@ -0,0 +1,145 @@ +import type { TraceSpanCategory } from "@evilmartians/agent-prism-types" +import cn from "classnames" +import { User } from "lucide-react" +import type { ComponentPropsWithRef, ReactElement } from "react" +import { useState } from "react" + +import type { ComponentSize } from "./shared" + +import { ROUNDED_CLASSES } from "./shared" + +export type AvatarSize = Extract< + ComponentSize, + "4" | "6" | "8" | "9" | "10" | "11" | "12" | "16" +> + +const sizeClasses: Record = { + "4": "size-4 text-xs", + "6": "size-6 text-xs", + "8": "size-8 text-xs", + "9": "size-9 text-sm", + "10": "size-10 text-base", + "11": "size-11 text-lg", + "12": "size-12 text-xl", + "16": "size-16 text-2xl", +} + +const iconSizeClasses: Record = { + "4": "size-3", + "6": "size-4", + "8": "size-6", + "9": "size-7", + "10": "size-8", + "11": "size-9", + "12": "size-10", + "16": "size-12", +} + +const bgColorClasses: Record = { + llm_call: "bg-agentprism-avatar-llm", + tool_execution: "bg-agentprism-avatar-tool", + agent_invocation: "bg-agentprism-avatar-agent", + chain_operation: "bg-agentprism-avatar-chain", + retrieval: "bg-agentprism-avatar-retrieval", + embedding: "bg-agentprism-avatar-embedding", + create_agent: "bg-agentprism-avatar-create-agent", + span: "bg-agentprism-avatar-span", + event: "bg-agentprism-avatar-event", + guardrail: "bg-agentprism-avatar-guardrail", + unknown: "bg-agentprism-avatar-unknown", +} + +export type AvatarProps = ComponentPropsWithRef<"div"> & { + /** + * The category of the span which avatar is associated with + */ + category: TraceSpanCategory + /** + * The image source for the avatar + */ + src?: string + /** + * The alt text for the avatar + */ + alt?: string + /** + * The size of the avatar + * @default "md" + */ + size?: AvatarSize + /** + * The border radius of the avatar + * @default "full" + */ + rounded?: "none" | "sm" | "md" | "lg" | "full" + /** + * Custom letter to display (will use first letter of alt if not provided) + */ + letter?: string + /** + * Optional className for additional styling + */ + className?: string +} + +export const Avatar = ({ + category, + src, + alt = "Avatar", + size = "10", + rounded = "full", + letter, + children, + className = "", + ...rest +}: AvatarProps): ReactElement => { + const [error, setError] = useState(false) + + const displayLetter = letter ? letter.charAt(0) : alt.charAt(0).toUpperCase() + + return ( +
+ {children ? ( + children + ) : error ? ( + + ) : ( + <> + {src ? ( + {alt} setError(true)} + /> + ) : ( +
+ {displayLetter} +
+ )} + + )} +
+ ) +} diff --git a/apps/web/src/components/agent-prism/Badge.tsx b/apps/web/src/components/agent-prism/Badge.tsx new file mode 100644 index 00000000..c2691ac9 --- /dev/null +++ b/apps/web/src/components/agent-prism/Badge.tsx @@ -0,0 +1,94 @@ +import cn from "classnames" +import type { ComponentPropsWithRef, ReactElement, ReactNode } from "react" + +import type { ComponentSize } from "./shared" + +type BadgeSize = Extract + +const sizeClasses: Record = { + "4": "px-1 gap-1 h-4", + "5": "px-1.5 gap-1 h-5", + "6": "px-2 gap-1.5 h-6", + "7": "px-2.5 gap-2 h-7", +} + +const textSizes: Record = { + "4": "text-xs leading-3", + "5": "text-xs", + "6": "text-sm", + "7": "text-sm", +} + +export type BadgeProps = ComponentPropsWithRef<"span"> & { + /** + * The content of the badge + */ + label: ReactNode + + /** + * The size of the badge + * @default "md" + */ + size?: BadgeSize + + /** + * Optional icon to display at the start of the badge + */ + iconStart?: ReactElement + + /** + * Optional icon to display at the end of the badge + */ + iconEnd?: ReactElement + + /** + * Optional className for additional styling + */ + className?: string + + /** + * Whether to render the badge without any default styles + * @default false + */ + unstyled?: boolean +} + +/** + * An unstyled badge component that displays a label with an optional icon + */ +export const Badge = ({ + label, + size = "4", + iconStart, + iconEnd, + className = "", + unstyled = false, + ...rest +}: BadgeProps): ReactElement => { + return ( + + {iconStart && {iconStart}} + + + {label} + + + {iconEnd && {iconEnd}} + + ) +} diff --git a/apps/web/src/components/agent-prism/BrandLogo.tsx b/apps/web/src/components/agent-prism/BrandLogo.tsx new file mode 100644 index 00000000..86b67cf7 --- /dev/null +++ b/apps/web/src/components/agent-prism/BrandLogo.tsx @@ -0,0 +1,102 @@ +import type { FC } from "react" + +const OpenAILogo: FC<{ className?: string }> = ({ className }) => ( + + + +) + +const AnthropicLogo: FC<{ className?: string }> = ({ className }) => ( + + + +) + +const GoogleLogo: FC<{ className?: string }> = ({ className }) => ( + + + + + + +) + +const MetaLogo: FC<{ className?: string }> = ({ className }) => ( + + + +) + +const MistralLogo: FC<{ className?: string }> = ({ className }) => ( + + + + + + + + + + + + + + + + + + + + + + + +) + +const PerplexityLogo: FC<{ className?: string }> = ({ className }) => ( + + + +) + +// Logo registry +const LOGO_REGISTRY = { + openai: OpenAILogo, + anthropic: AnthropicLogo, + google: GoogleLogo, + meta: MetaLogo, + mistral: MistralLogo, + perplexity: PerplexityLogo, +} as const + +type BrandType = keyof typeof LOGO_REGISTRY + +type BrandLogoProps = { + brand: BrandType | string + className?: string + fallback?: React.ReactNode +} + +export const BrandLogo: FC = ({ + brand, + className = "size-4", + fallback = null, +}) => { + const Logo = LOGO_REGISTRY[brand as BrandType] + + if (!Logo) return <>{fallback} + + return +} diff --git a/apps/web/src/components/agent-prism/Button.tsx b/apps/web/src/components/agent-prism/Button.tsx new file mode 100644 index 00000000..fd27714b --- /dev/null +++ b/apps/web/src/components/agent-prism/Button.tsx @@ -0,0 +1,123 @@ +import cn from "classnames" +import type { ComponentPropsWithRef, ReactElement } from "react" + +import type { ComponentSize } from "./shared" + +import { ROUNDED_CLASSES } from "./shared" + +type ButtonSize = Extract< + ComponentSize, + "6" | "7" | "8" | "9" | "10" | "11" | "12" | "16" +> + +type ButtonVariant = + | "brand" + | "primary" + | "outlined" + | "secondary" + | "ghost" + | "destructive" + | "success" + +const BASE_CLASSES = + "inline-flex items-center justify-center font-medium transition-all duration-200" + +const sizeClasses = { + "6": "h-6 px-2 gap-1 text-xs", + "7": "h-7 px-2 gap-1 text-xs", + "8": "h-8 px-2 gap-1 text-xs", + "9": "h-9 px-2.5 gap-2 text-sm", + "10": "h-10 px-4 gap-2 text-sm", + "11": "h-11 px-5 gap-3 text-base", + "12": "h-12 px-5 gap-2.5 text-base", + "16": "h-16 px-7 gap-3 text-lg", +} + +const variantClasses: Record = { + brand: "text-agentprism-brand-foreground bg-agentprism-brand", + primary: "text-agentprism-primary-foreground bg-agentprism-primary", + outlined: + "border border bg-transparent text-agentprism-foreground border-agentprism-foreground", + secondary: "bg-agentprism-secondary text-agentprism-secondary-foreground", + ghost: "bg-transparent text-agentprism-foreground", + destructive: "bg-agentprism-error text-agentprism-primary-foreground", + success: "bg-agentprism-success text-agentprism-primary-foreground", +} + +export type ButtonProps = ComponentPropsWithRef<"button"> & { + /** + * The size of the button + * @default "6" + */ + size?: ButtonSize + + /** + * The border radius of the button + * @default "md" + */ + rounded?: "none" | "sm" | "md" | "lg" | "full" + + /** + * The visual variant of the button + * @default "primary" + */ + variant?: ButtonVariant + + /** + * Makes the button full width + * @default false + */ + fullWidth?: boolean + + /** + * Optional icon to display at the start of the button + */ + iconStart?: ReactElement + + /** + * Optional icon to display at the end of the button + */ + iconEnd?: ReactElement +} + +export const Button = ({ + children, + size = "6", + rounded = "md", + variant = "primary", + fullWidth = false, + disabled = false, + iconStart, + iconEnd, + type = "button", + onClick, + className = "", + ...rest +}: ButtonProps) => { + const widthClass = fullWidth ? "w-full" : "" + const stateClasses = disabled + ? "cursor-not-allowed opacity-50" + : "hover:opacity-70" + + return ( + + ) +} diff --git a/apps/web/src/components/agent-prism/CollapseAndExpandControls.tsx b/apps/web/src/components/agent-prism/CollapseAndExpandControls.tsx new file mode 100644 index 00000000..6ff60070 --- /dev/null +++ b/apps/web/src/components/agent-prism/CollapseAndExpandControls.tsx @@ -0,0 +1,44 @@ +import { ChevronsDownUp, ChevronsUpDown } from "lucide-react" +import type { ComponentPropsWithRef } from "react" + +import { IconButton } from "./IconButton" + +export type SpanCardExpandAllButtonProps = ComponentPropsWithRef<"button"> & { + onExpandAll: () => void +} + +export type SpanCardCollapseAllButtonProps = ComponentPropsWithRef<"button"> & { + onCollapseAll: () => void +} + +export const ExpandAllButton = ({ + onExpandAll, + ...rest +}: SpanCardExpandAllButtonProps) => { + return ( + + + + ) +} + +export const CollapseAllButton = ({ + onCollapseAll, + ...rest +}: SpanCardCollapseAllButtonProps) => { + return ( + + + + ) +} diff --git a/apps/web/src/components/agent-prism/CollapsibleSection.tsx b/apps/web/src/components/agent-prism/CollapsibleSection.tsx new file mode 100644 index 00000000..f8f1ea72 --- /dev/null +++ b/apps/web/src/components/agent-prism/CollapsibleSection.tsx @@ -0,0 +1,124 @@ +import * as Collapsible from "@radix-ui/react-collapsible" +import cn from "classnames" +import { ChevronDown } from "lucide-react" +import * as React from "react" + +export interface CollapsibleSectionProps { + /** + * The title text displayed in the trigger button + */ + title: string + + /** + * The content to display on the right side of the title + */ + rightContent?: React.ReactNode + + /** + * The content to display when the section is expanded + */ + children: React.ReactNode + + /** + * Whether the section starts in an open state + * @default false + */ + defaultOpen?: boolean + + /** + * Optional className for the root container + */ + className?: string + + /** + * Optional className for the trigger button + */ + triggerClassName?: string + + /** + * Optional className for the content area + */ + contentClassName?: string + + /** + * Optional callback fired when the section is expanded or collapsed + */ + onOpenChange?: (open: boolean) => void +} + +export const CollapsibleSection: React.FC = ({ + title, + rightContent, + children, + defaultOpen = false, + className = "", + triggerClassName = "", + contentClassName = "", + onOpenChange, +}) => { + const [open, setOpen] = React.useState(defaultOpen) + + const handleOpenChange = React.useCallback( + (open: boolean): void => { + setOpen(open) + onOpenChange?.(open) + }, + [onOpenChange] + ) + + const handleKeyDown = React.useCallback( + (e: React.KeyboardEvent): void => { + if (e.key === "Enter" || e.key === " ") { + e.preventDefault() + handleOpenChange(!open) + } + }, + [handleOpenChange, open] + ) + + return ( + + +
+
+ + + {title} + +
+ +
{rightContent}
+
+
+ + + {children} + +
+ ) +} diff --git a/apps/web/src/components/agent-prism/CopyButton.tsx b/apps/web/src/components/agent-prism/CopyButton.tsx new file mode 100644 index 00000000..3e02a036 --- /dev/null +++ b/apps/web/src/components/agent-prism/CopyButton.tsx @@ -0,0 +1,63 @@ +import { Check, Copy, X } from "lucide-react" +import { useState } from "react" + +import { IconButton } from "./IconButton" + +type CopyButtonProps = { + label: string + content: string +} + +type CopyState = "idle" | "success" | "error" + +export const CopyButton = ({ label, content }: CopyButtonProps) => { + const [copyState, setCopyState] = useState("idle") + + const onClick = async () => { + try { + if (!navigator.clipboard) { + throw new Error("Clipboard API not supported") + } + + await navigator.clipboard.writeText(content) + setCopyState("success") + setTimeout(() => setCopyState("idle"), 2000) + } catch { + setCopyState("error") + setTimeout(() => setCopyState("idle"), 2000) + } + } + + const getIcon = () => { + switch (copyState) { + case "success": + return + case "error": + return + default: + return + } + } + + const getAriaLabel = () => { + switch (copyState) { + case "success": + return `${label} Copied` + case "error": + return `Failed to copy ${label}` + default: + return `Copy ${label}` + } + } + + return ( + + {getIcon()} + + ) +} diff --git a/apps/web/src/components/agent-prism/DetailsView/DetailsView.tsx b/apps/web/src/components/agent-prism/DetailsView/DetailsView.tsx new file mode 100644 index 00000000..76366a6f --- /dev/null +++ b/apps/web/src/components/agent-prism/DetailsView/DetailsView.tsx @@ -0,0 +1,141 @@ +import type { TraceSpan } from "@evilmartians/agent-prism-types" +import cn from "classnames" +import { ArrowRightLeft, SquareTerminal, Tags } from "lucide-react" +import type { ReactElement, ReactNode } from "react" +import { useState } from "react" + +import type { AvatarProps } from "../Avatar" +import { TabSelector } from "../TabSelector" +import type { TabItem } from "../Tabs" +import { DetailsViewAttributesTab } from "./DetailsViewAttributesTab" +import { DetailsViewHeader } from "./DetailsViewHeader" +import { DetailsViewInputOutputTab } from "./DetailsViewInputOutputTab" +import { DetailsViewRawDataTab } from "./DetailsViewRawDataTab" + +type DetailsViewTab = "input-output" | "attributes" | "raw" + +export interface DetailsViewProps { + /** + * The span data to display in the details view + */ + data: TraceSpan + + /** + * Optional avatar configuration for the header + */ + avatar?: AvatarProps + + /** + * The initially selected tab + */ + defaultTab?: DetailsViewTab + + /** + * Optional className for the root container + */ + className?: string + + /** + * Configuration for the copy button functionality + */ + copyButton?: { + isEnabled?: boolean + onCopy?: (data: TraceSpan) => void + } + + /** + * Custom header actions to render + * Can be a ReactNode or a render function that receives the data + */ + headerActions?: ReactNode | ((data: TraceSpan) => ReactNode) + + /** + * Optional custom header component to replace the default + */ + customHeader?: ReactNode | ((props: { data: TraceSpan }) => ReactNode) + + /** + * Callback fired when the active tab changes + */ + onTabChange?: (tabValue: DetailsViewTab) => void +} + +const TAB_ITEMS: TabItem[] = [ + { + value: "input-output", + label: "In/Out", + icon: , + }, + { + value: "attributes", + label: "Attributes", + icon: , + }, + { + value: "raw", + label: "RAW", + icon: , + }, +] + +export const DetailsView = ({ + data, + avatar, + defaultTab = "input-output", + className, + copyButton, + headerActions, + customHeader, + onTabChange, +}: DetailsViewProps): ReactElement => { + const [tab, setTab] = useState(defaultTab) + + const handleTabChange = (tabValue: DetailsViewTab) => { + setTab(tabValue) + onTabChange?.(tabValue) + } + + const resolvedHeaderActions = + typeof headerActions === "function" ? headerActions(data) : headerActions + + const headerContent = customHeader ? ( + typeof customHeader === "function" ? ( + customHeader({ data }) + ) : ( + customHeader + ) + ) : ( + + ) + + return ( +
+
{headerContent}
+
+ +
+ +
+ {tab === "input-output" && } + {tab === "attributes" && } + {tab === "raw" && } +
+
+ ) +} diff --git a/apps/web/src/components/agent-prism/DetailsView/DetailsViewAttributesTab.tsx b/apps/web/src/components/agent-prism/DetailsView/DetailsViewAttributesTab.tsx new file mode 100644 index 00000000..00ebdd86 --- /dev/null +++ b/apps/web/src/components/agent-prism/DetailsView/DetailsViewAttributesTab.tsx @@ -0,0 +1,123 @@ +import type { TraceSpan } from "@evilmartians/agent-prism-types" +import { type ReactElement, useState } from "react" +import { CollapsibleSection } from "../CollapsibleSection" +import { TabSelector } from "../TabSelector" +import type { TabItem } from "../Tabs" +import { + DetailsViewContentViewer, + type DetailsViewContentViewMode, +} from "./DetailsViewContentViewer" + +interface AttributesTabProps { + data: TraceSpan +} + +const TAB_ITEMS: TabItem[] = [ + { value: "json", label: "JSON" }, + { value: "plain", label: "Plain" }, +] + +export const DetailsViewAttributesTab = ({ + data, +}: AttributesTabProps): ReactElement => { + if (!data.attributes || data.attributes.length === 0) { + return ( +
+

+ No attributes available for this span. +

+
+ ) + } + + return ( +
+ {data.attributes.map((attribute, index) => { + const stringValue = attribute.value.stringValue + const simpleValue = + stringValue || + attribute.value.intValue?.toString() || + attribute.value.boolValue?.toString() || + "N/A" + + let parsedJson: string | null = null + if (typeof stringValue === "string") { + try { + parsedJson = JSON.parse(stringValue) + } catch { + parsedJson = null + } + } + + const isComplex = parsedJson !== null + + if (isComplex && parsedJson && stringValue) { + return ( + + ) + } + + return ( +
+
+ {attribute.key} +
+
+ {simpleValue} +
+
+ ) + })} +
+ ) +} + +interface AttributeSectionProps { + attributeKey: string + content: string + parsedContent: string + id: string +} + +const AttributeSection = ({ + attributeKey, + content, + parsedContent, + id, +}: AttributeSectionProps): ReactElement => { + const [tab, setTab] = useState("json") + + return ( + + items={TAB_ITEMS} + defaultValue="json" + value={tab} + onValueChange={setTab} + theme="pill" + onClick={(event) => event.stopPropagation()} + /> + } + > + + + ) +} diff --git a/apps/web/src/components/agent-prism/DetailsView/DetailsViewContentViewer.tsx b/apps/web/src/components/agent-prism/DetailsView/DetailsViewContentViewer.tsx new file mode 100644 index 00000000..b55dd99a --- /dev/null +++ b/apps/web/src/components/agent-prism/DetailsView/DetailsViewContentViewer.tsx @@ -0,0 +1,70 @@ +import type { ReactElement } from "react" + +import { CopyButton } from "../CopyButton" +import { DetailsViewJsonOutput } from "./DetailsViewJsonOutput" +import { DetailsViewMarkdown } from "./DetailsViewMarkdown" +import { DetailsViewPrettyOutput } from "./DetailsViewPrettyOutput" + +export type DetailsViewContentViewMode = "json" | "plain" + +export interface DetailsViewContentViewerProps { + content: string + parsedContent: unknown + mode: DetailsViewContentViewMode + label: string + id: string + className?: string + /** + * Render plain-mode content as markdown instead of a raw `
` dump.
+   * Only pass this for genuinely LLM-authored prose (assistant/user/system
+   * text) — tool call args/results are structured data, not markdown, and
+   * would render with spurious formatting (stray `_`/`*` in identifiers,
+   * error messages, etc.) if forced through a markdown parser.
+   */
+  renderMarkdown?: boolean
+}
+
+export const DetailsViewContentViewer = ({
+  content,
+  parsedContent,
+  mode,
+  label,
+  id,
+  className = "",
+  renderMarkdown = false,
+}: DetailsViewContentViewerProps): ReactElement => {
+  if (!content) {
+    return (
+      

+ No data available +

+ ) + } + + return ( +
+
+ +
+ {mode === "json" && parsedContent ? ( + + ) : renderMarkdown ? ( +
+ +
+ ) : parsedContent ? ( +
+ +
+ ) : ( +
+
+            {content}
+          
+
+ )} +
+ ) +} diff --git a/apps/web/src/components/agent-prism/DetailsView/DetailsViewHeader.tsx b/apps/web/src/components/agent-prism/DetailsView/DetailsViewHeader.tsx new file mode 100644 index 00000000..c3c6ce7e --- /dev/null +++ b/apps/web/src/components/agent-prism/DetailsView/DetailsViewHeader.tsx @@ -0,0 +1,99 @@ +import { formatDuration, getDurationMs } from "@evilmartians/agent-prism-data" +import type { TraceSpan } from "@evilmartians/agent-prism-types" +import { Check, Copy } from "lucide-react" +import type { ReactNode } from "react" +import { useState } from "react" + +import type { AvatarProps } from "../Avatar" + +import { Avatar } from "../Avatar" +import { IconButton } from "../IconButton" +import { PriceBadge } from "../PriceBadge" +import { SpanBadge } from "../SpanBadge" +import { SpanStatus } from "../SpanStatus" +import { TimestampBadge } from "../TimestampBadge" +import { TokensBadge } from "../TokensBadge" + +export interface DetailsViewHeaderProps { + data: TraceSpan + avatar?: AvatarProps + copyButton?: { + isEnabled?: boolean + onCopy?: (data: TraceSpan) => void + } + /** + * Custom actions to render in the header + */ + actions?: ReactNode + /** + * Optional className for the header container + */ + className?: string +} + +export const DetailsViewHeader = ({ + data, + avatar, + copyButton, + actions, + className, +}: DetailsViewHeaderProps) => { + const [hasCopied, setHasCopied] = useState(false) + const durationMs = getDurationMs(data) + + const handleCopy = () => { + if (copyButton?.onCopy) { + copyButton.onCopy(data) + setHasCopied(true) + setTimeout(() => setHasCopied(false), 2000) + } + } + + return ( +
+ {avatar && } + + + {data.title} + + +
+ +
+ + {copyButton && ( + + {hasCopied ? ( + + ) : ( + + )} + + )} + + + + {typeof data.tokensCount === "number" && ( + + )} + + {typeof data.cost === "number" && } + + + LATENCY: {formatDuration(durationMs)} + + + {typeof data.startTime === "number" && ( + + )} + + {actions} +
+ ) +} diff --git a/apps/web/src/components/agent-prism/DetailsView/DetailsViewHeaderActions.tsx b/apps/web/src/components/agent-prism/DetailsView/DetailsViewHeaderActions.tsx new file mode 100644 index 00000000..9d9841fc --- /dev/null +++ b/apps/web/src/components/agent-prism/DetailsView/DetailsViewHeaderActions.tsx @@ -0,0 +1,21 @@ +import type { ReactNode } from "react" + +export interface DetailsViewHeaderActionsProps { + /** + * Custom actions to render in the header + */ + children?: ReactNode + /** + * Optional className for the actions container + */ + className?: string +} + +export const DetailsViewHeaderActions = ({ + children, + className = "flex flex-wrap items-center gap-2", +}: DetailsViewHeaderActionsProps) => { + if (!children) return null + + return
{children}
+} diff --git a/apps/web/src/components/agent-prism/DetailsView/DetailsViewInputOutputTab.tsx b/apps/web/src/components/agent-prism/DetailsView/DetailsViewInputOutputTab.tsx new file mode 100644 index 00000000..814c296b --- /dev/null +++ b/apps/web/src/components/agent-prism/DetailsView/DetailsViewInputOutputTab.tsx @@ -0,0 +1,135 @@ +import type { TraceSpan } from "@evilmartians/agent-prism-types" +import type { ReactElement } from "react" + +import { useState } from "react" +import { CollapsibleSection } from "../CollapsibleSection" +import { deepParseJson } from "../shared" +import { TabSelector } from "../TabSelector" +import type { TabItem } from "../Tabs" +import { + DetailsViewContentViewer, + type DetailsViewContentViewMode, +} from "./DetailsViewContentViewer" + +interface DetailsViewInputOutputTabProps { + data: TraceSpan +} + +type IOSection = "Input" | "Output" + +export const DetailsViewInputOutputTab = ({ + data, +}: DetailsViewInputOutputTabProps): ReactElement => { + const hasInput = Boolean(data.input) + const hasOutput = Boolean(data.output) + + if (!hasInput && !hasOutput) { + return ( +
+

+ No input or output data available for this span +

+
+ ) + } + + let parsedInput: unknown = null + let parsedOutput: unknown = null + + if (typeof data.input === "string") { + try { + parsedInput = deepParseJson(JSON.parse(data.input)) + } catch { + parsedInput = null + } + } + + if (typeof data.output === "string") { + try { + parsedOutput = deepParseJson(JSON.parse(data.output)) + } catch { + parsedOutput = null + } + } + + // llm_call/event/agent_invocation spans (Assistant/System/User/check + // messages, and the root's final agentReport) carry LLM-authored prose — + // tool_execution content is structured data (args, results) that + // shouldn't be markdown-parsed (stray `_`/`*` in identifiers, SQL, etc.). + const renderMarkdown = + data.type === "llm_call" || + data.type === "event" || + data.type === "agent_invocation" + + return ( +
+ {typeof data.input === "string" && ( + + )} + {typeof data.output === "string" && ( + + )} +
+ ) +} + +interface IOSectionProps { + section: IOSection + content: string + parsedContent: unknown + renderMarkdown: boolean +} + +const IOSection = ({ + section, + content, + parsedContent, + renderMarkdown, +}: IOSectionProps): ReactElement => { + // "Plain" renders real line breaks (DetailsViewPrettyOutput) — the right + // default for tool call args/results, which are often multi-line shell + // commands or command output that read as an unreadable single line of + // escaped `\n`s under strict JSON. "JSON" stays available for exact syntax. + const [tab, setTab] = useState("plain") + + const tabItems: TabItem[] = [ + { value: "json", label: "JSON", disabled: !parsedContent }, + { value: "plain", label: "Plain" }, + ] + + return ( + + items={tabItems} + defaultValue="plain" + value={tab} + onValueChange={setTab} + theme="pill" + onClick={(event) => event.stopPropagation()} + /> + } + > + + + ) +} diff --git a/apps/web/src/components/agent-prism/DetailsView/DetailsViewJsonOutput.tsx b/apps/web/src/components/agent-prism/DetailsView/DetailsViewJsonOutput.tsx new file mode 100644 index 00000000..71071f49 --- /dev/null +++ b/apps/web/src/components/agent-prism/DetailsView/DetailsViewJsonOutput.tsx @@ -0,0 +1,32 @@ +import type { FC } from "react" +import JSONPretty from "react-json-pretty" + +import { agentPrismPrefix } from "../theme" + +export interface JsonViewerProps { + content: unknown + id: string + className?: string +} + +export const DetailsViewJsonOutput: FC = ({ + content, + id, + className = "", +}) => { + return ( + + ) +} diff --git a/apps/web/src/components/agent-prism/DetailsView/DetailsViewMarkdown.tsx b/apps/web/src/components/agent-prism/DetailsView/DetailsViewMarkdown.tsx new file mode 100644 index 00000000..d5559a20 --- /dev/null +++ b/apps/web/src/components/agent-prism/DetailsView/DetailsViewMarkdown.tsx @@ -0,0 +1,102 @@ +import type { ReactElement } from "react" +import ReactMarkdown, { type Components } from "react-markdown" +import remarkGfm from "remark-gfm" + +// Hand-styled (no typography plugin, matching the rest of this ported +// component set) — every element maps to the same agentprism-* tokens the +// surrounding trace viewer uses, so a rendered assistant response looks like +// part of the UI, not a foreign markdown blob. +const components: Components = { + h1: ({ children }) => ( +

+ {children} +

+ ), + h2: ({ children }) => ( +

+ {children} +

+ ), + h3: ({ children }) => ( +

+ {children} +

+ ), + p: ({ children }) => ( +

+ {children} +

+ ), + a: ({ children, href }) => ( + + {children} + + ), + ul: ({ children }) => ( +
    + {children} +
+ ), + ol: ({ children }) => ( +
    + {children} +
+ ), + li: ({ children }) =>
  • {children}
  • , + strong: ({ children }) => ( + + {children} + + ), + code: ({ children }) => ( + + {children} + + ), + pre: ({ children }) => ( +
    +      {children}
    +    
    + ), + blockquote: ({ children }) => ( +
    + {children} +
    + ), + hr: () =>
    , + table: ({ children }) => ( +
    + {children}
    +
    + ), + thead: ({ children }) => ( + {children} + ), + th: ({ children }) => ( + + {children} + + ), + td: ({ children }) => ( + + {children} + + ), +} + +export const DetailsViewMarkdown = ({ + content, +}: { + content: string +}): ReactElement => ( +
    + + {content} + +
    +) diff --git a/apps/web/src/components/agent-prism/DetailsView/DetailsViewPrettyOutput.tsx b/apps/web/src/components/agent-prism/DetailsView/DetailsViewPrettyOutput.tsx new file mode 100644 index 00000000..56b4fd32 --- /dev/null +++ b/apps/web/src/components/agent-prism/DetailsView/DetailsViewPrettyOutput.tsx @@ -0,0 +1,75 @@ +import type { ReactElement, ReactNode } from "react" + +/** + * Human-readable render of parsed JSON content — the counterpart to + * DetailsViewJsonOutput's exact-JSON view. Strings render with their real + * line breaks (tool call `command`/`stdout` fields are often multi-line + * shell scripts that are unreadable as an escaped `\n`-riddled JSON string), + * at the cost of no longer being valid JSON syntax itself. + */ +function renderValue(value: unknown): ReactNode { + if (value === null || value === undefined) { + return + } + + if (typeof value === "string") { + return ( +
    +        {value}
    +      
    + ) + } + + if (typeof value === "number" || typeof value === "boolean") { + return ( + + {String(value)} + + ) + } + + if (Array.isArray(value)) { + if (value.length === 0) { + return [] + } + + return ( +
    + {value.map((item, index) => ( +
    + {renderValue(item)} +
    + ))} +
    + ) + } + + if (typeof value === "object") { + const entries = Object.entries(value as Record) + + if (entries.length === 0) { + return {"{}"} + } + + return ( +
    + {entries.map(([key, val]) => ( +
    +
    + {key} +
    +
    {renderValue(val)}
    +
    + ))} +
    + ) + } + + return {String(value)} +} + +export const DetailsViewPrettyOutput = ({ + content, +}: { + content: unknown +}): ReactElement =>
    {renderValue(content)}
    diff --git a/apps/web/src/components/agent-prism/DetailsView/DetailsViewRawDataTab.tsx b/apps/web/src/components/agent-prism/DetailsView/DetailsViewRawDataTab.tsx new file mode 100644 index 00000000..a0dd847d --- /dev/null +++ b/apps/web/src/components/agent-prism/DetailsView/DetailsViewRawDataTab.tsx @@ -0,0 +1,31 @@ +import type { TraceSpan } from "@evilmartians/agent-prism-types" + +import type { ReactElement } from "react" + +import { CopyButton } from "../CopyButton" +import { DetailsViewJsonOutput } from "./DetailsViewJsonOutput" + +interface RawDataTabProps { + data: TraceSpan +} + +export const DetailsViewRawDataTab = ({ + data, +}: RawDataTabProps): ReactElement => ( +
    +
    +
    +
    + +
    +
    + +
    + +
    +
    +
    +) diff --git a/apps/web/src/components/agent-prism/IconButton.tsx b/apps/web/src/components/agent-prism/IconButton.tsx new file mode 100644 index 00000000..41958bd7 --- /dev/null +++ b/apps/web/src/components/agent-prism/IconButton.tsx @@ -0,0 +1,74 @@ +import cn from "classnames" +import type { ComponentPropsWithRef } from "react" + +import type { ComponentSize } from "./shared" + +type IconButtonSize = Extract< + ComponentSize, + "6" | "7" | "8" | "9" | "10" | "11" | "12" | "16" +> +type IconButtonVariant = "default" | "ghost" + +export type IconButtonProps = ComponentPropsWithRef<"button"> & { + /** + * The size of the icon button + */ + size?: IconButtonSize + + /** + * The visual variant of the icon button + */ + variant?: IconButtonVariant + + /** + * Accessible label for screen readers + * Required for accessibility compliance + */ + "aria-label": string +} + +const sizeClasses: Record = { + "6": "h-6 min-h-6", + "7": "h-7 min-h-7", + "8": "h-8 min-h-8", + "9": "h-9 min-h-9", + "10": "h-10 min-h-10", + "11": "h-11 min-h-11", + "12": "h-12 min-h-12", + "16": "h-16 min-h-16", +} + +const variantClasses: Record = { + default: "border border-agentprism-border bg-transparent", + ghost: "bg-transparent", +} + +// TODO: Remake to call Icon component directly instead of passing children +export const IconButton = ({ + children, + className, + size = "6", + variant = "default", + type = "button", + "aria-label": ariaLabel, + ...rest +}: IconButtonProps) => { + return ( + + ) +} diff --git a/apps/web/src/components/agent-prism/PriceBadge.tsx b/apps/web/src/components/agent-prism/PriceBadge.tsx new file mode 100644 index 00000000..79b77a22 --- /dev/null +++ b/apps/web/src/components/agent-prism/PriceBadge.tsx @@ -0,0 +1,14 @@ +import type { ComponentPropsWithRef } from "react" + +import type { BadgeProps } from "./Badge" + +import { Badge } from "./Badge" + +export type PriceBadgeProps = ComponentPropsWithRef<"span"> & { + cost: number + size?: BadgeProps["size"] +} + +export const PriceBadge = ({ cost, size, ...rest }: PriceBadgeProps) => { + return +} diff --git a/apps/web/src/components/agent-prism/SearchInput.tsx b/apps/web/src/components/agent-prism/SearchInput.tsx new file mode 100644 index 00000000..8a5b95ea --- /dev/null +++ b/apps/web/src/components/agent-prism/SearchInput.tsx @@ -0,0 +1,17 @@ +import { Search } from "lucide-react" + +import { TextInput, type TextInputProps } from "./TextInput" + +/** + * A simple wrapper around the TextInput component. + * It adds a search icon and a placeholder. + */ +export const SearchInput = ({ ...props }: TextInputProps) => { + return ( + } + placeholder="Filter..." + {...props} + /> + ) +} diff --git a/apps/web/src/components/agent-prism/SpanBadge.tsx b/apps/web/src/components/agent-prism/SpanBadge.tsx new file mode 100644 index 00000000..4528d59d --- /dev/null +++ b/apps/web/src/components/agent-prism/SpanBadge.tsx @@ -0,0 +1,54 @@ +import type { TraceSpanCategory } from "@evilmartians/agent-prism-types" + +import cn from "classnames" + +import { Badge, type BadgeProps } from "./Badge" +import { getSpanCategoryIcon, getSpanCategoryLabel } from "./shared" + +export interface SpanBadgeProps extends Omit< + BadgeProps, + "label" | "iconStart" | "iconEnd" +> { + category: TraceSpanCategory +} + +const badgeClasses: Record = { + llm_call: "bg-agentprism-badge-llm text-agentprism-badge-llm-foreground", + tool_execution: + "bg-agentprism-badge-tool text-agentprism-badge-tool-foreground", + chain_operation: + "bg-agentprism-badge-chain text-agentprism-badge-chain-foreground", + retrieval: + "bg-agentprism-badge-retrieval text-agentprism-badge-retrieval-foreground", + embedding: + "bg-agentprism-badge-embedding text-agentprism-badge-embedding-foreground", + guardrail: + "bg-agentprism-badge-guardrail text-agentprism-badge-guardrail-foreground", + agent_invocation: + "bg-agentprism-badge-agent text-agentprism-badge-agent-foreground", + create_agent: + "bg-agentprism-badge-create-agent text-agentprism-badge-create-agent-foreground", + span: "bg-agentprism-badge-span text-agentprism-badge-span-foreground", + event: "bg-agentprism-badge-event text-agentprism-badge-event-foreground", + unknown: + "bg-agentprism-badge-unknown text-agentprism-badge-unknown-foreground", +} + +export const SpanBadge = ({ + category, + className, + ...props +}: SpanBadgeProps) => { + const Icon = getSpanCategoryIcon(category) + const label = getSpanCategoryLabel(category) + + return ( + } + {...props} + label={label} + unstyled + /> + ) +} diff --git a/apps/web/src/components/agent-prism/SpanCard/SpanCard.tsx b/apps/web/src/components/agent-prism/SpanCard/SpanCard.tsx new file mode 100644 index 00000000..97f894a4 --- /dev/null +++ b/apps/web/src/components/agent-prism/SpanCard/SpanCard.tsx @@ -0,0 +1,419 @@ +import { formatDuration, getTimelineData } from "@evilmartians/agent-prism-data" +import type { TraceSpan } from "@evilmartians/agent-prism-types" +import * as Collapsible from "@radix-ui/react-collapsible" +import cn from "classnames" +import type { FC, KeyboardEvent, MouseEvent } from "react" +import { useCallback } from "react" + +import type { AvatarProps } from "../Avatar" +import { Avatar } from "../Avatar" +import { BrandLogo } from "../BrandLogo" +import { SpanStatus } from "../SpanStatus" +import { SpanCardBadges } from "./SpanCardBadges" +import type { SpanCardConnectorType } from "./SpanCardConnector" +import { SpanCardConnector } from "./SpanCardConnector" +import { SpanCardTimeline } from "./SpanCardTimeline" +import { SpanCardToggle } from "./SpanCardToggle" + +const LAYOUT_CONSTANTS = { + CONNECTOR_WIDTH: 20, +} as const + +type ExpandButtonPlacement = "inside" | "outside" + +export type SpanCardViewOptions = { + withStatus?: boolean + expandButton?: ExpandButtonPlacement +} + +const DEFAULT_VIEW_OPTIONS: Required = { + withStatus: true, + expandButton: "inside", +} + +interface SpanCardProps { + data: TraceSpan + level?: number + selectedSpan?: TraceSpan + avatar?: AvatarProps + onSpanSelect?: (span: TraceSpan) => void + minStart: number + maxEnd: number + isLastChild: boolean + prevLevelConnectors?: SpanCardConnectorType[] + expandedSpansIds: string[] + onExpandSpansIdsChange: (ids: string[]) => void + viewOptions?: SpanCardViewOptions +} + +interface SpanCardState { + isExpanded: boolean + hasChildren: boolean + isSelected: boolean +} + +const getGridTemplateColumns = ({ + connectorsColumnWidth, + expandButton, +}: { + connectorsColumnWidth: number + expandButton: ExpandButtonPlacement +}) => { + if (expandButton === "inside") { + return `${connectorsColumnWidth}px 1fr` + } + + return `${connectorsColumnWidth}px 1fr ${LAYOUT_CONSTANTS.CONNECTOR_WIDTH}px` +} + +const getConnectorsLayout = ({ + level, + hasExpandButton, + isLastChild, + prevConnectors, + expandButton, +}: { + hasExpandButton: boolean + isLastChild: boolean + level: number + prevConnectors: SpanCardConnectorType[] + expandButton: ExpandButtonPlacement +}): { + connectors: SpanCardConnectorType[] + connectorsColumnWidth: number +} => { + const connectors: SpanCardConnectorType[] = [] + + if (level === 0) { + return { + connectors: expandButton === "inside" ? [] : ["vertical"], + connectorsColumnWidth: 20, + } + } + + for (let i = 0; i < level - 1; i++) { + connectors.push("vertical") + } + + if (!isLastChild) { + connectors.push("t-right") + } + + if (isLastChild) { + connectors.push("corner-top-right") + } + + let connectorsColumnWidth = + connectors.length * LAYOUT_CONSTANTS.CONNECTOR_WIDTH + + if (hasExpandButton) { + connectorsColumnWidth += LAYOUT_CONSTANTS.CONNECTOR_WIDTH + } + + for (let i = 0; i < prevConnectors.length; i++) { + if ( + prevConnectors[i] === "empty" || + prevConnectors[i] === "corner-top-right" + ) { + connectors[i] = "empty" + } + } + + return { + connectors, + connectorsColumnWidth, + } +} + +const useSpanCardEventHandlers = ( + data: TraceSpan, + onSpanSelect?: (span: TraceSpan) => void +) => { + const handleCardClick = useCallback((): void => { + onSpanSelect?.(data) + }, [data, onSpanSelect]) + + const handleKeyDown = useCallback( + (e: KeyboardEvent): void => { + if (e.key === "Enter" || e.key === " ") { + e.preventDefault() + handleCardClick() + } + }, + [handleCardClick] + ) + + const handleToggleClick = useCallback( + (e: MouseEvent | KeyboardEvent): void => { + e.stopPropagation() + }, + [] + ) + + return { + handleCardClick, + handleKeyDown, + handleToggleClick, + } +} + +const SpanCardChildren: FC<{ + data: TraceSpan + level: number + selectedSpan?: TraceSpan + onSpanSelect?: (span: TraceSpan) => void + minStart: number + maxEnd: number + prevLevelConnectors: SpanCardConnectorType[] + expandedSpansIds: string[] + onExpandSpansIdsChange: (ids: string[]) => void + viewOptions?: SpanCardViewOptions +}> = ({ + data, + level, + selectedSpan, + onSpanSelect, + minStart, + maxEnd, + prevLevelConnectors, + expandedSpansIds, + onExpandSpansIdsChange, + viewOptions = DEFAULT_VIEW_OPTIONS, +}) => { + if (!data.children?.length) return null + + return ( +
    + +
      + {data.children.map((child, idx) => { + const brand = child.metadata?.brand as { type: string } | undefined + + return ( + , + size: "4", + rounded: "sm", + category: child.type, + } + : undefined + } + /> + ) + })} +
    +
    +
    + ) +} + +export const SpanCard: FC = ({ + data, + level = 0, + selectedSpan, + onSpanSelect, + viewOptions = DEFAULT_VIEW_OPTIONS, + avatar, + minStart, + maxEnd, + isLastChild, + prevLevelConnectors = [], + expandedSpansIds, + onExpandSpansIdsChange, +}) => { + const isExpanded = expandedSpansIds.includes(data.id) + + const withStatus = viewOptions.withStatus ?? DEFAULT_VIEW_OPTIONS.withStatus + const expandButton = + viewOptions.expandButton || DEFAULT_VIEW_OPTIONS.expandButton + + const handleToggleClick = useCallback( + (expanded: boolean) => { + const alreadyExpanded = expandedSpansIds.includes(data.id) + + if (alreadyExpanded && !expanded) { + onExpandSpansIdsChange(expandedSpansIds.filter((id) => id !== data.id)) + } + + if (!alreadyExpanded && expanded) { + onExpandSpansIdsChange([...expandedSpansIds, data.id]) + } + }, + [expandedSpansIds, data.id, onExpandSpansIdsChange] + ) + + const state: SpanCardState = { + isExpanded, + hasChildren: Boolean(data.children?.length), + isSelected: selectedSpan?.id === data.id, + } + + const eventHandlers = useSpanCardEventHandlers(data, onSpanSelect) + + const { durationMs } = getTimelineData({ + spanCard: data, + minStart, + maxEnd, + }) + + const hasExpandButtonAsFirstChild = + expandButton === "inside" && state.hasChildren + + const { connectors, connectorsColumnWidth } = getConnectorsLayout({ + level, + hasExpandButton: hasExpandButtonAsFirstChild, + isLastChild, + prevConnectors: prevLevelConnectors, + expandButton, + }) + + const gridTemplateColumns = getGridTemplateColumns({ + connectorsColumnWidth, + expandButton, + }) + + return ( +
  • + +
    +
    + {connectors.map((connector, idx) => ( + + ))} + + {hasExpandButtonAsFirstChild && ( +
    + + + {state.isExpanded && } +
    + )} +
    +
    +
    + {avatar && } + +

    + {data.title} +

    + + +
    + +
    + {expandButton === "outside" && withStatus && ( +
    + +
    + )} + + + +
    + + {formatDuration(durationMs)} + + + {expandButton === "inside" && withStatus && ( +
    + +
    + )} +
    +
    +
    + + {expandButton === "outside" && + (state.hasChildren ? ( + + ) : ( +
    + ))} +
    + + + +
  • + ) +} diff --git a/apps/web/src/components/agent-prism/SpanCard/SpanCardBadges.tsx b/apps/web/src/components/agent-prism/SpanCard/SpanCardBadges.tsx new file mode 100644 index 00000000..64fde294 --- /dev/null +++ b/apps/web/src/components/agent-prism/SpanCard/SpanCardBadges.tsx @@ -0,0 +1,23 @@ +import type { TraceSpan } from "@evilmartians/agent-prism-types" + +import { PriceBadge } from "../PriceBadge" +import { SpanBadge } from "../SpanBadge" +import { TokensBadge } from "../TokensBadge" + +interface SpanCardBagdesProps { + data: TraceSpan +} + +export const SpanCardBadges = ({ data }: SpanCardBagdesProps) => { + return ( +
    + + + {typeof data.tokensCount === "number" && ( + + )} + + {typeof data.cost === "number" && } +
    + ) +} diff --git a/apps/web/src/components/agent-prism/SpanCard/SpanCardConnector.tsx b/apps/web/src/components/agent-prism/SpanCard/SpanCardConnector.tsx new file mode 100644 index 00000000..78bab637 --- /dev/null +++ b/apps/web/src/components/agent-prism/SpanCard/SpanCardConnector.tsx @@ -0,0 +1,36 @@ +export type SpanCardConnectorType = + | "horizontal" + | "vertical" + | "t-right" + | "corner-top-right" + | "empty" + +interface SpanCardConnectorProps { + type: SpanCardConnectorType +} + +export const SpanCardConnector = ({ type }: SpanCardConnectorProps) => { + if (type === "empty") return
    + + return ( +
    + {(type === "vertical" || type === "t-right") && ( +
    + )} + + {type === "t-right" && ( +
    + )} + + {type === "corner-top-right" && ( + <> +
    + +
    + +
    + + )} +
    + ) +} diff --git a/apps/web/src/components/agent-prism/SpanCard/SpanCardTimeline.tsx b/apps/web/src/components/agent-prism/SpanCard/SpanCardTimeline.tsx new file mode 100644 index 00000000..c2eb8b9c --- /dev/null +++ b/apps/web/src/components/agent-prism/SpanCard/SpanCardTimeline.tsx @@ -0,0 +1,59 @@ +import { getTimelineData } from "@evilmartians/agent-prism-data" +import type { + TraceSpan, + TraceSpanCategory, +} from "@evilmartians/agent-prism-types" +import cn from "classnames" + +interface SpanCardTimelineProps { + spanCard: TraceSpan + minStart: number + maxEnd: number + className?: string +} + +const timelineBgColors: Record = { + llm_call: "bg-agentprism-timeline-llm", + agent_invocation: "bg-agentprism-timeline-agent", + tool_execution: "bg-agentprism-timeline-tool", + chain_operation: "bg-agentprism-timeline-chain", + retrieval: "bg-agentprism-timeline-retrieval", + embedding: "bg-agentprism-timeline-embedding", + guardrail: "bg-agentprism-timeline-guardrail", + create_agent: "bg-agentprism-timeline-create-agent", + span: "bg-agentprism-timeline-span", + event: "bg-agentprism-timeline-event", + unknown: "bg-agentprism-timeline-unknown", +} + +export const SpanCardTimeline = ({ + spanCard, + minStart, + maxEnd, + className, +}: SpanCardTimelineProps) => { + const { startPercent, widthPercent } = getTimelineData({ + spanCard, + minStart, + maxEnd, + }) + + return ( + + + + + + ) +} diff --git a/apps/web/src/components/agent-prism/SpanCard/SpanCardToggle.tsx b/apps/web/src/components/agent-prism/SpanCard/SpanCardToggle.tsx new file mode 100644 index 00000000..b8d9eab1 --- /dev/null +++ b/apps/web/src/components/agent-prism/SpanCard/SpanCardToggle.tsx @@ -0,0 +1,38 @@ +import * as Collapsible from "@radix-ui/react-collapsible" +import { ChevronDown, ChevronRight } from "lucide-react" +import type { KeyboardEvent, MouseEvent } from "react" + +interface SpanCardToggleProps { + isExpanded: boolean + title: string + onToggleClick: (e: MouseEvent | KeyboardEvent) => void +} + +export const SpanCardToggle = ({ + isExpanded, + title, + onToggleClick, +}: SpanCardToggleProps) => ( + + + +) diff --git a/apps/web/src/components/agent-prism/SpanStatus.tsx b/apps/web/src/components/agent-prism/SpanStatus.tsx new file mode 100644 index 00000000..698d0959 --- /dev/null +++ b/apps/web/src/components/agent-prism/SpanStatus.tsx @@ -0,0 +1,79 @@ +import type { TraceSpanStatus } from "@evilmartians/agent-prism-types" +import cn from "classnames" +import { Check, Ellipsis, Info, TriangleAlert } from "lucide-react" +import type { ComponentPropsWithRef } from "react" + +type StatusVariant = "dot" | "badge" + +export type StatusProps = ComponentPropsWithRef<"div"> & { + status: TraceSpanStatus + variant?: StatusVariant +} + +const STATUS_COLORS_DOT: Record = { + success: "bg-agentprism-success", + error: "bg-agentprism-error", + pending: "bg-agentprism-pending", + warning: "bg-agentprism-warning", +} + +const STATUS_COLORS_BADGE: Record = { + success: + "bg-agentprism-success-muted text-agentprism-success-muted-foreground", + error: "bg-agentprism-error-muted text-agentprism-error-muted-foreground", + pending: + "bg-agentprism-pending-muted text-agentprism-pending-muted-foreground", + warning: + "bg-agentprism-warning-muted text-agentprism-warning-muted-foreground", +} + +export const SpanStatus = ({ + status, + variant = "dot", + ...rest +}: StatusProps) => { + const title = `Status: ${status}` + + return ( +
    + {variant === "dot" ? ( + + ) : ( + + )} +
    + ) +} + +interface StatusWithTitleProps extends StatusProps { + title: string +} + +const SpanStatusDot = ({ status, title }: StatusWithTitleProps) => { + return ( + + ) +} + +const SpanStatusBadge = ({ status, title }: StatusWithTitleProps) => { + return ( + + {status === "success" && } + {status === "error" && } + {status === "warning" && } + {status === "pending" && } + + ) +} diff --git a/apps/web/src/components/agent-prism/TabSelector.tsx b/apps/web/src/components/agent-prism/TabSelector.tsx new file mode 100644 index 00000000..5565b22f --- /dev/null +++ b/apps/web/src/components/agent-prism/TabSelector.tsx @@ -0,0 +1,35 @@ +import type { ReactElement } from "react" + +import { type TabItem, Tabs } from "./Tabs" + +export interface TabSelectorProps { + items: TabItem[] + value: T + onValueChange: (value: T) => void + defaultValue?: T + theme?: "underline" | "pill" + className?: string + onClick?: (event: React.MouseEvent) => void +} + +export const TabSelector = ({ + items, + value, + onValueChange, + defaultValue, + theme = "underline", + className, + onClick, +}: TabSelectorProps): ReactElement => { + return ( + + items={items} + value={value} + onValueChange={onValueChange} + defaultValue={defaultValue} + theme={theme} + className={className} + onClick={onClick} + /> + ) +} diff --git a/apps/web/src/components/agent-prism/Tabs.tsx b/apps/web/src/components/agent-prism/Tabs.tsx new file mode 100644 index 00000000..968c90a6 --- /dev/null +++ b/apps/web/src/components/agent-prism/Tabs.tsx @@ -0,0 +1,139 @@ +import * as RadixTabs from "@radix-ui/react-tabs" +import cn from "classnames" +import type * as React from "react" +import type { ComponentPropsWithRef } from "react" + +export interface TabItem { + value: T + label: string + icon?: React.ReactNode + disabled?: boolean +} + +export type TabTheme = "underline" | "pill" + +const BASE_TRIGGER = + "text-sm font-medium transition-colors duration-200 disabled:opacity-50 disabled:cursor-not-allowed" + +const THEMES = { + underline: { + list: "h-9 flex border-b border-agentprism-border", + trigger: `w-full justify-center px-3 ${BASE_TRIGGER} + text-agentprism-secondary-foreground data-[state=active]:text-agentprism-foreground + border-b-2 border-transparent data-[state=active]:border-agentprism-border-inverse + -mb-[2px] + data-[state=inactive]:[&:not(:disabled)]:hover:border-agentprism-border-inverse/20 + data-[state=inactive]:[&:not(:disabled)]:hover:text-agentprism-muted-foreground`, + }, + pill: { + list: "h-9 inline-flex gap-1 p-1 bg-agentprism-secondary rounded-lg", + trigger: `px-3 ${BASE_TRIGGER} rounded-md + text-agentprism-muted-foreground data-[state=active]:text-agentprism-foreground + data-[state=inactive]:[&:not(:disabled)]:hover:bg-agentprism-background/50 data-[state=active]:bg-agentprism-background data-[state=active]:shadow-sm + dark:data-[state=active]:shadow-none`, + }, +} as const + +export type TabsProps = Omit< + ComponentPropsWithRef<"div">, + "dir" +> & { + /** + * Array of tab items to display + */ + items: TabItem[] + + /** + * The initially selected tab value (uncontrolled) + */ + defaultValue?: T + + /** + * The currently selected tab value (controlled) + */ + value?: T + + /** + * Callback fired when the selected tab changes + */ + onValueChange?: (value: T) => void + + /** + * Visual theme variant for the tabs + * @default "underline" + */ + theme?: TabTheme + + /** + * Optional className for the root container + */ + className?: string + + /** + * Optional className for the tabs list container + */ + tabsListClassName?: string + + /** + * Optional className for individual tab triggers + */ + triggerClassName?: string + + /** + * The direction of the content of the tabs + */ + dir?: "ltr" | "rtl" +} + +export const Tabs = ({ + items, + defaultValue, + value, + onValueChange, + theme = "underline", + className = "", + tabsListClassName = "", + triggerClassName = "", + dir, + ...rest +}: TabsProps) => { + const defaultTab = defaultValue || items[0]?.value + + const currentTheme = THEMES[theme] + + return ( + void} + dir={dir} + {...rest} + > + + {items.map((item: TabItem) => ( + + {item.icon && ( + + {item.icon} + + )} + {item.label} + + ))} + + + ) +} diff --git a/apps/web/src/components/agent-prism/TextInput.tsx b/apps/web/src/components/agent-prism/TextInput.tsx new file mode 100644 index 00000000..a772d442 --- /dev/null +++ b/apps/web/src/components/agent-prism/TextInput.tsx @@ -0,0 +1,143 @@ +import cn from "classnames" +import { X } from "lucide-react" +import { + type ChangeEvent, + type ComponentPropsWithRef, + type ReactNode, + type RefObject, + useRef, +} from "react" + +export type TextInputProps = ComponentPropsWithRef<"input"> & { + /** + * Callback fired when the input value changes + */ + onValueChange?: (value: string) => void + + /** + * Icon to display at the start of the input + */ + startIcon?: ReactNode + + /** + * Callback fired when the clear button is clicked. If this callback is provided, + * the clear button will be shown. + */ + onClear?: () => void + + /** + * Ref to the input element + */ + ref?: RefObject + + /** + * Optional className for the input element + */ + inputClassName?: string + + /** + * Unique identifier for the input (required) + */ + id: string + + /** + * Label text for the input + */ + label?: string + + /** + * Whether to visually hide the label while keeping it for screen readers + * @default false + */ + hideLabel?: boolean +} + +const iconBaseClassName = + "absolute top-1/2 -translate-y-1/2 flex items-center justify-center text-agentprism-muted-foreground" + +export const TextInput = ({ + className, + onChange, + onValueChange, + startIcon, + onClear, + ref, + inputClassName, + label, + hideLabel = false, + id, + ...rest +}: TextInputProps) => { + const inputRef = useRef(null) + + const handleChange = (e: ChangeEvent) => { + onChange?.(e) + onValueChange?.(e.target.value) + } + + const handleClear = () => { + onClear?.() + + if (ref) { + ref.current?.focus() + return + } + + inputRef.current?.focus() + } + + return ( +
    + {label && ( + + )} +
    + + {startIcon && ( +
    + {startIcon} +
    + )} + {onClear && rest.value && ( + + )} +
    +
    + ) +} diff --git a/apps/web/src/components/agent-prism/TimestampBadge.tsx b/apps/web/src/components/agent-prism/TimestampBadge.tsx new file mode 100644 index 00000000..847df44c --- /dev/null +++ b/apps/web/src/components/agent-prism/TimestampBadge.tsx @@ -0,0 +1,22 @@ +import type { ComponentPropsWithRef } from "react" + +import type { BadgeProps } from "./Badge" + +import { Badge } from "./Badge" + +export type TimestampBadgeProps = ComponentPropsWithRef<"span"> & { + timestamp: number + size?: BadgeProps["size"] +} + +export const TimestampBadge = ({ + timestamp, + size, + ...rest +}: TimestampBadgeProps) => { + return +} + +function formatTimestamp(timestamp: number): string { + return new Date(timestamp).toLocaleString() +} diff --git a/apps/web/src/components/agent-prism/TokensBadge.tsx b/apps/web/src/components/agent-prism/TokensBadge.tsx new file mode 100644 index 00000000..63c20589 --- /dev/null +++ b/apps/web/src/components/agent-prism/TokensBadge.tsx @@ -0,0 +1,28 @@ +import { Coins } from "lucide-react" +import type { ComponentPropsWithRef } from "react" + +import type { BadgeProps } from "./Badge" + +import { Badge } from "./Badge" +import { formatTokenCount } from "./shared" + +export type TokensBadgeProps = ComponentPropsWithRef<"span"> & { + tokensCount: number + size?: BadgeProps["size"] +} + +export const TokensBadge = ({ + tokensCount, + size, + ...rest +}: TokensBadgeProps) => { + return ( + } + size={size} + {...rest} + label={formatTokenCount(tokensCount)} + title={`${tokensCount.toLocaleString()} tokens`} + /> + ) +} diff --git a/apps/web/src/components/agent-prism/TraceList/TraceList.tsx b/apps/web/src/components/agent-prism/TraceList/TraceList.tsx new file mode 100644 index 00000000..164560ec --- /dev/null +++ b/apps/web/src/components/agent-prism/TraceList/TraceList.tsx @@ -0,0 +1,87 @@ +import type { TraceRecord } from "@evilmartians/agent-prism-types" + +import cn from "classnames" +import { ArrowLeft } from "lucide-react" + +import type { BadgeProps } from "../Badge" + +import { Badge } from "../Badge" +import { IconButton } from "../IconButton" +import { TraceListItem } from "./TraceListItem" + +type TraceRecordWithBadges = TraceRecord & { + badges?: Array +} + +type TraceListProps = { + traces: TraceRecordWithBadges[] + expanded: boolean + onExpandStateChange: (expanded: boolean) => void + className?: string + onTraceSelect?: (trace: TraceRecord) => void + selectedTrace?: TraceRecord +} + +export const TraceList = ({ + traces, + expanded, + onExpandStateChange, + className, + onTraceSelect, + selectedTrace, +}: TraceListProps) => { + return ( +
    +
    +
    +

    Traces

    + + +
    + + onExpandStateChange(!expanded)} + > + + +
    + + {expanded && ( +
      +
      + {traces.map((trace) => ( +
    • + onTraceSelect?.(trace)} + isSelected={selectedTrace?.id === trace.id} + badges={trace.badges} + /> +
    • + ))} +
      +
    + )} +
    + ) +} diff --git a/apps/web/src/components/agent-prism/TraceList/TraceListItem.tsx b/apps/web/src/components/agent-prism/TraceList/TraceListItem.tsx new file mode 100644 index 00000000..e08d0fc0 --- /dev/null +++ b/apps/web/src/components/agent-prism/TraceList/TraceListItem.tsx @@ -0,0 +1,104 @@ +import type { TraceRecord } from "@evilmartians/agent-prism-types" +import cn from "classnames" +import { Coins } from "lucide-react" +import type { KeyboardEvent } from "react" +import { useCallback } from "react" + +import type { AvatarProps } from "../Avatar" +import type { BadgeProps } from "../Badge" + +import { Badge } from "../Badge" +import { formatTokenCount } from "../shared" +import { TraceListItemHeader } from "./TraceListItemHeader" + +interface TraceListItemProps { + trace: TraceRecord + badges?: Array + avatar?: AvatarProps + onClick?: () => void + isSelected?: boolean + showDescription?: boolean +} + +export const TraceListItem = ({ + trace, + avatar, + onClick, + badges, + isSelected, + showDescription = true, +}: TraceListItemProps) => { + const handleKeyDown = useCallback( + (e: KeyboardEvent): void => { + if (e.key === "Enter" || e.key === " ") { + e.preventDefault() + onClick?.() + } + }, + [onClick] + ) + + const { name, agentDescription, totalCost, totalTokens } = trace + + // Pass/fail is the one signal worth a colored pill. Everything else + // (skills, model, ...) reads as plain metadata below — a wall of same-size + // badges made every field compete for attention instead of just the status. + const statusBadge = badges?.find((b) => + /^(passed|failed)$/i.test(String(b.label)) + ) + const metaBadges = badges?.filter((b) => b !== statusBadge) ?? [] + + return ( +
    + + +
    + {showDescription && agentDescription && ( + + {agentDescription} + + )} + +
    + {statusBadge && } + + {typeof totalTokens === "number" && ( + + + {formatTokenCount(totalTokens)} + + )} + + {typeof totalCost === "number" && ( + + ${totalCost} + + )} + + {metaBadges.map((badge, index) => ( + + {badge.label} + + ))} +
    +
    +
    + ) +} diff --git a/apps/web/src/components/agent-prism/TraceList/TraceListItemHeader.tsx b/apps/web/src/components/agent-prism/TraceList/TraceListItemHeader.tsx new file mode 100644 index 00000000..dfb6fd96 --- /dev/null +++ b/apps/web/src/components/agent-prism/TraceList/TraceListItemHeader.tsx @@ -0,0 +1,37 @@ +import type { TraceRecord } from "@evilmartians/agent-prism-types" + +import type { AvatarProps } from "../Avatar" + +import { Avatar } from "../Avatar" +import { Badge } from "../Badge" + +interface TraceListItemHeaderProps { + trace: TraceRecord + avatar?: AvatarProps +} + +export const TraceListItemHeader = ({ + trace, + avatar, +}: TraceListItemHeaderProps) => { + return ( +
    +
    + {avatar && } + +

    + {trace.name} +

    +
    + +
    + +
    +
    + ) +} diff --git a/apps/web/src/components/agent-prism/TraceViewer/TraceViewer.tsx b/apps/web/src/components/agent-prism/TraceViewer/TraceViewer.tsx new file mode 100644 index 00000000..e0deb48f --- /dev/null +++ b/apps/web/src/components/agent-prism/TraceViewer/TraceViewer.tsx @@ -0,0 +1,192 @@ +import { + filterSpansRecursively, + flattenSpans, +} from "@evilmartians/agent-prism-data" +import type { TraceRecord, TraceSpan } from "@evilmartians/agent-prism-types" +import { useCallback, useEffect, useMemo, useState } from "react" + +import type { BadgeProps } from "../Badge" +import type { SpanCardViewOptions } from "../SpanCard/SpanCard" +import { useIsMobile, useIsMounted } from "../shared" +import { TraceViewerDesktopLayout } from "./TraceViewerDesktopLayout" +import { TraceViewerMobileLayout } from "./TraceViewerMobileLayout" + +export interface TraceViewerData { + traceRecord: TraceRecord + badges?: Array + spans: TraceSpan[] + spanCardViewOptions?: SpanCardViewOptions +} + +export interface TraceViewerProps { + data: Array + spanCardViewOptions?: SpanCardViewOptions +} + +export const TraceViewer = ({ + data, + spanCardViewOptions, +}: TraceViewerProps) => { + const isMobile = useIsMobile() + const isMounted = useIsMounted() + + const [selectedSpan, setSelectedSpan] = useState() + const [searchValue, setSearchValue] = useState("") + const [traceListExpanded, setTraceListExpanded] = useState(true) + + const lastItem = data[data.length - 1] + const [selectedTrace, setSelectedTrace] = useState< + TraceRecordWithDisplayData | undefined + >( + lastItem + ? { + ...lastItem.traceRecord, + badges: lastItem.badges, + spanCardViewOptions: lastItem.spanCardViewOptions, + } + : undefined + ) + const [selectedTraceSpans, setSelectedTraceSpans] = useState( + lastItem?.spans || [] + ) + + // Auto-select the newest trace when a new one is added to the list. + useEffect(() => { + const newest = data[data.length - 1] + if (!newest) return + setSelectedTrace({ + ...newest.traceRecord, + badges: newest.badges, + spanCardViewOptions: newest.spanCardViewOptions, + }) + setSelectedTraceSpans(newest.spans) + setSelectedSpan(undefined) + setExpandedSpansIds([]) + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [data.length]) + + // Keep the selected trace's spans in sync as new spans arrive (live updates). + useEffect(() => { + if (!selectedTrace) return + const current = data.find((d) => d.traceRecord.id === selectedTrace.id) + if (current) setSelectedTraceSpans(current.spans) + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [data, selectedTrace?.id]) + + const traceRecords: TraceRecordWithDisplayData[] = useMemo(() => { + return data.map((item) => ({ + ...item.traceRecord, + badges: item.badges, + spanCardViewOptions: item.spanCardViewOptions, + })) + }, [data]) + + const filteredSpans = useMemo(() => { + if (!searchValue.trim()) { + return selectedTraceSpans + } + return filterSpansRecursively(selectedTraceSpans, searchValue) + }, [selectedTraceSpans, searchValue]) + + const allIds = useMemo(() => { + return flattenSpans(selectedTraceSpans).map((span) => span.id) + }, [selectedTraceSpans]) + + const [expandedSpansIds, setExpandedSpansIds] = useState(allIds) + + useEffect(() => { + setExpandedSpansIds(allIds) + }, [allIds]) + + useEffect(() => { + if (!isMounted || isMobile) return + + if (selectedTraceSpans.length > 0 && !selectedSpan) { + setSelectedSpan(selectedTraceSpans[0]) + } + }, [selectedTraceSpans, isMobile, isMounted, selectedSpan]) + + const handleExpandAll = useCallback(() => { + setExpandedSpansIds(allIds) + }, [allIds]) + + const handleCollapseAll = useCallback(() => { + setExpandedSpansIds([]) + }, []) + + const handleTraceSelect = useCallback( + (trace: TraceRecord) => { + setSelectedSpan(undefined) + setExpandedSpansIds([]) + setSelectedTrace(trace) + setSelectedTraceSpans( + data.find((item) => item.traceRecord.id === trace.id)?.spans ?? [] + ) + }, + [data] + ) + + const handleClearTraceSelection = useCallback(() => { + setSelectedTrace(undefined) + setSelectedTraceSpans([]) + setSelectedSpan(undefined) + setExpandedSpansIds([]) + }, []) + + const props: TraceViewerLayoutProps = { + traceRecords, + traceListExpanded, + setTraceListExpanded, + selectedTrace, + selectedTraceId: selectedTrace?.id, + selectedSpan, + setSelectedSpan, + searchValue, + setSearchValue, + filteredSpans, + expandedSpansIds, + setExpandedSpansIds, + handleExpandAll, + handleCollapseAll, + handleTraceSelect, + spanCardViewOptions: + spanCardViewOptions || selectedTrace?.spanCardViewOptions, + onClearTraceSelection: handleClearTraceSelection, + } + + return ( +
    +
    + +
    +
    + +
    +
    + ) +} + +export interface TraceRecordWithDisplayData extends TraceRecord { + spanCardViewOptions?: SpanCardViewOptions + badges?: BadgeProps[] +} + +export interface TraceViewerLayoutProps { + traceRecords: TraceRecordWithDisplayData[] + traceListExpanded: boolean + setTraceListExpanded: (expanded: boolean) => void + selectedTrace: TraceRecordWithDisplayData | undefined + selectedTraceId?: string + selectedSpan: TraceSpan | undefined + setSelectedSpan: (span: TraceSpan | undefined) => void + searchValue: string + setSearchValue: (value: string) => void + filteredSpans: TraceSpan[] + expandedSpansIds: string[] + setExpandedSpansIds: (ids: string[]) => void + handleExpandAll: () => void + handleCollapseAll: () => void + handleTraceSelect: (trace: TraceRecord) => void + spanCardViewOptions?: SpanCardViewOptions + onClearTraceSelection: () => void +} diff --git a/apps/web/src/components/agent-prism/TraceViewer/TraceViewerDesktopLayout.tsx b/apps/web/src/components/agent-prism/TraceViewer/TraceViewerDesktopLayout.tsx new file mode 100644 index 00000000..fd1950f6 --- /dev/null +++ b/apps/web/src/components/agent-prism/TraceViewer/TraceViewerDesktopLayout.tsx @@ -0,0 +1,111 @@ +import { + Panel, + Group as PanelGroup, + Separator as PanelResizeHandle, +} from "react-resizable-panels" + +import { DetailsView } from "../DetailsView/DetailsView" +import { TraceList } from "../TraceList/TraceList" +import type { TraceViewerLayoutProps } from "./TraceViewer" +import { TraceViewerPlaceholder } from "./TraceViewerPlaceholder" +import { TraceViewerTreeViewContainer } from "./TraceViewerTreeViewContainer" + +export const TraceViewerDesktopLayout = ({ + traceRecords, + traceListExpanded, + setTraceListExpanded, + selectedTrace, + selectedTraceId, + selectedSpan, + setSelectedSpan, + searchValue, + setSearchValue, + filteredSpans, + expandedSpansIds, + setExpandedSpansIds, + handleExpandAll, + handleCollapseAll, + handleTraceSelect, + spanCardViewOptions, +}: TraceViewerLayoutProps) => { + const actualSelectedTrace = + traceRecords.find((t) => t.id === selectedTraceId) || selectedTrace + + // The trace list only earns its ~20% of width when there's more than one + // trace to switch between; with exactly one (our only usage today: one + // eval's run opened at a time) it's a dead-end list with nothing to select. + const showTraceList = traceRecords.length > 1 + + return ( + + {showTraceList && ( + <> + + + + + + + )} + + {selectedTrace ? ( + + + + ) : ( + + + + )} + + + + + {selectedSpan ? ( + + ) : ( + + )} + + + ) +} diff --git a/apps/web/src/components/agent-prism/TraceViewer/TraceViewerMobileLayout.tsx b/apps/web/src/components/agent-prism/TraceViewer/TraceViewerMobileLayout.tsx new file mode 100644 index 00000000..c68f7e6a --- /dev/null +++ b/apps/web/src/components/agent-prism/TraceViewer/TraceViewerMobileLayout.tsx @@ -0,0 +1,109 @@ +import { ArrowLeft } from "lucide-react" + +import { Button } from "../Button" +import { DetailsView } from "../DetailsView/DetailsView" +import { TraceList } from "../TraceList/TraceList" +import type { TraceViewerLayoutProps } from "../TraceViewer/TraceViewer" +import { TraceViewerTreeViewContainer } from "./TraceViewerTreeViewContainer" + +export const TraceViewerMobileLayout = ({ + traceRecords, + traceListExpanded, + setTraceListExpanded, + selectedTrace, + selectedTraceId, + selectedSpan, + setSelectedSpan, + searchValue, + setSearchValue, + filteredSpans, + expandedSpansIds, + setExpandedSpansIds, + handleExpandAll, + handleCollapseAll, + handleTraceSelect, + spanCardViewOptions, + onClearTraceSelection, +}: TraceViewerLayoutProps) => { + if ( + selectedTrace && + selectedTraceId && + filteredSpans.length > 0 && + selectedSpan + ) { + return ( +
    + + +
    + ) + } + + if ( + selectedTrace && + selectedTraceId && + filteredSpans.length > 0 && + !selectedSpan + ) { + // With more than one trace, "back" returns to a real list to pick + // another. With exactly one (our only usage today) there's nowhere + // useful to go back to, so skip the dead-end button. + const showBackToList = traceRecords.length > 1 + + return ( +
    + {showBackToList && ( +
    + +
    + )} + + +
    + ) + } + + return ( +
    + t.id === selectedTraceId)} + /> +
    + ) +} diff --git a/apps/web/src/components/agent-prism/TraceViewer/TraceViewerPlaceholder.tsx b/apps/web/src/components/agent-prism/TraceViewer/TraceViewerPlaceholder.tsx new file mode 100644 index 00000000..1c084caa --- /dev/null +++ b/apps/web/src/components/agent-prism/TraceViewer/TraceViewerPlaceholder.tsx @@ -0,0 +1,5 @@ +export const TraceViewerPlaceholder = ({ title }: { title: string }) => ( +

    + {title} +

    +) diff --git a/apps/web/src/components/agent-prism/TraceViewer/TraceViewerSearchAndControls.tsx b/apps/web/src/components/agent-prism/TraceViewer/TraceViewerSearchAndControls.tsx new file mode 100644 index 00000000..d64540e0 --- /dev/null +++ b/apps/web/src/components/agent-prism/TraceViewer/TraceViewerSearchAndControls.tsx @@ -0,0 +1,30 @@ +import { + CollapseAllButton, + ExpandAllButton, +} from "../CollapseAndExpandControls" +import { SearchInput } from "../SearchInput" + +export const TraceViewerSearchAndControls = ({ + searchValue, + setSearchValue, + handleExpandAll, + handleCollapseAll, +}: { + searchValue: string + setSearchValue: (value: string) => void + handleExpandAll: () => void + handleCollapseAll: () => void +}) => ( +
    + setSearchValue(e.target.value)} + placeholder="Search spans" + /> +
    + + +
    +
    +) diff --git a/apps/web/src/components/agent-prism/TraceViewer/TraceViewerTreeViewContainer.tsx b/apps/web/src/components/agent-prism/TraceViewer/TraceViewerTreeViewContainer.tsx new file mode 100644 index 00000000..eaa54678 --- /dev/null +++ b/apps/web/src/components/agent-prism/TraceViewer/TraceViewerTreeViewContainer.tsx @@ -0,0 +1,76 @@ +import type { TraceSpan } from "@evilmartians/agent-prism-types" +import { Badge } from "../Badge" +import type { SpanCardViewOptions } from "../SpanCard/SpanCard" +import { TraceListItemHeader } from "../TraceList/TraceListItemHeader" +import { TreeView } from "../TreeView" +import type { TraceRecordWithDisplayData } from "./TraceViewer" +import { TraceViewerSearchAndControls } from "./TraceViewerSearchAndControls" + +export const TraceViewerTreeViewContainer = ({ + searchValue, + setSearchValue, + handleExpandAll, + handleCollapseAll, + filteredSpans, + selectedSpan, + setSelectedSpan, + expandedSpansIds, + setExpandedSpansIds, + spanCardViewOptions, + selectedTrace, + showHeader = true, +}: { + searchValue: string + setSearchValue: (value: string) => void + handleExpandAll: () => void + handleCollapseAll: () => void + filteredSpans: TraceSpan[] + selectedSpan: TraceSpan | undefined + setSelectedSpan: (span: TraceSpan | undefined) => void + expandedSpansIds: string[] + setExpandedSpansIds: (ids: string[]) => void + spanCardViewOptions?: SpanCardViewOptions + selectedTrace?: TraceRecordWithDisplayData + showHeader?: boolean +}) => ( + <> + {showHeader && selectedTrace && ( +
    + + + {selectedTrace.badges && selectedTrace.badges.length > 0 && ( +
    + {selectedTrace.badges.map((badge, index) => ( + + ))} +
    + )} +
    + )} + +
    + +
    + {filteredSpans.length === 0 ? ( +
    + No spans found +
    + ) : ( + + )} +
    +
    + +) diff --git a/apps/web/src/components/agent-prism/TreeView.tsx b/apps/web/src/components/agent-prism/TreeView.tsx new file mode 100644 index 00000000..e64b94a8 --- /dev/null +++ b/apps/web/src/components/agent-prism/TreeView.tsx @@ -0,0 +1,70 @@ +import { findTimeRange, flattenSpans } from "@evilmartians/agent-prism-data" +import type { TraceSpan } from "@evilmartians/agent-prism-types" +import cn from "classnames" +import type { FC } from "react" +import { BrandLogo } from "./BrandLogo" +import type { SpanCardViewOptions } from "./SpanCard/SpanCard" +import { SpanCard } from "./SpanCard/SpanCard" + +interface TreeViewProps { + spans: TraceSpan[] + className?: string + selectedSpan?: TraceSpan + onSpanSelect?: (span: TraceSpan) => void + expandedSpansIds: string[] + onExpandSpansIdsChange: (ids: string[]) => void + spanCardViewOptions?: SpanCardViewOptions +} + +export const TreeView: FC = ({ + spans, + onSpanSelect, + className = "", + selectedSpan, + expandedSpansIds, + onExpandSpansIdsChange, + spanCardViewOptions, +}) => { + const allCards = flattenSpans(spans) + const { minStart, maxEnd } = findTimeRange(allCards) + + return ( +
    +
      + {spans.map((span, idx) => { + const brand = span.metadata?.brand as { type: string } | undefined + + return ( + , + size: "4", + rounded: "sm", + category: span.type, + } + : undefined + } + /> + ) + })} +
    +
    + ) +} diff --git a/apps/web/src/components/agent-prism/shared.test.ts b/apps/web/src/components/agent-prism/shared.test.ts new file mode 100644 index 00000000..d1aad8d3 --- /dev/null +++ b/apps/web/src/components/agent-prism/shared.test.ts @@ -0,0 +1,49 @@ +import { describe, expect, it } from "vitest" +import { deepParseJson, formatTokenCount } from "./shared" + +describe("formatTokenCount", () => { + it("renders under-1000 counts as-is", () => { + expect(formatTokenCount(0)).toBe("0") + expect(formatTokenCount(999)).toBe("999") + }) + + it("renders thousands with one decimal and a K suffix", () => { + expect(formatTokenCount(2_760)).toBe("2.8K") + expect(formatTokenCount(164_900)).toBe("164.9K") + }) + + it("renders millions with an M suffix", () => { + expect(formatTokenCount(1_500_000)).toBe("1.5M") + }) +}) + +describe("deepParseJson", () => { + it("recursively parses a JSON string nested inside another JSON structure", () => { + const raw = { + content: [ + { type: "text", text: '{"error":{"name":"Error","message":"boom"}}' }, + ], + isError: true, + } + expect(deepParseJson(raw)).toEqual({ + content: [ + { type: "text", text: { error: { name: "Error", message: "boom" } } }, + ], + isError: true, + }) + }) + + it("leaves plain strings that are not JSON objects/arrays alone", () => { + expect(deepParseJson("hello world")).toBe("hello world") + expect(deepParseJson("5")).toBe("5") // not treated as JSON number 5 + expect(deepParseJson("true")).toBe("true") + }) + + it("leaves a malformed JSON-looking string alone instead of throwing", () => { + expect(deepParseJson("{not valid json")).toBe("{not valid json") + }) + + it("recurses into arrays too", () => { + expect(deepParseJson(['{"a":1}', "plain"])).toEqual([{ a: 1 }, "plain"]) + }) +}) diff --git a/apps/web/src/components/agent-prism/shared.ts b/apps/web/src/components/agent-prism/shared.ts new file mode 100644 index 00000000..3c2b66c4 --- /dev/null +++ b/apps/web/src/components/agent-prism/shared.ts @@ -0,0 +1,213 @@ +import type { TraceSpanCategory } from "@evilmartians/agent-prism-types" +import type { LucideIcon } from "lucide-react" + +import { + BarChart2, + Bot, + CircleDot, + HelpCircle, + Link, + MoveHorizontal, + Plus, + Search, + ShieldCheck, + Wrench, + Zap, +} from "lucide-react" +import { useEffect, useState } from "react" + +// TYPES + +export type ColorVariant = + | "purple" + | "indigo" + | "orange" + | "teal" + | "cyan" + | "sky" + | "yellow" + | "emerald" + | "red" + | "gray" + +export type ComponentSize = + | "4" + | "5" + | "6" + | "7" + | "8" + | "9" + | "10" + | "11" + | "12" + | "16" + +// CONSTANTS + +export const ROUNDED_CLASSES = { + none: "rounded-none", + sm: "rounded-sm", + md: "rounded-md", + lg: "rounded-lg", + full: "rounded-full", +} + +/** + * Shared configuration for span categories containing label, theme, and icon + */ +export const SPAN_CATEGORY_CONFIG: Record< + TraceSpanCategory, + { + label: string + theme: ColorVariant + icon: LucideIcon + } +> = { + llm_call: { + label: "LLM", + theme: "purple", + icon: Zap, + }, + tool_execution: { + label: "TOOL", + theme: "orange", + icon: Wrench, + }, + agent_invocation: { + label: "AGENT INVOCATION", + theme: "indigo", + icon: Bot, + }, + chain_operation: { + label: "CHAIN", + theme: "teal", + icon: Link, + }, + retrieval: { + label: "RETRIEVAL", + theme: "cyan", + icon: Search, + }, + embedding: { + label: "EMBEDDING", + theme: "emerald", + icon: BarChart2, + }, + create_agent: { + label: "CREATE AGENT", + theme: "sky", + icon: Plus, + }, + span: { + label: "SPAN", + theme: "cyan", + icon: MoveHorizontal, + }, + event: { + label: "EVENT", + theme: "emerald", + icon: CircleDot, + }, + guardrail: { + label: "GUARDRAIL", + theme: "red", + icon: ShieldCheck, + }, + unknown: { + label: "UNKNOWN", + theme: "gray", + icon: HelpCircle, + }, +} + +// UTILS + +/** `164900` -> `"164.9K"`. Under 1,000 renders as-is (no decimals to add). */ +export function formatTokenCount(count: number): string { + const abs = Math.abs(count) + if (abs < 1_000) return String(count) + if (abs < 1_000_000) return `${(count / 1_000).toFixed(1)}K` + return `${(count / 1_000_000).toFixed(1)}M` +} + +/** + * Recursively parses string values that are themselves JSON — common in + * tool results (e.g. an MCP error payload's `content[0].text` is a + * JSON-stringified error object). Without this, the JSON viewer renders that + * inner structure as one long escaped-quote string instead of a real nested + * object. Only recurses into strings that look like a JSON object/array + * (trimmed, starts with `{` or `[`) — a plain string that happens to parse + * as a number/boolean (e.g. `"5"`, `"true"`) is left alone, since collapsing + * that would just be surprising, not clarifying. + */ +export function deepParseJson(value: unknown, depth = 0): unknown { + if (depth > 10) return value // guard against pathological nesting + if (typeof value === "string") { + const trimmed = value.trim() + if (!trimmed.startsWith("{") && !trimmed.startsWith("[")) return value + try { + return deepParseJson(JSON.parse(trimmed), depth + 1) + } catch { + return value + } + } + if (Array.isArray(value)) { + return value.map((item) => deepParseJson(item, depth + 1)) + } + if (value && typeof value === "object") { + return Object.fromEntries( + Object.entries(value).map(([key, v]) => [ + key, + deepParseJson(v, depth + 1), + ]) + ) + } + return value +} + +export function getSpanCategoryTheme( + category: TraceSpanCategory +): ColorVariant { + return SPAN_CATEGORY_CONFIG[category].theme +} + +export function getSpanCategoryLabel(category: TraceSpanCategory): string { + return SPAN_CATEGORY_CONFIG[category].label +} + +export function getSpanCategoryIcon(category: TraceSpanCategory): LucideIcon { + return SPAN_CATEGORY_CONFIG[category].icon +} + +export const useIsMobile = () => { + const isMounted = useIsMounted() + + const [isMobile, setIsMobile] = useState(false) + + useEffect(() => { + // TODO: replace with something more beautiful and correct (tailwind screens?) + const mediaQuery = window.matchMedia("(max-width: 1023px)") + + const handleChange = (e: MediaQueryListEvent | MediaQueryList) => { + setIsMobile(e.matches) + } + + handleChange(mediaQuery) + + mediaQuery.addEventListener("change", handleChange) + + return () => mediaQuery.removeEventListener("change", handleChange) + }, []) + + return isMounted ? isMobile : false +} + +export const useIsMounted = () => { + const [isMounted, setIsMounted] = useState(false) + + useEffect(() => { + setIsMounted(true) + }, []) + + return isMounted +} diff --git a/apps/web/src/components/agent-prism/theme/index.ts b/apps/web/src/components/agent-prism/theme/index.ts new file mode 100644 index 00000000..a1cd765b --- /dev/null +++ b/apps/web/src/components/agent-prism/theme/index.ts @@ -0,0 +1,99 @@ +export const agentPrismPrefix = "agentprism" + +export const AGENT_PRISM_TOKENS = [ + "background", + "foreground", + "primary", + "primary-foreground", + "secondary", + "secondary-foreground", + "muted", + "muted-foreground", + "accent", + "accent-foreground", + "brand", + "brand-foreground", + "brand-secondary", + "brand-secondary-foreground", + "border", + "border-subtle", + "border-strong", + "border-inverse", + "success", + "success-muted", + "success-muted-foreground", + "error", + "error-muted", + "error-muted-foreground", + "warning", + "warning-muted", + "warning-muted-foreground", + "pending", + "pending-muted", + "pending-muted-foreground", + "code-string", + "code-number", + "code-key", + "code-base", + "badge-default", + "badge-default-foreground", + "avatar-llm", + "badge-llm", + "badge-llm-foreground", + "timeline-llm", + "avatar-agent", + "badge-agent", + "badge-agent-foreground", + "timeline-agent", + "avatar-tool", + "badge-tool", + "badge-tool-foreground", + "timeline-tool", + "avatar-chain", + "badge-chain", + "badge-chain-foreground", + "timeline-chain", + "avatar-retrieval", + "badge-retrieval", + "badge-retrieval-foreground", + "timeline-retrieval", + "avatar-embedding", + "badge-embedding", + "badge-embedding-foreground", + "timeline-embedding", + "avatar-guardrail", + "badge-guardrail", + "badge-guardrail-foreground", + "timeline-guardrail", + "avatar-create-agent", + "badge-create-agent", + "badge-create-agent-foreground", + "timeline-create-agent", + "avatar-span", + "badge-span", + "badge-span-foreground", + "timeline-span", + "avatar-event", + "badge-event", + "badge-event-foreground", + "timeline-event", + "avatar-unknown", + "badge-unknown", + "badge-unknown-foreground", + "timeline-unknown", +] as const + +export type AgentPrismToken = (typeof AGENT_PRISM_TOKENS)[number] + +export type AgentPrismColors = Record + +export const agentPrismTailwindColors = Object.fromEntries( + AGENT_PRISM_TOKENS.map((tokenName) => [ + `agentprism-${tokenName}`, + token(tokenName), + ]) +) as AgentPrismColors + +function token(name: string) { + return `var(--${agentPrismPrefix}-${name})` +} diff --git a/apps/web/src/components/agent-prism/theme/theme.css b/apps/web/src/components/agent-prism/theme/theme.css new file mode 100644 index 00000000..ba7bc526 --- /dev/null +++ b/apps/web/src/components/agent-prism/theme/theme.css @@ -0,0 +1,232 @@ +/* + * AgentPrism design tokens, remapped onto the app's own Supabase semantic + * tokens (index.css) instead of AgentPrism's generic defaults — so the trace + * viewer's surface, borders, and status colors are the SAME tokens the rest + * of the app already uses, not a second unrelated palette. Per-span-type + * badges (LLM/TOOL/CHAIN/...) keep distinct hues for scannability, but are + * tuned to sit on the app's actual dark surface rather than AgentPrism's own. + * `:root`/`.dark` here just mirror index.css's `.light`/`.dark` split (same + * ThemeProvider toggle), not a separate light/dark system. + */ + +:root { + /* Structure: same tokens as the rest of the app's chrome. */ + --agentprism-background: var(--background); + --agentprism-foreground: var(--foreground); + --agentprism-primary: var(--primary); + --agentprism-primary-foreground: var(--primary-foreground); + /* Foreground-tinted overlay, not the app's own `--secondary` — that token + lightens off `--background` (an "elevation" trick), which clips to white + in light mode where the background is already near-max lightness. This + stays visibly distinct from the surface in both themes. */ + --agentprism-secondary: oklch(from var(--foreground) l c h / 8%); + --agentprism-secondary-foreground: var(--secondary-foreground); + --agentprism-muted: var(--muted); + --agentprism-muted-foreground: var(--muted-foreground); + --agentprism-accent: var(--accent); + --agentprism-accent-foreground: var(--accent-foreground); + + /* Brand: the actual Supabase green, not AgentPrism's default purple. */ + --agentprism-brand: var(--primary); + --agentprism-brand-foreground: var(--primary-foreground); + --agentprism-brand-secondary: oklch(from var(--foreground) l c h / 8%); + --agentprism-brand-secondary-foreground: var(--secondary-foreground); + + /* Borders */ + --agentprism-border: var(--border); + --agentprism-border-subtle: oklch(from var(--border) l c h / 50%); + --agentprism-border-strong: oklch(from var(--border) l c h / 90%); + --agentprism-border-inverse: var(--foreground); + + /* Status — reuses the app's own warning/destructive/info tokens. Supabase + has no separate "success" token; the brand green already carries that + meaning across the product (e.g. "active"/"healthy" states). */ + --agentprism-success: var(--primary); + --agentprism-success-muted: oklch(from var(--primary) l c h / 15%); + --agentprism-success-muted-foreground: var(--primary); + --agentprism-error: var(--destructive); + --agentprism-error-muted: oklch(from var(--destructive) l c h / 15%); + --agentprism-error-muted-foreground: var(--destructive); + --agentprism-warning: var(--warning); + --agentprism-warning-muted: oklch(from var(--warning) l c h / 15%); + --agentprism-warning-muted-foreground: var(--warning); + --agentprism-pending: var(--info); + --agentprism-pending-muted: oklch(from var(--info) l c h / 15%); + --agentprism-pending-muted-foreground: var(--info); + + /* Code (JSON viewer syntax highlighting) */ + --agentprism-code-string: var(--primary); + --agentprism-code-number: var(--warning); + --agentprism-code-key: var(--info); + --agentprism-code-base: var(--muted-foreground); + + /* Badges */ + --agentprism-badge-default: var(--muted); + --agentprism-badge-default-foreground: var(--muted-foreground); + + /* Agent invocation (the root span) — the most prominent span in a trace, + so it gets the brand color itself. */ + --agentprism-avatar-agent: var(--primary); + --agentprism-badge-agent: oklch(from var(--primary) l c h / 15%); + --agentprism-badge-agent-foreground: var(--primary); + --agentprism-timeline-agent: var(--primary); + + /* LLM */ + --agentprism-avatar-llm: oklch(58% 0.2 292); + --agentprism-badge-llm: oklch(58% 0.2 292 / 15%); + --agentprism-badge-llm-foreground: oklch(65% 0.19 292); + --agentprism-timeline-llm: oklch(65% 0.19 292); + + /* Tool */ + --agentprism-avatar-tool: oklch(68% 0.16 45); + --agentprism-badge-tool: oklch(68% 0.16 45 / 15%); + --agentprism-badge-tool-foreground: oklch(70% 0.15 45); + --agentprism-timeline-tool: oklch(70% 0.15 45); + + /* Chain */ + --agentprism-avatar-chain: oklch(65% 0.13 220); + --agentprism-badge-chain: oklch(65% 0.13 220 / 15%); + --agentprism-badge-chain-foreground: oklch(68% 0.13 220); + --agentprism-timeline-chain: oklch(68% 0.13 220); + + /* Retrieval */ + --agentprism-avatar-retrieval: oklch(68% 0.13 205); + --agentprism-badge-retrieval: oklch(68% 0.13 205 / 15%); + --agentprism-badge-retrieval-foreground: oklch(70% 0.13 205); + --agentprism-timeline-retrieval: oklch(70% 0.13 205); + + /* Embedding */ + --agentprism-avatar-embedding: var(--primary); + --agentprism-badge-embedding: oklch(from var(--primary) l c h / 15%); + --agentprism-badge-embedding-foreground: var(--primary); + --agentprism-timeline-embedding: var(--primary); + + /* Guardrail */ + --agentprism-avatar-guardrail: var(--destructive); + --agentprism-badge-guardrail: oklch(from var(--destructive) l c h / 15%); + --agentprism-badge-guardrail-foreground: var(--destructive); + --agentprism-timeline-guardrail: var(--destructive); + + /* Create agent */ + --agentprism-avatar-create-agent: oklch(68% 0.13 205); + --agentprism-badge-create-agent: oklch(68% 0.13 205 / 15%); + --agentprism-badge-create-agent-foreground: oklch(70% 0.13 205); + --agentprism-timeline-create-agent: oklch(70% 0.13 205); + + /* Span */ + --agentprism-avatar-span: var(--muted-foreground); + --agentprism-badge-span: var(--muted); + --agentprism-badge-span-foreground: var(--muted-foreground); + --agentprism-timeline-span: var(--muted-foreground); + + /* Event */ + --agentprism-avatar-event: var(--muted-foreground); + --agentprism-badge-event: var(--muted); + --agentprism-badge-event-foreground: var(--muted-foreground); + --agentprism-timeline-event: var(--muted-foreground); + + /* Unknown */ + --agentprism-avatar-unknown: var(--muted-foreground); + --agentprism-badge-unknown: var(--muted); + --agentprism-badge-unknown-foreground: var(--muted-foreground); + --agentprism-timeline-unknown: var(--muted-foreground); +} + +.dark { + --agentprism-background: var(--background); + --agentprism-foreground: var(--foreground); + --agentprism-primary: var(--primary); + --agentprism-primary-foreground: var(--primary-foreground); + --agentprism-secondary: oklch(from var(--foreground) l c h / 8%); + --agentprism-secondary-foreground: var(--secondary-foreground); + --agentprism-muted: var(--muted); + --agentprism-muted-foreground: var(--muted-foreground); + --agentprism-accent: var(--accent); + --agentprism-accent-foreground: var(--accent-foreground); + + --agentprism-brand: var(--primary); + --agentprism-brand-foreground: var(--primary-foreground); + --agentprism-brand-secondary: oklch(from var(--foreground) l c h / 8%); + --agentprism-brand-secondary-foreground: var(--secondary-foreground); + + --agentprism-border: var(--border); + --agentprism-border-subtle: oklch(from var(--border) l c h / 50%); + --agentprism-border-strong: oklch(from var(--border) l c h / 90%); + --agentprism-border-inverse: var(--foreground); + + --agentprism-success: var(--primary); + --agentprism-success-muted: oklch(from var(--primary) l c h / 18%); + --agentprism-success-muted-foreground: var(--primary); + --agentprism-error: var(--destructive); + --agentprism-error-muted: oklch(from var(--destructive) l c h / 18%); + --agentprism-error-muted-foreground: var(--destructive); + --agentprism-warning: var(--warning); + --agentprism-warning-muted: oklch(from var(--warning) l c h / 18%); + --agentprism-warning-muted-foreground: var(--warning); + --agentprism-pending: var(--info); + --agentprism-pending-muted: oklch(from var(--info) l c h / 18%); + --agentprism-pending-muted-foreground: var(--info); + + --agentprism-code-string: var(--primary); + --agentprism-code-number: var(--warning); + --agentprism-code-key: var(--info); + --agentprism-code-base: var(--muted-foreground); + + --agentprism-badge-default: var(--muted); + --agentprism-badge-default-foreground: var(--muted-foreground); + + --agentprism-avatar-agent: var(--primary); + --agentprism-badge-agent: oklch(from var(--primary) l c h / 18%); + --agentprism-badge-agent-foreground: var(--primary); + --agentprism-timeline-agent: var(--primary); + + --agentprism-avatar-llm: oklch(78% 0.14 292); + --agentprism-badge-llm: oklch(78% 0.14 292 / 18%); + --agentprism-badge-llm-foreground: oklch(80% 0.13 292); + --agentprism-timeline-llm: oklch(78% 0.14 292); + + --agentprism-avatar-tool: oklch(80% 0.12 45); + --agentprism-badge-tool: oklch(80% 0.12 45 / 18%); + --agentprism-badge-tool-foreground: oklch(82% 0.11 45); + --agentprism-timeline-tool: oklch(80% 0.12 45); + + --agentprism-avatar-chain: oklch(80% 0.1 220); + --agentprism-badge-chain: oklch(80% 0.1 220 / 18%); + --agentprism-badge-chain-foreground: oklch(82% 0.1 220); + --agentprism-timeline-chain: oklch(80% 0.1 220); + + --agentprism-avatar-retrieval: oklch(82% 0.1 205); + --agentprism-badge-retrieval: oklch(82% 0.1 205 / 18%); + --agentprism-badge-retrieval-foreground: oklch(84% 0.1 205); + --agentprism-timeline-retrieval: oklch(82% 0.1 205); + + --agentprism-avatar-embedding: var(--primary); + --agentprism-badge-embedding: oklch(from var(--primary) l c h / 18%); + --agentprism-badge-embedding-foreground: var(--primary); + --agentprism-timeline-embedding: var(--primary); + + --agentprism-avatar-guardrail: var(--destructive); + --agentprism-badge-guardrail: oklch(from var(--destructive) l c h / 18%); + --agentprism-badge-guardrail-foreground: var(--destructive); + --agentprism-timeline-guardrail: var(--destructive); + + --agentprism-avatar-create-agent: oklch(82% 0.1 205); + --agentprism-badge-create-agent: oklch(82% 0.1 205 / 18%); + --agentprism-badge-create-agent-foreground: oklch(84% 0.1 205); + --agentprism-timeline-create-agent: oklch(82% 0.1 205); + + --agentprism-avatar-span: var(--muted-foreground); + --agentprism-badge-span: var(--muted); + --agentprism-badge-span-foreground: var(--muted-foreground); + --agentprism-timeline-span: var(--muted-foreground); + + --agentprism-avatar-event: var(--muted-foreground); + --agentprism-badge-event: var(--muted); + --agentprism-badge-event-foreground: var(--muted-foreground); + --agentprism-timeline-event: var(--muted-foreground); + + --agentprism-avatar-unknown: var(--muted-foreground); + --agentprism-badge-unknown: var(--muted); + --agentprism-badge-unknown-foreground: var(--muted-foreground); + --agentprism-timeline-unknown: var(--muted-foreground); +} diff --git a/apps/web/src/components/results/eval-details.tsx b/apps/web/src/components/results/eval-details.tsx index 3cffc9c6..116c8cd4 100644 --- a/apps/web/src/components/results/eval-details.tsx +++ b/apps/web/src/components/results/eval-details.tsx @@ -1,4 +1,5 @@ import type { ReactNode } from "react" +import { useQueryState } from "nuqs" import { CheckIcon, ChevronRightIcon, @@ -11,6 +12,7 @@ import { import { passFailClassName } from "@/components/results/table-shared" import type { CheckResult, DocsCall, ParsedResult } from "@/lib/eval-results" import { formatProductLabel, formatTagLabel } from "@/lib/format" +import { traceEvalParser, TRACE_EVAL_QUERY_KEY } from "@/lib/url-state" import { cn } from "@/lib/utils" const DOCS_CALL_SOURCE_LABEL: Record = { @@ -205,6 +207,8 @@ function ResultDocsCalls({ calls }: { calls: DocsCall[] }) { /** Everything recorded about one run, shown when its row in the sheet is expanded. */ export function EvalDetails({ result }: { result: ParsedResult }) { + const [, setTraceEval] = useQueryState(TRACE_EVAL_QUERY_KEY, traceEvalParser) + return (
    {result.prompt ? ( @@ -219,6 +223,18 @@ export function EvalDetails({ result }: { result: ParsedResult }) { } /> ) : null} + void setTraceEval(result.eval)} + className="text-primary underline-offset-2 hover:underline" + > + View span tree + + } + /> .json" -> asset URL string +// (the JSON payload is NOT inlined — only its URL), so the aggregate bundle +// stays lean and a trace is fetched only when its row is opened. +const traceUrls = import.meta.glob("../data/traces/*.json", { + eager: true, + query: "?url", + import: "default", +}) as Record + +const TONE_CLASS: Record = { + success: + "bg-agentprism-success-muted text-agentprism-success-muted-foreground", + error: "bg-agentprism-error-muted text-agentprism-error-muted-foreground", + warning: + "bg-agentprism-warning-muted text-agentprism-warning-muted-foreground", + neutral: + "bg-agentprism-badge-default text-agentprism-badge-default-foreground", +} + +function traceUrlFor(evalId: string): string | undefined { + return traceUrls[`../data/traces/${evalId}.json`] +} + +export function TracePanel({ + evalId, + onClose, +}: { + evalId: string + onClose: () => void +}): ReactElement { + const [data, setData] = useState(null) + const [error, setError] = useState(null) + + useEffect(() => { + let cancelled = false + setData(null) + setError(null) + const url = traceUrlFor(evalId) + if (!url) { + setError("No trace recorded for this eval.") + return + } + fetch(url) + .then((res) => { + if (!res.ok) throw new Error(`HTTP ${res.status}`) + return res.json() as Promise + }) + .then((payload) => { + if (!cancelled) { + setData({ ...payload, spans: payload.spans.map(reviveSpanDates) }) + } + }) + .catch((e: unknown) => { + if (!cancelled) setError(e instanceof Error ? e.message : String(e)) + }) + return () => { + cancelled = true + } + }, [evalId]) + + // Close on Escape. + useEffect(() => { + const onKey = (e: KeyboardEvent) => { + if (e.key === "Escape") onClose() + } + window.addEventListener("keydown", onKey) + return () => window.removeEventListener("keydown", onKey) + }, [onClose]) + + const badges: BadgeProps[] = (data?.badges ?? []).map((b) => ({ + label: b.label, + size: "5", + className: cn(TONE_CLASS[b.tone]), + })) + + return ( +
    +
    +
    + + {evalId} + + {badges.length > 0 ? ( + + {badges.map((b) => ( + + ))} + + ) : null} +
    + +
    +
    + {error ? ( +
    + {error} +
    + ) : data ? ( + + ) : ( +
    + Loading trace… +
    + )} +
    +
    + ) +} diff --git a/apps/web/src/data/traces/build-cli-001-bootstrap-app.json b/apps/web/src/data/traces/build-cli-001-bootstrap-app.json new file mode 100644 index 00000000..f9f16059 --- /dev/null +++ b/apps/web/src/data/traces/build-cli-001-bootstrap-app.json @@ -0,0 +1,343 @@ +{ + "traceRecord": { + "id": "build-cli-001-bootstrap-app", + "name": "build-cli-001-bootstrap-app", + "spansCount": 27, + "durationMs": 88036, + "agentDescription": "gpt-5.6-luna", + "totalTokens": 40452 + }, + "spans": [ + { + "id": "eval:build-cli-001-bootstrap-app", + "title": "build-cli-001-bootstrap-app", + "startTime": "2026-08-01T15:55:55.147Z", + "endTime": "2026-08-01T15:57:23.183Z", + "duration": 88036, + "type": "agent_invocation", + "status": "success", + "raw": "Implemented the Supabase development setup:\n\n- Added migration for `public.todos`\n- Enabled RLS\n- Allowed `SELECT` only for authenticated users\n- Denied anonymous access and all API writes\n- Added two sample todos in `supabase/seed.sql`\n- Started and reset the local Supabase stack\n- Verified API behavior:\n - Signed out: `401`\n - Signed in read: `200` with todos\n - Signed in write: `403`", + "output": "Implemented the Supabase development setup:\n\n- Added migration for `public.todos`\n- Enabled RLS\n- Allowed `SELECT` only for authenticated users\n- Denied anonymous access and all API writes\n- Added two sample todos in `supabase/seed.sql`\n- Started and reset the local Supabase stack\n- Verified API behavior:\n - Signed out: `401`\n - Signed in read: `200` with todos\n - Signed in write: `403`", + "children": [ + { + "id": "build-cli-001-bootstrap-app-msg-1", + "title": "System", + "startTime": "2026-08-01T15:55:55.147Z", + "endTime": "2026-08-01T15:55:55.147Z", + "duration": 0, + "type": "event", + "status": "success", + "raw": "You are an agent solving a Supabase eval task in a Linux workspace. Use the provided tools to inspect and modify the workspace and run commands. When you are done, end your turn with a short summary of what you did.\n\nThe Supabase CLI (`supabase`), docker, psql, git, and curl are installed in the workspace. Use the bash tool to run commands (the working directory is always the workspace root) and the files tools to inspect and modify files. Services started with `supabase start` are reachable on their default 127.0.0.1 ports.\n\n## Available skills\n\nThe following agent skills are installed in this workspace under `.claude/skills/`. Only their names and descriptions are shown — the full instructions are not loaded yet. When a task matches a skill, read `.claude/skills//SKILL.md` with the `files_read` tool for its full instructions, then read any files it references in that directory with `files_read` or `bash`.\n\n- supabase: Use when doing ANY task involving Supabase. Triggers: Supabase products (Database, Auth, Edge Functions, Realtime, Storage, Vectors, Cron, Queues); client libraries and SSR integrations (supabase-js, @supabase/ssr) in Next.js, React, SvelteKit, Astro, Remix; auth issues (login, logout, sessions, JWT, cookies, getSession, getUser, getClaims, RLS); Supabase CLI or MCP server; schema changes, migrations, declarative schemas, security audits, Postgres extensions (pg_graphql, pg_cron, pg_vector).\n- supabase-postgres-best-practices: Postgres best practices maintained by Supabase, for Postgres running anywhere. Load this skill BEFORE writing or changing anything that lives in a Postgres database: creating or altering tables and columns (including choosing column types), schema design, migrations and declarative schema files, RLS policies and the tests that verify them, indexes, triggers, database functions, queues and scheduled jobs (pg_cron, pgmq), vector/semantic search (pgvector), and restoring dumps (pg_restore) or importing data. Also load it when diagnosing slow queries, high CPU, timeouts, EXPLAIN plans, connection exhaustion, locking, bloat, or rows visible to the wrong user or tenant. This is not just a performance guide — schema, migration, security, and SQL authoring tasks need these rules too, even for a one-column change or a single query.", + "input": "You are an agent solving a Supabase eval task in a Linux workspace. Use the provided tools to inspect and modify the workspace and run commands. When you are done, end your turn with a short summary of what you did.\n\nThe Supabase CLI (`supabase`), docker, psql, git, and curl are installed in the workspace. Use the bash tool to run commands (the working directory is always the workspace root) and the files tools to inspect and modify files. Services started with `supabase start` are reachable on their default 127.0.0.1 ports.\n\n## Available skills\n\nThe following agent skills are installed in this workspace under `.claude/skills/`. Only their names and descriptions are shown — the full instructions are not loaded yet. When a task matches a skill, read `.claude/skills//SKILL.md` with the `files_read` tool for its full instructions, then read any files it references in that directory with `files_read` or `bash`.\n\n- supabase: Use when doing ANY task involving Supabase. Triggers: Supabase products (Database, Auth, Edge Functions, Realtime, Storage, Vectors, Cron, Queues); client libraries and SSR integrations (supabase-js, @supabase/ssr) in Next.js, React, SvelteKit, Astro, Remix; auth issues (login, logout, sessions, JWT, cookies, getSession, getUser, getClaims, RLS); Supabase CLI or MCP server; schema changes, migrations, declarative schemas, security audits, Postgres extensions (pg_graphql, pg_cron, pg_vector).\n- supabase-postgres-best-practices: Postgres best practices maintained by Supabase, for Postgres running anywhere. Load this skill BEFORE writing or changing anything that lives in a Postgres database: creating or altering tables and columns (including choosing column types), schema design, migrations and declarative schema files, RLS policies and the tests that verify them, indexes, triggers, database functions, queues and scheduled jobs (pg_cron, pgmq), vector/semantic search (pgvector), and restoring dumps (pg_restore) or importing data. Also load it when diagnosing slow queries, high CPU, timeouts, EXPLAIN plans, connection exhaustion, locking, bloat, or rows visible to the wrong user or tenant. This is not just a performance guide — schema, migration, security, and SQL authoring tasks need these rules too, even for a one-column change or a single query." + }, + { + "id": "build-cli-001-bootstrap-app-msg-2", + "title": "User", + "startTime": "2026-08-01T15:55:55.147Z", + "endTime": "2026-08-01T15:55:55.147Z", + "duration": 0, + "type": "event", + "status": "success", + "raw": "We're kicking off a todos app and I want the Supabase side ready for the team\nto build on. Set it up the way we'd run it in development, with schema changes\ntracked as migrations so they can be reviewed and replayed.\n\nFor the first slice we just need a `todos` table. Todos aren't public: anyone\nsigned in can read all of them, but nothing should be writable through the API\nfor now. Add a couple of sample todos so there's something to look at.\n\nBefore you hand it back, make sure the running API actually behaves that way —\nsigned-in users get the todos, signed-out requests get nothing.", + "input": "We're kicking off a todos app and I want the Supabase side ready for the team\nto build on. Set it up the way we'd run it in development, with schema changes\ntracked as migrations so they can be reviewed and replayed.\n\nFor the first slice we just need a `todos` table. Todos aren't public: anyone\nsigned in can read all of them, but nothing should be writable through the API\nfor now. Add a couple of sample todos so there's something to look at.\n\nBefore you hand it back, make sure the running API actually behaves that way —\nsigned-in users get the todos, signed-out requests get nothing." + }, + { + "id": "build-cli-001-bootstrap-app-tool-3", + "title": "Tool: files_read", + "startTime": "2026-08-01T15:55:55.147Z", + "endTime": "2026-08-01T15:55:55.148Z", + "duration": 1, + "type": "tool_execution", + "status": "success", + "raw": "{\n \"contents\": \"---\\nname: supabase\\ndescription: \\\"Use when doing ANY task involving Supabase. Triggers: Supabase products (Database, Auth, Edge Functions, Realtime, Storage, Vectors, Cron, Queues); client libraries and SSR integrations (supabase-js, @supabase/ssr) in Next.js, React, SvelteKit, Astro, Remix; auth issues (login, logout, sessions, JWT, cookies, getSession, getUser, getClaims, RLS); Supabase CLI or MCP server; schema changes, migrations, declarative schemas, security audits, Postgres extensions (pg_graphql, pg_cron, pg_vector).\\\"\\nmetadata:\\n author: supabase\\n version: \\\"0.1.2\\\"\\n---\\n\\n# Supabase\\n\\n## Core Principles\\n\\n**1. Supabase changes frequently — verify against changelog and current docs before implementing.**\\nDo not rely on training data for Supabase features. Function signatures, config.toml settings, and API conventions change between versions.\\n\\nFirst, fetch `https://supabase.com/changelog.md` (a lightweight summary index — not a heavy pull), scan for `breaking-change` tags relevant to your task, and follow the linked page for any that apply. Then look up the relevant topic using the documentation access methods below.\\n\\n**2. Verify your work.**\\nAfter implementing any fix, run a test query to confirm the change works. A fix without verification is incomplete.\\n\\n**3. Recover from errors, don't loop.**\\nIf an approach fails after 2-3 attempts, stop and reconsider. Try a different method, check documentation, inspect the error more carefully, and review relevant logs when available. Supabase issues are not always solved by retrying the same command, and the answer is not always in the logs, but logs are often worth checking before proceeding.\\n\\n**4. Exposing tables to the Data API:** Depending on the user's [Data API settings](https://supabase.com/dashboard/project//integrations/data_api/settings), newly created tables may not be automatically exposed via the Data (REST) API. If this is the case, `anon` and `authenticated` roles will need to be explicitly granted access.\\n\\n> Note that this is separate from RLS, which controls which _rows_ are visible once a table is accessible, not whether the table is accessible at all.\\n\\nWhen a user reports a SQL-created table is unexpectedly inaccessible, check their Data API settings and whether the roles have been granted access via explicit `GRANT` SQL. When granting public (`anon`/`authenticated`) access, always enable RLS too. See [Exposing a Table to the Data API](https://supabase.com/docs/guides/api/securing-your-api.md) for the full setup workflow.\\n\\n**5. RLS in exposed schemas.**\\nEnable RLS on every table in any exposed schema, which includes `public` by default. This is critical in Supabase because tables in exposed schemas can be reachable through the Data API when the `anon`/`authenticated` roles have access (see [Exposing a Table to the Data API](https://supabase.com/docs/guides/api/securing-your-api.md)). For private schemas, prefer RLS as defense in depth. After enabling RLS, create policies that match the actual access model rather than defaulting every table to the same `auth.uid()` pattern.\\n\\n**6. Security checklist.**\\nWhen working on any Supabase task that touches auth, RLS, views, storage, or user data, run through this checklist. These are Supabase-specific security traps that silently create vulnerabilities:\\n\\n- **Auth and session security**\\n - **Never use `user_metadata` claims in JWT-based authorization decisions.** In Supabase, `raw_user_meta_data` is user-editable and can appear in `auth.jwt()`, so it is unsafe for RLS policies or any other authorization logic. Store authorization data in `raw_app_meta_data` / `app_metadata` instead.\\n - **Deleting a user does not invalidate existing access tokens.** Sign out or revoke sessions first, keep JWT expiry short for sensitive apps, and for strict guarantees validate `session_id` against `auth.sessions` on sensitive operations.\\n - **If you use `app_metadata` or `auth.jwt()` for authorization, remember JWT claims are not always fresh until the user's token is refreshed.**\\n\\n- **API key and client exposure**\\n - **Never expose the `service_role` or secret key in public clients.** Prefer publishable keys for frontend code. Legacy `anon` keys are only for compatibility. In Next.js, any `NEXT_PUBLIC_` env var is sent to the browser.\\n\\n- **RLS, views, and privileged database code**\\n - **Views bypass RLS by default.** In Postgres 15 and above, use `CREATE VIEW ... WITH (security_invoker = true)`. In older versions of Postgres, protect your views by revoking access from the `anon` and `authenticated` roles, or by putting them in an unexposed schema.\\n - **UPDATE requires a SELECT policy.** In Postgres RLS, an UPDATE needs to first SELECT the row. Without a SELECT policy, updates silently return 0 rows — no error, just no change.\\n - **`auth.role()` is deprecated — use the `TO` clause instead.** Supabase has deprecated `auth.role()` in favour of specifying the target role directly on the policy with `TO authenticated` or `TO anon`. Beyond deprecation, `auth.role() = 'authenticated'` breaks silently when anonymous sign-ins are enabled, because anonymous users carry the `authenticated` Postgres role and pass the check regardless of whether the user is genuinely signed in.\\n ```sql\\n -- Deprecated (do not use)\\n create policy \\\"example\\\" on table_name for select\\n using ( auth.role() = 'authenticated' );\\n ```\\n - **`TO authenticated` alone is authentication without authorization (BOLA / IDOR).** Using `TO authenticated` only checks the role — it does not restrict which rows a user can access. The correct pattern combines `TO authenticated` with an ownership predicate in `USING`:\\n ```sql\\n create policy \\\"example\\\" on table_name for select\\n to authenticated\\n using ( (select auth.uid()) = user_id );\\n ```\\n - **UPDATE policies require both `USING` and `WITH CHECK`.** Without `WITH CHECK`, a user can reassign a row's `user_id` to another user:\\n ```sql\\n create policy \\\"example\\\" on table_name for update\\n to authenticated\\n using ( (select auth.uid()) = user_id )\\n with check ( (select auth.uid()) = user_id );\\n ```\\n - **`SECURITY DEFINER` functions bypass RLS.** A `SECURITY DEFINER` function runs with its creator's privileges — typically a role with `bypassrls` (e.g., `postgres`). Never add `SECURITY DEFINER` to resolve a permission error; it silently removes access control without fixing the underlying cause. Prefer `SECURITY INVOKER`.\\n - **`SECURITY DEFINER` functions in `public` are callable by all roles.** Postgres grants `EXECUTE` to `PUBLIC` by default for every new function, so any `SECURITY DEFINER` function in `public` is a public API endpoint callable by `anon` and `authenticated` (which inherit from `PUBLIC`) without any additional grant. When `SECURITY DEFINER` is genuinely needed (e.g., bypassing RLS on an internal lookup table), keep the function in a non-exposed schema, always include an `auth.uid()` check in the function body, and run `supabase db advisors` after making changes.\\n\\n- **Storage access control**\\n - **Storage upsert requires INSERT + SELECT + UPDATE.** Granting only INSERT allows new uploads but file replacement (upsert) silently fails. You need all three.\\n\\n- **Dependency and supply-chain security**\\n - **Always pin package versions and commit lockfiles** when installing Supabase packages (`supabase-js`, `@supabase/ssr`, `supabase-py`, etc.). See the [npm security guide](https://supabase.com/docs/guides/security/npm-security.md) for the full checklist.\\n\\nFor any security concern not covered above, fetch the Supabase product security index: `https://supabase.com/docs/guides/security/product-security.md`\\n\\n## Supabase CLI\\n\\nAlways discover commands via `--help` — never guess. The CLI structure changes between versions.\\n\\n```bash\\nsupabase --help # All top-level commands\\nsupabase --help # Subcommands (e.g., supabase db --help)\\nsupabase --help # Flags for a specific command\\n```\\n\\n**Supabase CLI Known gotchas:**\\n\\n- `supabase db query` requires **CLI v2.79.0+** → use MCP `execute_sql` or `psql` as fallback\\n- `supabase db advisors` requires **CLI v2.81.3+** → use MCP `get_advisors` as fallback\\n- In imperative migration projects, create new hand-authored migration files with `supabase migration new ` first. Never invent a migration filename or rely on memory for the expected format. Declarative schema projects generate migrations from `supabase/schemas/`; see \\\"Making and Committing Schema Changes\\\" below.\\n\\n**Version check and upgrade:** Run `supabase --version` to check. For CLI changelogs and version-specific features, consult the [CLI documentation](https://supabase.com/docs/reference/cli/introduction) or [GitHub releases](https://github.com/supabase/cli/releases).\\n\\n## Supabase MCP Server\\n\\nFor setup instructions, server URL, and configuration, see the [MCP setup guide](https://supabase.com/docs/guides/getting-started/mcp).\\n\\n**Troubleshooting connection issues** — follow these steps in order:\\n\\n1. **Check if the server is reachable:**\\n `curl -so /dev/null -w \\\"%{http_code}\\\" https://mcp.supabase.com/mcp`\\n A `401` is expected (no token) and means the server is up. Timeout or \\\"connection refused\\\" means it may be down.\\n\\n2. **Check `.mcp.json` configuration:**\\n Verify the project root has a valid `.mcp.json` with the correct server URL. If missing, create one pointing to `https://mcp.supabase.com/mcp`.\\n\\n3. **Authenticate the MCP server:**\\n If the server is reachable and `.mcp.json` is correct but tools aren't visible, the user needs to authenticate. The Supabase MCP server uses OAuth 2.1 — tell the user to trigger the auth flow in their agent, complete it in the browser, and reload the session.\\n\\n## Supabase Documentation\\n\\nBefore implementing any Supabase feature, find the relevant documentation. Use these methods in priority order:\\n\\n1. **MCP `search_docs` tool** (preferred — returns relevant snippets directly)\\n2. **Fetch docs pages as markdown** — any docs page can be fetched by appending `.md` to the URL path.\\n3. **Web search** for Supabase-specific topics when you don't know which page to look at.\\n\\n## Making and Committing Schema Changes\\n\\nFirst decide which schema workflow the project uses.\\n\\n### Option A: Declarative schemas\\n\\nUse this when `supabase/schemas/` exists or `config.toml` sets `schema_paths`. Edit the desired schema state in those files, then generate and review the migration. Do not start by hand-writing a migration. See the [Declarative database schemas guide](https://supabase.com/docs/guides/local-development/declarative-database-schemas).\\n\\n### Option B: Imperative migrations\\n\\nUse this when the project does not use declarative schemas.\\n\\n**To make schema changes, use `execute_sql` (MCP) or `supabase db query` (CLI).** These run SQL directly on the database without creating migration history entries, so you can iterate freely and generate a clean migration when ready.\\n\\nDo NOT use `apply_migration` to change a local database schema — it writes a migration history entry on every call, which means you can't iterate, and `supabase db diff` / `supabase db pull` will produce empty or conflicting diffs. If you use it, you'll be stuck with whatever SQL you passed on the first try.\\n\\n**When ready to commit** your changes to a migration file:\\n\\n1. **Run advisors** → `supabase db advisors` (CLI v2.81.3+) or MCP `get_advisors`. Fix any issues.\\n2. **Review the Security Checklist above** if your changes involve views, functions, triggers, or storage.\\n3. **Generate the migration** → `supabase db pull --local --yes`\\n4. **Verify** → `supabase migration list --local`\\n\\n## Reference Guides\\n\\n- **Skill Feedback** → [references/skill-feedback.md](references/skill-feedback.md)\\n **MUST read when** the user reports that this skill gave incorrect guidance or is missing information.\\n\"\n}", + "input": "{\n \"path\": \".claude/skills/supabase/SKILL.md\"\n}", + "output": "{\n \"contents\": \"---\\nname: supabase\\ndescription: \\\"Use when doing ANY task involving Supabase. Triggers: Supabase products (Database, Auth, Edge Functions, Realtime, Storage, Vectors, Cron, Queues); client libraries and SSR integrations (supabase-js, @supabase/ssr) in Next.js, React, SvelteKit, Astro, Remix; auth issues (login, logout, sessions, JWT, cookies, getSession, getUser, getClaims, RLS); Supabase CLI or MCP server; schema changes, migrations, declarative schemas, security audits, Postgres extensions (pg_graphql, pg_cron, pg_vector).\\\"\\nmetadata:\\n author: supabase\\n version: \\\"0.1.2\\\"\\n---\\n\\n# Supabase\\n\\n## Core Principles\\n\\n**1. Supabase changes frequently — verify against changelog and current docs before implementing.**\\nDo not rely on training data for Supabase features. Function signatures, config.toml settings, and API conventions change between versions.\\n\\nFirst, fetch `https://supabase.com/changelog.md` (a lightweight summary index — not a heavy pull), scan for `breaking-change` tags relevant to your task, and follow the linked page for any that apply. Then look up the relevant topic using the documentation access methods below.\\n\\n**2. Verify your work.**\\nAfter implementing any fix, run a test query to confirm the change works. A fix without verification is incomplete.\\n\\n**3. Recover from errors, don't loop.**\\nIf an approach fails after 2-3 attempts, stop and reconsider. Try a different method, check documentation, inspect the error more carefully, and review relevant logs when available. Supabase issues are not always solved by retrying the same command, and the answer is not always in the logs, but logs are often worth checking before proceeding.\\n\\n**4. Exposing tables to the Data API:** Depending on the user's [Data API settings](https://supabase.com/dashboard/project//integrations/data_api/settings), newly created tables may not be automatically exposed via the Data (REST) API. If this is the case, `anon` and `authenticated` roles will need to be explicitly granted access.\\n\\n> Note that this is separate from RLS, which controls which _rows_ are visible once a table is accessible, not whether the table is accessible at all.\\n\\nWhen a user reports a SQL-created table is unexpectedly inaccessible, check their Data API settings and whether the roles have been granted access via explicit `GRANT` SQL. When granting public (`anon`/`authenticated`) access, always enable RLS too. See [Exposing a Table to the Data API](https://supabase.com/docs/guides/api/securing-your-api.md) for the full setup workflow.\\n\\n**5. RLS in exposed schemas.**\\nEnable RLS on every table in any exposed schema, which includes `public` by default. This is critical in Supabase because tables in exposed schemas can be reachable through the Data API when the `anon`/`authenticated` roles have access (see [Exposing a Table to the Data API](https://supabase.com/docs/guides/api/securing-your-api.md)). For private schemas, prefer RLS as defense in depth. After enabling RLS, create policies that match the actual access model rather than defaulting every table to the same `auth.uid()` pattern.\\n\\n**6. Security checklist.**\\nWhen working on any Supabase task that touches auth, RLS, views, storage, or user data, run through this checklist. These are Supabase-specific security traps that silently create vulnerabilities:\\n\\n- **Auth and session security**\\n - **Never use `user_metadata` claims in JWT-based authorization decisions.** In Supabase, `raw_user_meta_data` is user-editable and can appear in `auth.jwt()`, so it is unsafe for RLS policies or any other authorization logic. Store authorization data in `raw_app_meta_data` / `app_metadata` instead.\\n - **Deleting a user does not invalidate existing access tokens.** Sign out or revoke sessions first, keep JWT expiry short for sensitive apps, and for strict guarantees validate `session_id` against `auth.sessions` on sensitive operations.\\n - **If you use `app_metadata` or `auth.jwt()` for authorization, remember JWT claims are not always fresh until the user's token is refreshed.**\\n\\n- **API key and client exposure**\\n - **Never expose the `service_role` or secret key in public clients.** Prefer publishable keys for frontend code. Legacy `anon` keys are only for compatibility. In Next.js, any `NEXT_PUBLIC_` env var is sent to the browser.\\n\\n- **RLS, views, and privileged database code**\\n - **Views bypass RLS by default.** In Postgres 15 and above, use `CREATE VIEW ... WITH (security_invoker = true)`. In older versions of Postgres, protect your views by revoking access from the `anon` and `authenticated` roles, or by putting them in an unexposed schema.\\n - **UPDATE requires a SELECT policy.** In Postgres RLS, an UPDATE needs to first SELECT the row. Without a SELECT policy, updates silently return 0 rows — no error, just no change.\\n - **`auth.role()` is deprecated — use the `TO` clause instead.** Supabase has deprecated `auth.role()` in favour of specifying the target role directly on the policy with `TO authenticated` or `TO anon`. Beyond deprecation, `auth.role() = 'authenticated'` breaks silently when anonymous sign-ins are enabled, because anonymous users carry the `authenticated` Postgres role and pass the check regardless of whether the user is genuinely signed in.\\n ```sql\\n -- Deprecated (do not use)\\n create policy \\\"example\\\" on table_name for select\\n using ( auth.role() = 'authenticated' );\\n ```\\n - **`TO authenticated` alone is authentication without authorization (BOLA / IDOR).** Using `TO authenticated` only checks the role — it does not restrict which rows a user can access. The correct pattern combines `TO authenticated` with an ownership predicate in `USING`:\\n ```sql\\n create policy \\\"example\\\" on table_name for select\\n to authenticated\\n using ( (select auth.uid()) = user_id );\\n ```\\n - **UPDATE policies require both `USING` and `WITH CHECK`.** Without `WITH CHECK`, a user can reassign a row's `user_id` to another user:\\n ```sql\\n create policy \\\"example\\\" on table_name for update\\n to authenticated\\n using ( (select auth.uid()) = user_id )\\n with check ( (select auth.uid()) = user_id );\\n ```\\n - **`SECURITY DEFINER` functions bypass RLS.** A `SECURITY DEFINER` function runs with its creator's privileges — typically a role with `bypassrls` (e.g., `postgres`). Never add `SECURITY DEFINER` to resolve a permission error; it silently removes access control without fixing the underlying cause. Prefer `SECURITY INVOKER`.\\n - **`SECURITY DEFINER` functions in `public` are callable by all roles.** Postgres grants `EXECUTE` to `PUBLIC` by default for every new function, so any `SECURITY DEFINER` function in `public` is a public API endpoint callable by `anon` and `authenticated` (which inherit from `PUBLIC`) without any additional grant. When `SECURITY DEFINER` is genuinely needed (e.g., bypassing RLS on an internal lookup table), keep the function in a non-exposed schema, always include an `auth.uid()` check in the function body, and run `supabase db advisors` after making changes.\\n\\n- **Storage access control**\\n - **Storage upsert requires INSERT + SELECT + UPDATE.** Granting only INSERT allows new uploads but file replacement (upsert) silently fails. You need all three.\\n\\n- **Dependency and supply-chain security**\\n - **Always pin package versions and commit lockfiles** when installing Supabase packages (`supabase-js`, `@supabase/ssr`, `supabase-py`, etc.). See the [npm security guide](https://supabase.com/docs/guides/security/npm-security.md) for the full checklist.\\n\\nFor any security concern not covered above, fetch the Supabase product security index: `https://supabase.com/docs/guides/security/product-security.md`\\n\\n## Supabase CLI\\n\\nAlways discover commands via `--help` — never guess. The CLI structure changes between versions.\\n\\n```bash\\nsupabase --help # All top-level commands\\nsupabase --help # Subcommands (e.g., supabase db --help)\\nsupabase --help # Flags for a specific command\\n```\\n\\n**Supabase CLI Known gotchas:**\\n\\n- `supabase db query` requires **CLI v2.79.0+** → use MCP `execute_sql` or `psql` as fallback\\n- `supabase db advisors` requires **CLI v2.81.3+** → use MCP `get_advisors` as fallback\\n- In imperative migration projects, create new hand-authored migration files with `supabase migration new ` first. Never invent a migration filename or rely on memory for the expected format. Declarative schema projects generate migrations from `supabase/schemas/`; see \\\"Making and Committing Schema Changes\\\" below.\\n\\n**Version check and upgrade:** Run `supabase --version` to check. For CLI changelogs and version-specific features, consult the [CLI documentation](https://supabase.com/docs/reference/cli/introduction) or [GitHub releases](https://github.com/supabase/cli/releases).\\n\\n## Supabase MCP Server\\n\\nFor setup instructions, server URL, and configuration, see the [MCP setup guide](https://supabase.com/docs/guides/getting-started/mcp).\\n\\n**Troubleshooting connection issues** — follow these steps in order:\\n\\n1. **Check if the server is reachable:**\\n `curl -so /dev/null -w \\\"%{http_code}\\\" https://mcp.supabase.com/mcp`\\n A `401` is expected (no token) and means the server is up. Timeout or \\\"connection refused\\\" means it may be down.\\n\\n2. **Check `.mcp.json` configuration:**\\n Verify the project root has a valid `.mcp.json` with the correct server URL. If missing, create one pointing to `https://mcp.supabase.com/mcp`.\\n\\n3. **Authenticate the MCP server:**\\n If the server is reachable and `.mcp.json` is correct but tools aren't visible, the user needs to authenticate. The Supabase MCP server uses OAuth 2.1 — tell the user to trigger the auth flow in their agent, complete it in the browser, and reload the session.\\n\\n## Supabase Documentation\\n\\nBefore implementing any Supabase feature, find the relevant documentation. Use these methods in priority order:\\n\\n1. **MCP `search_docs` tool** (preferred — returns relevant snippets directly)\\n2. **Fetch docs pages as markdown** — any docs page can be fetched by appending `.md` to the URL path.\\n3. **Web search** for Supabase-specific topics when you don't know which page to look at.\\n\\n## Making and Committing Schema Changes\\n\\nFirst decide which schema workflow the project uses.\\n\\n### Option A: Declarative schemas\\n\\nUse this when `supabase/schemas/` exists or `config.toml` sets `schema_paths`. Edit the desired schema state in those files, then generate and review the migration. Do not start by hand-writing a migration. See the [Declarative database schemas guide](https://supabase.com/docs/guides/local-development/declarative-database-schemas).\\n\\n### Option B: Imperative migrations\\n\\nUse this when the project does not use declarative schemas.\\n\\n**To make schema changes, use `execute_sql` (MCP) or `supabase db query` (CLI).** These run SQL directly on the database without creating migration history entries, so you can iterate freely and generate a clean migration when ready.\\n\\nDo NOT use `apply_migration` to change a local database schema — it writes a migration history entry on every call, which means you can't iterate, and `supabase db diff` / `supabase db pull` will produce empty or conflicting diffs. If you use it, you'll be stuck with whatever SQL you passed on the first try.\\n\\n**When ready to commit** your changes to a migration file:\\n\\n1. **Run advisors** → `supabase db advisors` (CLI v2.81.3+) or MCP `get_advisors`. Fix any issues.\\n2. **Review the Security Checklist above** if your changes involve views, functions, triggers, or storage.\\n3. **Generate the migration** → `supabase db pull --local --yes`\\n4. **Verify** → `supabase migration list --local`\\n\\n## Reference Guides\\n\\n- **Skill Feedback** → [references/skill-feedback.md](references/skill-feedback.md)\\n **MUST read when** the user reports that this skill gave incorrect guidance or is missing information.\\n\"\n}" + }, + { + "id": "build-cli-001-bootstrap-app-tool-4", + "title": "Tool: files_read", + "startTime": "2026-08-01T15:55:55.148Z", + "endTime": "2026-08-01T15:55:55.158Z", + "duration": 10, + "type": "tool_execution", + "status": "success", + "raw": "{\n \"contents\": \"---\\nname: supabase-postgres-best-practices\\ndescription: \\\"Postgres best practices maintained by Supabase, for Postgres running anywhere. Load this skill BEFORE writing or changing anything that lives in a Postgres database: creating or altering tables and columns (including choosing column types), schema design, migrations and declarative schema files, RLS policies and the tests that verify them, indexes, triggers, database functions, queues and scheduled jobs (pg_cron, pgmq), vector/semantic search (pgvector), and restoring dumps (pg_restore) or importing data. Also load it when diagnosing slow queries, high CPU, timeouts, EXPLAIN plans, connection exhaustion, locking, bloat, or rows visible to the wrong user or tenant. This is not just a performance guide — schema, migration, security, and SQL authoring tasks need these rules too, even for a one-column change or a single query.\\\"\\nlicense: MIT\\nmetadata:\\n author: supabase\\n version: \\\"1.1.1\\\"\\n organization: Supabase\\n date: January 2026\\n abstract: Comprehensive Postgres performance optimization guide for developers using Supabase and Postgres. Contains performance rules across 8 categories, prioritized by impact from critical (query performance, connection management) to incremental (advanced features). Each rule includes detailed explanations, incorrect vs. correct SQL examples, query plan analysis, and specific performance metrics to guide automated optimization and code generation.\\n---\\n\\n# Supabase Postgres Best Practices\\n\\nComprehensive performance optimization guide for Postgres, maintained by Supabase. Contains rules across 8 categories, prioritized by impact to guide automated query optimization and schema design.\\n\\n## When to Apply\\n\\nReference these guidelines when:\\n- Writing SQL queries or designing schemas\\n- Implementing indexes or query optimization\\n- Reviewing database performance issues\\n- Configuring connection pooling or scaling\\n- Optimizing for Postgres-specific features\\n- Working with Row-Level Security (RLS)\\n\\n## Rule Categories by Priority\\n\\n| Priority | Category | Impact | Prefix |\\n|----------|----------|--------|--------|\\n| 1 | Query Performance | CRITICAL | `query-` |\\n| 2 | Connection Management | CRITICAL | `conn-` |\\n| 3 | Security & RLS | CRITICAL | `security-` |\\n| 4 | Schema Design | HIGH | `schema-` |\\n| 5 | Concurrency & Locking | MEDIUM-HIGH | `lock-` |\\n| 6 | Data Access Patterns | MEDIUM | `data-` |\\n| 7 | Monitoring & Diagnostics | LOW-MEDIUM | `monitor-` |\\n| 8 | Advanced Features | LOW | `advanced-` |\\n\\n## How to Use\\n\\nRead individual rule files for detailed explanations and SQL examples:\\n\\n```\\nreferences/query-missing-indexes.md\\nreferences/query-partial-indexes.md\\nreferences/_sections.md\\n```\\n\\nEach rule file contains:\\n- Brief explanation of why it matters\\n- Incorrect SQL example with explanation\\n- Correct SQL example with explanation\\n- Optional EXPLAIN output or metrics\\n- Additional context and references\\n- Supabase-specific notes (when applicable)\\n\\n## References\\n\\n- https://www.postgresql.org/docs/current/\\n- https://supabase.com/docs\\n- https://wiki.postgresql.org/wiki/Performance_Optimization\\n- https://supabase.com/docs/guides/database/overview\\n- https://supabase.com/docs/guides/auth/row-level-security\\n\"\n}", + "input": "{\n \"path\": \".claude/skills/supabase-postgres-best-practices/SKILL.md\"\n}", + "output": "{\n \"contents\": \"---\\nname: supabase-postgres-best-practices\\ndescription: \\\"Postgres best practices maintained by Supabase, for Postgres running anywhere. Load this skill BEFORE writing or changing anything that lives in a Postgres database: creating or altering tables and columns (including choosing column types), schema design, migrations and declarative schema files, RLS policies and the tests that verify them, indexes, triggers, database functions, queues and scheduled jobs (pg_cron, pgmq), vector/semantic search (pgvector), and restoring dumps (pg_restore) or importing data. Also load it when diagnosing slow queries, high CPU, timeouts, EXPLAIN plans, connection exhaustion, locking, bloat, or rows visible to the wrong user or tenant. This is not just a performance guide — schema, migration, security, and SQL authoring tasks need these rules too, even for a one-column change or a single query.\\\"\\nlicense: MIT\\nmetadata:\\n author: supabase\\n version: \\\"1.1.1\\\"\\n organization: Supabase\\n date: January 2026\\n abstract: Comprehensive Postgres performance optimization guide for developers using Supabase and Postgres. Contains performance rules across 8 categories, prioritized by impact from critical (query performance, connection management) to incremental (advanced features). Each rule includes detailed explanations, incorrect vs. correct SQL examples, query plan analysis, and specific performance metrics to guide automated optimization and code generation.\\n---\\n\\n# Supabase Postgres Best Practices\\n\\nComprehensive performance optimization guide for Postgres, maintained by Supabase. Contains rules across 8 categories, prioritized by impact to guide automated query optimization and schema design.\\n\\n## When to Apply\\n\\nReference these guidelines when:\\n- Writing SQL queries or designing schemas\\n- Implementing indexes or query optimization\\n- Reviewing database performance issues\\n- Configuring connection pooling or scaling\\n- Optimizing for Postgres-specific features\\n- Working with Row-Level Security (RLS)\\n\\n## Rule Categories by Priority\\n\\n| Priority | Category | Impact | Prefix |\\n|----------|----------|--------|--------|\\n| 1 | Query Performance | CRITICAL | `query-` |\\n| 2 | Connection Management | CRITICAL | `conn-` |\\n| 3 | Security & RLS | CRITICAL | `security-` |\\n| 4 | Schema Design | HIGH | `schema-` |\\n| 5 | Concurrency & Locking | MEDIUM-HIGH | `lock-` |\\n| 6 | Data Access Patterns | MEDIUM | `data-` |\\n| 7 | Monitoring & Diagnostics | LOW-MEDIUM | `monitor-` |\\n| 8 | Advanced Features | LOW | `advanced-` |\\n\\n## How to Use\\n\\nRead individual rule files for detailed explanations and SQL examples:\\n\\n```\\nreferences/query-missing-indexes.md\\nreferences/query-partial-indexes.md\\nreferences/_sections.md\\n```\\n\\nEach rule file contains:\\n- Brief explanation of why it matters\\n- Incorrect SQL example with explanation\\n- Correct SQL example with explanation\\n- Optional EXPLAIN output or metrics\\n- Additional context and references\\n- Supabase-specific notes (when applicable)\\n\\n## References\\n\\n- https://www.postgresql.org/docs/current/\\n- https://supabase.com/docs\\n- https://wiki.postgresql.org/wiki/Performance_Optimization\\n- https://supabase.com/docs/guides/database/overview\\n- https://supabase.com/docs/guides/auth/row-level-security\\n\"\n}" + }, + { + "id": "build-cli-001-bootstrap-app-tool-5", + "title": "Tool: bash", + "startTime": "2026-08-01T15:55:55.158Z", + "endTime": "2026-08-01T15:55:57.202Z", + "duration": 2044, + "type": "tool_execution", + "status": "success", + "raw": "{\n \"exit_code\": 0,\n \"stdout\": \"/tmp/sandbox-0cf0bd36\\ntotal 4\\ndrwxr-xr-x 1 node node 1696 Aug 1 15:55 .\\ndrwxrwxrwt 1 root root 52 Aug 1 15:55 ..\\ndrwxr-xr-x 1 node node 96 Aug 1 15:55 .adal\\ndrwxr-xr-x 1 node node 96 Aug 1 15:55 .agents\\ndrwxr-xr-x 1 node node 96 Aug 1 15:55 .aider-desk\\ndrwxr-xr-x 1 node node 96 Aug 1 15:55 .augment\\ndrwxr-xr-x 1 node node 96 Aug 1 15:55 .autohand\\ndrwxr-xr-x 1 node node 96 Aug 1 15:55 .bob\\ndrwxr-xr-x 1 node node 96 Aug 1 15:55 .claude\\ndrwxr-xr-x 1 node node 96 Aug 1 15:55 .codeartsdoer\\ndrwxr-xr-x 1 node node 96 Aug 1 15:55 .codebuddy\\ndrwxr-xr-x 1 node node 96 Aug 1 15:55 .codemaker\\ndrwxr-xr-x 1 node node 96 Aug 1 15:55 .codestudio\\ndrwxr-xr-x 1 node node 96 Aug 1 15:55 .commandcode\\ndrwxr-xr-x 1 node node 96 Aug 1 15:55 .continue\\ndrwxr-xr-x 1 node node 96 Aug 1 15:55 .cortex\\ndrwxr-xr-x 1 node node 96 Aug 1 15:55 .crush\\ndrwxr-xr-x 1 node node 96 Aug 1 15:55 .devin\\ndrwxr-xr-x 1 node node 96 Aug 1 15:55 .factory\\ndrwxr-xr-x 1 node node 96 Aug 1 15:55 .forge\\ndrwxr-xr-x 1 node node 96 Aug 1 15:55 .goose\\ndrwxr-xr-x 1 node node 96 Aug 1 15:55 .hermes\\ndrwxr-xr-x 1 node node 96 Aug 1 15:55 .iflow\\ndrwxr-xr-x 1 node node 96 Aug 1 15:55 .inferencesh\\ndrwxr-xr-x 1 node node 96 Aug 1 15:55 .jazz\\ndrwxr-xr-x 1 node node 96 Aug 1 15:55 .junie\\ndrwxr-xr-x 1 node node 96 Aug 1 15:55 .kilocode\\ndrwxr-xr-x 1 node node 96 Aug 1 15:55 .kiro\\ndrwxr-xr-x 1 node node 96 Aug 1 15:55 .kode\\ndrwxr-xr-x 1 node node 96 Aug 1 15:55 .lingma\\ndrwxr-xr-x 1 node node 96 Aug 1 15:55 .mcpjam\\ndrwxr-xr-x 1 node node 96 Aug 1 15:55 .moxby\\ndrwxr-xr-x 1 node node 96 Aug 1 15:55 .mux\\ndrwxr-xr-x 1 node node 96 Aug 1 15:55 .neovate\\ndrwxr-xr-x 1 node node 96 Aug 1 15:55 .ona\\ndrwxr-xr-x 1 node node 96 Aug 1 15:55 .openhands\\ndrwxr-xr-x 1 node node 96 Aug 1 15:55 .pi\\ndrwxr-xr-x 1 node node 96 Aug 1 15:55 .pochi\\ndrwxr-xr-x 1 node node 96 Aug 1 15:55 .qoder\\ndrwxr-xr-x 1 node node 96 Aug 1 15:55 .qwen\\ndrwxr-xr-x 1 node node 96 Aug 1 15:55 .reasonix\\ndrwxr-xr-x 1 node node 96 Aug 1 15:55 .roo\\ndrwxr-xr-x 1 node node 96 Aug 1 15:55 .rovodev\\ndrwxr-xr-x 1 node node 96 Aug 1 15:55 .tabnine\\ndrwxr-xr-x 1 node node 96 Aug 1 15:55 .terramind\\ndrwxr-xr-x 1 node node 96 Aug 1 15:55 .tinycloud\\ndrwxr-xr-x 1 node node 96 Aug 1 15:55 .trae\\ndrwxr-xr-x 1 node node 96 Aug 1 15:55 .vibe\\ndrwxr-xr-x 1 node node 96 Aug 1 15:55 .windsurf\\ndrwxr-xr-x 1 node node 96 Aug 1 15:55 .zencoder\\ndrwxr-xr-x 1 node node 96 Aug 1 15:55 data\\ndrwxr-xr-x 1 node node 128 Aug 1 15:55 skills\\n-rw-r--r-- 1 node node 417 Aug 1 15:55 skills-lock.json\\n./skills-lock.json\\n./skills/supabase-postgres-best-practices/CHANGELOG.md\\n./skills/supabase-postgres-best-practices/SKILL.md\\n./skills/supabase/CHANGELOG.md\\n./skills/supabase/SKILL.md\\n\",\n \"stderr\": \"\"\n}", + "input": "{\n \"command\": \"pwd && ls -la && find . -maxdepth 3 -type f | sort | head -200\",\n \"timeout_sec\": 240\n}", + "output": "{\n \"exit_code\": 0,\n \"stdout\": \"/tmp/sandbox-0cf0bd36\\ntotal 4\\ndrwxr-xr-x 1 node node 1696 Aug 1 15:55 .\\ndrwxrwxrwt 1 root root 52 Aug 1 15:55 ..\\ndrwxr-xr-x 1 node node 96 Aug 1 15:55 .adal\\ndrwxr-xr-x 1 node node 96 Aug 1 15:55 .agents\\ndrwxr-xr-x 1 node node 96 Aug 1 15:55 .aider-desk\\ndrwxr-xr-x 1 node node 96 Aug 1 15:55 .augment\\ndrwxr-xr-x 1 node node 96 Aug 1 15:55 .autohand\\ndrwxr-xr-x 1 node node 96 Aug 1 15:55 .bob\\ndrwxr-xr-x 1 node node 96 Aug 1 15:55 .claude\\ndrwxr-xr-x 1 node node 96 Aug 1 15:55 .codeartsdoer\\ndrwxr-xr-x 1 node node 96 Aug 1 15:55 .codebuddy\\ndrwxr-xr-x 1 node node 96 Aug 1 15:55 .codemaker\\ndrwxr-xr-x 1 node node 96 Aug 1 15:55 .codestudio\\ndrwxr-xr-x 1 node node 96 Aug 1 15:55 .commandcode\\ndrwxr-xr-x 1 node node 96 Aug 1 15:55 .continue\\ndrwxr-xr-x 1 node node 96 Aug 1 15:55 .cortex\\ndrwxr-xr-x 1 node node 96 Aug 1 15:55 .crush\\ndrwxr-xr-x 1 node node 96 Aug 1 15:55 .devin\\ndrwxr-xr-x 1 node node 96 Aug 1 15:55 .factory\\ndrwxr-xr-x 1 node node 96 Aug 1 15:55 .forge\\ndrwxr-xr-x 1 node node 96 Aug 1 15:55 .goose\\ndrwxr-xr-x 1 node node 96 Aug 1 15:55 .hermes\\ndrwxr-xr-x 1 node node 96 Aug 1 15:55 .iflow\\ndrwxr-xr-x 1 node node 96 Aug 1 15:55 .inferencesh\\ndrwxr-xr-x 1 node node 96 Aug 1 15:55 .jazz\\ndrwxr-xr-x 1 node node 96 Aug 1 15:55 .junie\\ndrwxr-xr-x 1 node node 96 Aug 1 15:55 .kilocode\\ndrwxr-xr-x 1 node node 96 Aug 1 15:55 .kiro\\ndrwxr-xr-x 1 node node 96 Aug 1 15:55 .kode\\ndrwxr-xr-x 1 node node 96 Aug 1 15:55 .lingma\\ndrwxr-xr-x 1 node node 96 Aug 1 15:55 .mcpjam\\ndrwxr-xr-x 1 node node 96 Aug 1 15:55 .moxby\\ndrwxr-xr-x 1 node node 96 Aug 1 15:55 .mux\\ndrwxr-xr-x 1 node node 96 Aug 1 15:55 .neovate\\ndrwxr-xr-x 1 node node 96 Aug 1 15:55 .ona\\ndrwxr-xr-x 1 node node 96 Aug 1 15:55 .openhands\\ndrwxr-xr-x 1 node node 96 Aug 1 15:55 .pi\\ndrwxr-xr-x 1 node node 96 Aug 1 15:55 .pochi\\ndrwxr-xr-x 1 node node 96 Aug 1 15:55 .qoder\\ndrwxr-xr-x 1 node node 96 Aug 1 15:55 .qwen\\ndrwxr-xr-x 1 node node 96 Aug 1 15:55 .reasonix\\ndrwxr-xr-x 1 node node 96 Aug 1 15:55 .roo\\ndrwxr-xr-x 1 node node 96 Aug 1 15:55 .rovodev\\ndrwxr-xr-x 1 node node 96 Aug 1 15:55 .tabnine\\ndrwxr-xr-x 1 node node 96 Aug 1 15:55 .terramind\\ndrwxr-xr-x 1 node node 96 Aug 1 15:55 .tinycloud\\ndrwxr-xr-x 1 node node 96 Aug 1 15:55 .trae\\ndrwxr-xr-x 1 node node 96 Aug 1 15:55 .vibe\\ndrwxr-xr-x 1 node node 96 Aug 1 15:55 .windsurf\\ndrwxr-xr-x 1 node node 96 Aug 1 15:55 .zencoder\\ndrwxr-xr-x 1 node node 96 Aug 1 15:55 data\\ndrwxr-xr-x 1 node node 128 Aug 1 15:55 skills\\n-rw-r--r-- 1 node node 417 Aug 1 15:55 skills-lock.json\\n./skills-lock.json\\n./skills/supabase-postgres-best-practices/CHANGELOG.md\\n./skills/supabase-postgres-best-practices/SKILL.md\\n./skills/supabase/CHANGELOG.md\\n./skills/supabase/SKILL.md\\n\",\n \"stderr\": \"\"\n}" + }, + { + "id": "build-cli-001-bootstrap-app-tool-6", + "title": "Tool: search_docs", + "startTime": "2026-08-01T15:55:57.202Z", + "endTime": "2026-08-01T15:56:01.177Z", + "duration": 3975, + "type": "tool_execution", + "status": "success", + "raw": "{\n \"content\": [\n {\n \"type\": \"text\",\n \"text\": \"{\\\"result\\\":{\\\"searchDocs\\\":{\\\"nodes\\\":[{\\\"title\\\":\\\"Integrating With Supabase Auth\\\",\\\"href\\\":\\\"https://supabase.com/docs/guides/functions/auth-legacy-jwt\\\",\\\"content\\\":\\\"# Integrating With Supabase Auth\\\\n\\\\nIntegrate Supabase Auth with Edge Functions\\\\n\\\\nEdge Functions work with [Supabase Auth](/docs/guides/auth).\\\\n\\\\nThis allows you to:\\\\n\\\\n* Automatically identify users through Legacy JWT tokens\\\\n* Enforce Row Level Security policies\\\\n* Integrate with your existing auth flow\\\\n\\\\n\\\\n## Setting up auth context\\\\n\\\\nWhen a user makes a request to an Edge Function, you can use the `Authorization` header to set the Auth context in the Supabase client and enforce Row Level Security policies.\\\\n\\\\n```js\\\\nimport { createClient } from 'npm:@supabase/supabase-js@2'\\\\n\\\\nDeno.serve(async (req: Request) => {\\\\n const supabaseClient = createClient(\\\\n Deno.env.get('SUPABASE_URL') ?? '',\\\\n Deno.env.get('SUPABASE_ANON_KEY') ?? '',\\\\n // Create client with Auth context of the user that called the function.\\\\n // This way your row-level-security (RLS) policies are applied.\\\\n {\\\\n global: {\\\\n headers: { Authorization: req.headers.get('Authorization')! },\\\\n },\\\\n }\\\\n );\\\\n\\\\n //...\\\\n})\\\\n```\\\\n\\\\n\\\\n This context setting happens in the `Deno.serve()` callback argument, so that the `Authorization` header is set for each individual request scope.\\\\n\\\\n\\\\n***\\\\n\\\\n\\\\n## Fetching the user\\\\n\\\\nBy getting the JWT from the `Authorization` header, you can provide the token to `getUser()` to fetch the user object to obtain metadata for the logged in user.\\\\n\\\\n```js\\\\nDeno.serve(async (req: Request) => {\\\\n // ...\\\\n const authHeader = req.headers.get('Authorization')!\\\\n const token = authHeader.replace('Bearer ', '')\\\\n const { data } = await supabaseClient.auth.getUser(token)\\\\n // ...\\\\n})\\\\n```\\\\n\\\\n***\\\\n\\\\n\\\\n## Row Level Security\\\\n\\\\nAfter initializing a Supabase client with the Auth context, all queries will be executed with the context of the user. For database queries, this means [Row Level Security](/docs/guides/database/postgres/row-level-security) will be enforced.\\\\n\\\\n```js\\\\nimport { createClient } from 'npm:@supabase/supabase-js@2'\\\\n\\\\nDeno.serve(async (req: Request) => {\\\\n // ...\\\\n // This query respects RLS - users only see rows they have access to\\\\n const { data, error } = await supabaseClient.from('profiles').select('*');\\\\n\\\\n if (error) {\\\\n return new Response('Database error', { status: 500 })\\\\n }\\\\n\\\\n // ...\\\\n})\\\\n```\\\\n\\\\n***\\\\n\\\\n\\\\n## Example\\\\n\\\\nSee the full [example on GitHub](https://github.com/supabase/supabase/blob/master/examples/edge-functions/supabase/functions/select-from-table-with-auth-rls/index.ts).\\\\n\\\\n\\\\n ```typescript\\\\n // Follow this setup guide to integrate the Deno language server with your editor:\\\\n // https://deno.land/manual/getting_started/setup_your_environment\\\\n // This enables autocomplete, go to definition, etc.\\\\n\\\\n import { createClient } from 'npm:supabase-js@2'\\\\n // New approach (v2.95.0+)\\\\n import { corsHeaders } from 'jsr:@supabase/supabase-js@2/cors'\\\\n // For older versions:\\\\n // import { corsHeaders } from '../_shared/cors.ts'\\\\n\\\\n console.log(`Function \\\\\\\"select-from-table-with-auth-rls\\\\\\\" up and running!`)\\\\n\\\\n Deno.serve(async (req: Request) => {\\\\n // This is needed if you're planning to invoke your function from a browser.\\\\n if (req.method === 'OPTIONS') {\\\\n return new Response('ok', { headers: corsHeaders })\\\\n }\\\\n\\\\n try {\\\\n const SUPABASE_PUBLISHABLE_KEYS = JSON.parse(Deno.env.get('SUPABASE_PUBLISHABLE_KEYS')!)\\\\n // Create a Supabase client with the Auth context of the logged in user.\\\\n const supabaseClient = createClient(\\\\n // Supabase API URL - env var exported by default.\\\\n Deno.env.get('SUPABASE_URL') ?? '',\\\\n // Supabase API PUBLISHABLE KEY - env var exported by default.\\\\n Deno.env.get(SUPABASE_PUBLISHABLE_KEYS['default']) ?? '',\\\\n // Create client with Auth context of the user that called the function.\\\\n // This way your row-level-security (RLS) policies are applied.\\\\n {\\\\n global: {\\\\n headers: { Authorization: req.headers.get('Authorization')! },\\\\n },\\\\n }\\\\n )\\\\n\\\\n // First get the token from the Authorization header\\\\n const token = req.headers.get('Authorization').replace('Bearer ', '')\\\\n\\\\n // Now we can get the session or user object\\\\n const {\\\\n data: { user },\\\\n } = await supabaseClient.auth.getUser(token)\\\\n\\\\n // And we can run queries in the context of our authenticated user\\\\n const { data, error } = await supabaseClient.from('users').select('*')\\\\n if (error) throw error\\\\n\\\\n return new Response(JSON.stringify({ user, data }), {\\\\n headers: { ...corsHeaders, 'Content-Type': 'application/json' },\\\\n status: 200,\\\\n })\\\\n } catch (error) {\\\\n return new Response(JSON.stringify({ error: error.message }), {\\\\n headers: { ...corsHeaders, 'Content-Type': 'application/json' },\\\\n status: 400,\\\\n })\\\\n }\\\\n })\\\\n\\\\n // To invoke:\\\\n // curl -i --location --request POST 'http://localhost:54321/functions/v1/select-from-table-with-auth-rls' \\\\\\\\\\\\n // --header 'Authorization: Bearer REDACTED_JWT' \\\\\\\\\\\\n // --header 'Content-Type: application/json' \\\\\\\\\\\\n // --data '{\\\\\\\"name\\\\\\\":\\\\\\\"Functions\\\\\\\"}'\\\\n ```\\\\n\\\\n\\\"},{\\\"title\\\":\\\"Local development workflow\\\",\\\"href\\\":\\\"https://supabase.com/docs/guides/local-development/cli-workflows\\\",\\\"content\\\":\\\"# Local development workflow\\\\n\\\\nSet up and run your day-to-day local development workflow with the Supabase CLI.\\\\n\\\\nThis guide walks through two common starting points for local development with the Supabase CLI, and shows how they converge into the same daily workflow. By the end, you'll have a `./supabase` directory in your repo that anyone can clone to recreate the full project, locally or on a fresh remote instance.\\\\n\\\\nThere are two starting points, both leading to the same place: database schema and migrations tracked in version control, with seed data for local development.\\\\n\\\\n* **[Move an existing project to local development](#move-an-existing-project-to-local-development)**: you have a project on the Supabase platform and want to bring it into a proper local development workflow.\\\\n* **[Start a new project from scratch](#start-a-new-project-from-scratch)**: you're building locally and will eventually push to a remote instance.\\\\n\\\\n\\\\n## Before you begin\\\\n\\\\nYou need the Supabase CLI installed and a Docker-compatible runtime running. If you haven't set these up yet, see [Install and run the CLI](/docs/guides/local-development/cli/getting-started) for installation across macOS, Windows, and Linux, and for the details of what `supabase start` brings up and how to access each service.\\\\n\\\\nKeep in mind that **the local stack is for development only**. It is not hardened for production use and must never be exposed to external traffic. It has no TLS, no rate limiting, and default credentials. Use it to develop and test, then deploy to the [Supabase Platform](https://supabase.com) or a proper self-hosted setup for anything beyond that.\\\\n\\\\n\\\\n How you invoke the CLI depends on how you installed it:\\\\n\\\\n * Installed globally with **Homebrew or Scoop**: run `supabase `.\\\\n * Added as a **project dependency** with npm, pnpm, yarn, or bun: run it through your package runner instead, for example `npx supabase ` (or `pnpm supabase`, `yarn supabase`, `bunx supabase`).\\\\n\\\\n Every example in this guide is written as `supabase `. Translate it to whichever form matches your install. See [Install and run the CLI](/docs/guides/local-development/cli/getting-started) for the full setup.\\\\n\\\\n\\\\n\\\\n If you want a working project to explore rather than an empty one, `supabase bootstrap` scaffolds a starter application (Next.js, Flutter, and more) with schema, migrations, and config already wired up. It's an alternative entry point to `supabase init` when starting a new project from scratch.\\\\n\\\\n\\\\n\\\\n## The `./supabase` directory\\\\n\\\\nAfter `supabase init`, your project contains a `./supabase` directory. Here's what goes in it and what to commit:\\\\n\\\\n| Path | Purpose | Commit? |\\\\n| ---------------------- | ----------------------------------------------------------------- | ------- |\\\\n| `config.toml` | Local stack configuration (ports, auth settings, etc.) | Yes |\\\\n| `migrations/` | Timestamped SQL migration files, applied in order | Yes |\\\\n| `seed.sql` | Dev/test data, applied after migrations on `start` and `db reset` | Yes |\\\\n| `schemas/` | Declarative schema files (if using that approach) | Yes |\\\\n| `.temp/`, `.branches/` | CLI internal state | No |\\\\n\\\\nThe `config.toml` is safe to commit. It contains no secrets by default. If you add sensitive values (OAuth credentials, API keys), use the `env()` function to reference environment variables instead of hardcoding them. See [Managing config and secrets](/docs/guides/local-development/managing-config).\\\\n\\\\n\\\\n Many database commands accept `--local` and `--linked` flags to choose what they act on. The defaults are not the same across commands: `db diff` and `db reset` default to `--local`, while `db pull`, `db push`, and `db dump` default to `--linked`. When in doubt, pass the flag explicitly.\\\\n\\\\n\\\\n\\\\n## Move an existing project to local development\\\\n\\\\nYou've built a project on the Supabase platform, with tables created via the Dashboard, SQL editor, or client libraries. Now you want a local dev setup with everything in version control.\\\\n\\\\n\\\\n### Step 1: Initialize\\\\n\\\\nIn your project root:\\\\n\\\\n```bash\\\\nsupabase init\\\\n```\\\\n\\\\nThis creates `./supabase/config.toml`. If you already have a project directory with application code, run this at the root. The `supabase/` directory will sit alongside your app code.\\\\n\\\\n\\\\n### Step 2: Authenticate\\\\n\\\\n```bash\\\\nsupabase login\\\\n```\\\\n\\\\nOpens a browser to generate an access token. The token is stored locally and used for all subsequent CLI commands that interact with the platform.\\\\n\\\\n\\\\n### Step 3: Link to your remote project\\\\n\\\\n```bash\\\\nsupabase link --project-ref \\\\n```\\\\n\\\\nFind your project ID in the Supabase Dashboard URL: `https://supabase.com/dashboard/project/`.\\\\n\\\\nThis tells the CLI which remote project to connect to for `db pull`, `db push`, and other remote operations. You'll be prompted for the database password, which is the password set when you created the project.\\\\n\\\\n\\\\n### Step 4: Pull the remote schema\\\\n\\\\n```bash\\\\nsupabase db pull\\\\n```\\\\n\\\\nThis connects to your remote database, dumps the entire schema, and saves it as a migration file:\\\\n\\\\n```\\\\nsupabase/migrations/_remote_schema.sql\\\\n```\\\\n\\\\nThis initial migration is your baseline. It represents the current state of your database, and all future changes build on top of it. `db pull` also records this migration as already applied in the remote migration history (the `supabase_migrations.schema_migrations` table), so a later `db push` won't try to reapply it.\\\\n\\\\n\\\\n `db pull` diffs your remote database against the CLI's default local stack, so the generated file can include statements you didn't expect. A common example is `DROP EXTENSION pg_net;`, emitted when your remote project has an extension disabled that the local stack enables by default. These statements apply silently on `db reset` and change your local schema, so read the file before committing it. See [Cleaning up generated migrations](#cleaning-up-generated-migrations) for what to look for.\\\\n\\\\n\\\\n\\\\n If you also use Supabase Auth or Storage and have customized their schemas, pull them separately:\\\\n\\\\n ```bash\\\\n supabase db pull --schema auth -f pull-auth-schema\\\\n supabase db pull --schema storage -f pull-storage-schema\\\\n ```\\\\n\\\\n These schemas are managed by Supabase and typically don't need to be pulled unless you've made custom modifications.\\\\n\\\\n\\\\n\\\\n### Step 5: Create seed data\\\\n\\\\nYou have two options:\\\\n\\\\n**Option A: Dump existing data from remote** (then clean it up):\\\\n\\\\n```bash\\\\nsupabase db dump --data-only --linked > supabase/seed.sql\\\\n```\\\\n\\\\n\\\\n Review and clean up the dump before committing. Remove production user data, secrets, personal information, and anything sensitive. Keep only representative test data that a developer needs to work with the project.\\\\n\\\\n\\\\n**Option B: Write seed data by hand** (recommended for most projects):\\\\n\\\\nCreate `supabase/seed.sql` with INSERT statements that set up a useful local development state: a few test users, sample data, and so on. This is often better than dumping production data because you control exactly what's in it.\\\\n\\\\nFor more on organizing seed files, glob patterns, and generating realistic data, see [Seeding your database](/docs/guides/local-development/seeding-your-database).\\\\n\\\\n\\\\n### Step 6: Verify\\\\n\\\\n```bash\\\\nsupabase start\\\\nsupabase db reset\\\\n```\\\\n\\\\n`db reset` destroys the local database and recreates it from scratch: it applies all migrations in order, then runs `seed.sql`. If this succeeds, your setup is reproducible. Anyone who clones the repo can do the same.\\\\n\\\\n\\\\n### Step 7: Commit\\\\n\\\\n```bash\\\\ngit add supabase/\\\\ngit commit -m \\\\\\\"add supabase local development setup\\\\\\\"\\\\n```\\\\n\\\\nYour project now has a fully reproducible local development environment.\\\\n\\\\n\\\\n For an existing project, the pulled migration already serves as your schema baseline. You don't need to also create a `schemas/` directory, because that would mean maintaining two representations of the same schema. If you want to adopt declarative schemas later, see [Declarative database schemas](/docs/guides/local-development/declarative-database-schemas). For day-to-day changes going forward, see [The daily workflow](#the-daily-workflow) below.\\\\n\\\\n\\\\n\\\\n## Start a new project from scratch\\\\n\\\\nNo remote project yet. You're building from scratch and want to do it right from the start.\\\\n\\\\n\\\\n### Step 1: Initialize\\\\n\\\\n```bash\\\\nsupabase init\\\\n```\\\\n\\\\n\\\\n### Step 2: Start the local stack\\\\n\\\\n```bash\\\\nsupabase start\\\\n```\\\\n\\\\nOn first run, Docker images are pulled, which takes a few minutes. Subsequent starts are fast. Once running, the CLI outputs local service URLs and credentials, including the Studio URL for a local instance of the Dashboard. See [Install and run the CLI](/docs/guides/local-development/cli/getting-started#access-your-projects-services) for the full output and how to reach each service.\\\\n\\\\n\\\\n### Step 3: Create your schema\\\\n\\\\nTwo approaches, pick one:\\\\n\\\\n**Option A: Declarative schema** (recommended for new projects)\\\\n\\\\nDeclare the state you want your database to be in as a file in `supabase/schemas/`, for example:\\\\n\\\\n```sql title=\\\\\\\"supabase/schemas/schema.sql\\\\\\\"\\\\ncreate table public.todos (\\\\n id bigint generated by default as identity primary key,\\\\n created_at timestamptz default now() not null,\\\\n title text not null,\\\\n is_complete boolean default false not null,\\\\n user_id uuid references auth.users (id) default auth.uid() not null\\\\n);\\\\n\\\\nalter table public.todos enable row level security;\\\\n\\\\ncreate policy \\\\\\\"Users can read their own todos\\\\\\\"\\\\n on public.todos for select\\\\n using (auth.uid() = user_id);\\\\n\\\\ncreate policy \\\\\\\"Users can create their own todos\\\\\\\"\\\\n on public.todos for insert\\\\n with check (auth.uid() = user_id);\\\\n```\\\\n\\\\nThen generate a migration from it:\\\\n\\\\n```bash\\\\nsupabase db diff -f initial-schema\\\\n```\\\\n\\\\nThis compares your declared schema against the current (empty) database and generates a migration file in `supabase/migrations/`. For the full declarative workflow, including managing views and functions, ordering schema files, and known caveats, see [Declarative database schemas](/docs/guides/local-development/declarative-database-schemas).\\\\n\\\\n**Option B: Write the migration directly**\\\\n\\\\n```bash\\\\nsupabase migration new initial-schema\\\\n```\\\\n\\\\nThis creates an empty file at `supabase/migrations/_initial-schema.sql`. Write your SQL in it, then apply:\\\\n\\\\n```bash\\\\nsupabase db reset\\\\n```\\\\n\\\\n\\\\n### Step 4: Add seed data\\\\n\\\\nCreate `supabase/seed.sql`:\\\\n\\\\n```sql title=\\\\\\\"supabase/seed.sql\\\\\\\"\\\\n-- Create a test user (Supabase Auth)\\\\n-- Note: this is a placeholder row so seeded data has a user_id to reference.\\\\n-- It has no password, so it can't be used to sign in. To create a\\\\n-- login-capable user, use the Auth admin API or the local Studio.\\\\ninsert into auth.users (id, email, raw_user_meta_data)\\\\nvalues ('d0e3c8f0-1234-5678-9abc-def012345678', 'test@example.com', '{}');\\\\n\\\\n-- Seed application data\\\\ninsert into public.todos (title, user_id)\\\\nvalues\\\\n ('Buy groceries', 'd0e3c8f0-1234-5678-9abc-def012345678'),\\\\n ('Write documentation', 'd0e3c8f0-1234-5678-9abc-def012345678');\\\\n```\\\\n\\\\n\\\\n### Step 5: Verify\\\\n\\\\n```bash\\\\nsupabase db reset\\\\n```\\\\n\\\\nDrops everything, applies migrations, runs seed. If this passes, your project is reproducible.\\\\n\\\\n\\\\n### Step 6: Commit\\\\n\\\\n```bash\\\\ngit add supabase/\\\\ngit commit -m \\\\\\\"add supabase local development setup\\\\\\\"\\\\n```\\\\n\\\\n\\\\n## The daily workflow\\\\n\\\\nBoth starting points converge here. You have a working `./supabase` directory in your repo. Here's how day-to-day development works.\\\\n\\\\n\\\\n### Making schema changes\\\\n\\\\nWhich approach you use is a project-level decision, set when you first created your schema - not a per-change choice. It depends on whether you keep declarative files in `supabase/schemas/`. Pick the tab that matches your project.\\\\n\\\\n\\\\n \\\\n 1. Edit your schema file(s) in `supabase/schemas/` (add a table, a column, a policy, etc.)\\\\n 2. Generate a migration: `supabase db diff -f add-due-date-to-todo`\\\\n 3. Review the generated migration file. See [Cleaning up generated migrations](#cleaning-up-generated-migrations)\\\\n 4. Verify the full chain: `supabase db reset`\\\\n 5. Commit the schema file **and** the migration together\\\\n\\\\n \\\\n `db diff` compares your `supabase/schemas/` files against your existing migrations; it does **not** read the live local database. Changes you make directly in Studio or via SQL are ignored, so `db diff` reports \\\\\\\"No schema changes found\\\\\\\" and silently drops them. Always edit the schema files, then diff.\\\\n \\\\n \\\\n\\\\n \\\\n **If you made changes through the local Studio UI:**\\\\n\\\\n ```bash\\\\n supabase db diff -f add-due-date-to-todo\\\\n ```\\\\n\\\\n This captures your UI changes as a migration file. This works only when your project has **no** declarative files in `supabase/schemas/`: `db diff` then compares the live local database against your migrations. If you use declarative schemas, don't edit through Studio expecting `db diff` to catch it - see the **Declarative schemas** tab.\\\\n\\\\n **If you prefer to write SQL directly:**\\\\n\\\\n ```bash\\\\n supabase migration new add-due-date-to-todo\\\\n ```\\\\n\\\\n Write the SQL in the generated file. Then verify:\\\\n\\\\n ```bash\\\\n supabase db reset\\\\n ```\\\\n\\\\n Commit the migration.\\\\n \\\\n\\\\n\\\\n\\\\n### Generating types\\\\n\\\\nIf your app uses the generated TypeScript types, regenerate them whenever your schema changes:\\\\n\\\\n```bash\\\\nsupabase gen types --lang typescript --local > database.types.ts\\\\n```\\\\n\\\\nUse `--linked` instead of `--local` to generate from your remote project. TypeScript is the default language; pass `--lang go`, `--lang swift`, or `--lang python` for others.\\\\n\\\\nFor working with the generated types (helper types, JSON inference, type-safe queries) and automating regeneration in CI, see [Generating types](/docs/guides/api/rest/generating-types).\\\\n\\\\n\\\\n### Staying in sync with your team\\\\n\\\\nWhen someone else pushes new migrations:\\\\n\\\\n```bash\\\\ngit pull\\\\nsupabase db reset\\\\n```\\\\n\\\\n`db reset` replays all migrations from scratch, so you'll always match the current state of the repo.\\\\n\\\\n\\\\n## Pushing to a remote project\\\\n\\\\nWhen you're ready to deploy your schema to a remote Supabase instance:\\\\n\\\\n```bash\\\\n# Authenticate (if not already)\\\\nsupabase login\\\\n\\\\n# Link to the remote project (if not already)\\\\nsupabase link --project-ref \\\\n\\\\n# Preview what will be applied\\\\nsupabase db push --dry-run\\\\n\\\\n# Apply migrations\\\\nsupabase db push\\\\n```\\\\n\\\\n`db push` applies only migrations that haven't been applied to the remote yet. It tracks this via the `supabase_migrations.schema_migrations` table created automatically on the remote database.\\\\n\\\\nTo also seed a fresh remote instance (dev/staging environments only):\\\\n\\\\n```bash\\\\nsupabase db push --include-seed\\\\n```\\\\n\\\\n\\\\n Never use `--include-seed` on a production database. Seed data is for development and testing.\\\\n\\\\n\\\\n\\\\n### Resetting a remote dev or staging project\\\\n\\\\nIf a dev or staging remote drifts or gets into a messy state, you can wipe it and rebuild it from your local migrations:\\\\n\\\\n```bash\\\\nsupabase db reset --linked\\\\n```\\\\n\\\\nUnlike the default `supabase db reset`, which targets your local database, the `--linked` flag runs against the remote project you connected with `supabase link`: it drops the remote schema, then replays every local migration in order. Add `--include-seed` to reload seed data as well.\\\\n\\\\n\\\\n `db reset --linked` is destructive: it erases all data in the linked remote database. Only run it against throwaway dev or staging projects, and double-check which project you're linked to (`supabase projects list` shows the linked one) before running it. Never use it on production.\\\\n\\\\n\\\\nFor multi-environment setups with CI/CD (feature branches, staging, production), see [Managing Environments](/docs/guides/deployment/managing-environments).\\\\n\\\\n\\\\n## Key commands at a glance\\\\n\\\\n| Command | What it does |\\\\n| -------------------------------------- | ------------------------------------------------------------------------------------------------------- |\\\\n| `supabase init` | Creates `./supabase/config.toml` |\\\\n| `supabase start` | Starts the local stack, applies migrations + seed |\\\\n| `supabase stop` | Stops the local stack (data persists until `db reset`) |\\\\n| `supabase db reset` | Destroys local DB, applies all migrations + seed from scratch |\\\\n| `supabase db reset --linked` | Destroys the **linked remote** DB and rebuilds it from local migrations (destructive; dev/staging only) |\\\\n| `supabase db diff -f ` | Generates a migration by diffing current DB state against a shadow database |\\\\n| `supabase db pull` | Pulls remote schema into a new local migration file |\\\\n| `supabase db push` | Applies pending local migrations to the remote database |\\\\n| `supabase db dump` | Exports remote DB schema (or `--data-only` for data) via `pg_dump` |\\\\n| `supabase migration new ` | Creates an empty migration file |\\\\n| `supabase migration list` | Compares local migrations against remote migration history |\\\\n| `supabase gen types --lang typescript` | Generates TypeScript types from your database schema |\\\\n| `supabase link --project-ref` | Connects local project to a remote Supabase project |\\\\n| `supabase login` | Authenticates with the Supabase platform |\\\\n\\\\nFor the full command reference and every flag, see the [CLI reference](/docs/reference/cli).\\\\n\\\\n\\\\n## Cleaning up generated migrations\\\\n\\\\nWhen `supabase db diff` generates a migration, it may include statements that are technically correct but noisy. Review every generated migration before committing.\\\\n\\\\n\\\\n### Grants\\\\n\\\\nYou may see lines like:\\\\n\\\\n```sql\\\\nGRANT MAINTAIN, REFERENCES, TRIGGER, TRUNCATE ON public.todos TO anon;\\\\nGRANT MAINTAIN, REFERENCES, TRIGGER, TRUNCATE ON public.todos TO authenticated;\\\\nGRANT MAINTAIN, REFERENCES, TRIGGER, TRUNCATE ON public.todos TO service_role;\\\\n```\\\\n\\\\nThese appear because the diff tool treats permissions as part of the schema state. For tables in the `public` schema, these grants are applied by default and the lines are redundant. They're harmless, but if you want clean migrations, you can remove them. Be consistent across your team about whether you keep or remove them.\\\\n\\\\n\\\\n### Revoke/re-grant patterns\\\\n\\\\nSometimes a diff produces:\\\\n\\\\n```sql\\\\nREVOKE ALL ON TABLE public.todos FROM anon;\\\\nGRANT ALL ON TABLE public.todos TO anon;\\\\n```\\\\n\\\\nThis is the diff tool being overly cautious. If you haven't changed permissions, these lines can be safely removed.\\\\n\\\\n\\\\n### Extension statements\\\\n\\\\n`CREATE EXTENSION IF NOT EXISTS ...` may appear. Keep these if the extension is required by your migration. Remove them if the extension is already created by a previous migration or is part of the default Supabase setup.\\\\n\\\\n\\\\n### Known limitations of `db diff`\\\\n\\\\nThe diff is generated by `pg-delta`, the default schema diff engine. (The older [`migra`](https://github.com/djrobstep/migra) engine is still available: set `enabled = false` under `[experimental.pgdelta]` in `config.toml`, or pass `--use-migra`.) No diff engine captures everything. Most notably, DML (INSERT, UPDATE, DELETE) is not tracked, so data changes must be added to the migration manually, and some entities like RLS policy renames and certain view properties don't diff cleanly. See the [full list of caveats](/docs/guides/local-development/declarative-database-schemas#known-caveats) in the declarative schemas guide.\\\\n\\\\nTreat `db diff` output as a draft, not a final migration. When in doubt, review the generated SQL and adjust it manually.\\\\n\\\\n\\\\n## Troubleshooting\\\\n\\\\n**`db reset` fails with a migration error**\\\\n\\\\nThe output will show which migration file failed and the SQL error. Fix the migration file, then run `db reset` again.\\\\n\\\\n**`db push` says migrations are already applied**\\\\n\\\\nThe remote database already has those migrations in its history. Run `supabase migration list` to compare local vs. remote state. If they're out of sync, use `supabase migration repair` to correct the remote history.\\\\n\\\\n**Schema drift: remote was changed outside of migrations**\\\\n\\\\nIf someone modified the remote database directly (via Dashboard, SQL editor, etc.), run `supabase db pull` to capture those changes as a new migration file. Then `supabase db reset` locally to verify everything still works.\\\\n\\\\n**Docker issues on `supabase start`**\\\\n\\\\nEnsure Docker is running and has at least 7 GB of RAM allocated. If containers fail health checks, try:\\\\n\\\\n```bash\\\\nsupabase stop\\\\nsupabase start\\\\n```\\\\n\\\\nIf problems persist, `supabase stop --no-backup` for a clean restart (this removes local database data).\\\\n\\\"},{\\\"title\\\":\\\"Build a User Management App with RedwoodJS\\\",\\\"href\\\":\\\"https://supabase.com/docs/guides/getting-started/tutorials/with-redwoodjs\\\",\\\"content\\\":\\\"# Build a User Management App with RedwoodJS\\\\n\\\\n\\\\n\\\\nThis tutorial demonstrates how to build a basic user management app. The app authenticates and identifies the user, stores their profile information in the database, and allows the user to log in, update their profile details, and upload a profile photo. The app uses:\\\\n\\\\n* [Supabase Database](/docs/guides/database) - a Postgres database for storing your user data and [Row Level Security](/docs/guides/auth#row-level-security) so data is protected and users can only access their own information.\\\\n* [Supabase Auth](/docs/guides/auth) - allow users to sign up and log in.\\\\n* [Supabase Storage](/docs/guides/storage) - allow users to upload a profile photo.\\\\n\\\\n![Supabase User Management example](/docs/img/user-management-demo.png)\\\\n\\\\n\\\\n If you get stuck while working through this guide, you can find the [full example on GitHub](https://github.com/redwoodjs/redwoodjs-supabase-quickstart).\\\\n\\\\n\\\\n\\\\n## About RedwoodJS\\\\n\\\\nA Redwood application is split into two parts: a frontend and a backend. This is represented as two node projects within a single monorepo.\\\\n\\\\nThe frontend project is called **`web`** and the backend project is called **`api`**. For clarity, we will refer to these in prose as **\\\\\\\"sides,\\\\\\\"** that is, the `web side` and the `api side`.\\\\nThey are separate projects because code on the `web side` will end up running in the user's browser while code on the `api side` will run on a server somewhere.\\\\n\\\\n\\\\n Important: When this guide refers to \\\\\\\"API,\\\\\\\" that means the Supabase API and when it refers to `api side`, that means the RedwoodJS `api side`.\\\\n\\\\n\\\\nThe **`api side`** is an implementation of a GraphQL API. The business logic is organized into \\\\\\\"services\\\\\\\" that represent their own internal API and can be called both from external GraphQL requests and other internal services.\\\\n\\\\nThe **`web side`** is built with React. Redwood's router makes it simple to map URL paths to React \\\\\\\"Page\\\\\\\" components (and automatically code-split your app on each route).\\\\nPages may contain a \\\\\\\"Layout\\\\\\\" component to wrap content. They also contain \\\\\\\"Cells\\\\\\\" and regular React components.\\\\nCells allow you to declaratively manage the lifecycle of a component that fetches and displays data.\\\\n\\\\nFor the sake of consistency with the other framework tutorials, we'll build this app a little differently than normal.\\\\nWe ***won't use*** Prisma to connect to the Supabase Postgres database or [Prisma migrations](https://redwoodjs.com/docs/cli-commands#prisma-migrate) as one typically might in a Redwood app.\\\\nInstead, we'll rely on the Supabase client to do some of the work on the **`web`** side and use the client again on the **`api`** side to do data fetching as well.\\\\n\\\\nThat means you will want to refrain from running any `yarn rw prisma migrate` commands and also double check your build commands on deployment to ensure Prisma won't reset your database. Prisma currently doesn't support cross-schema foreign keys, so introspecting the schema fails due\\\\nto how your Supabase `public` schema references the `auth.users`.\\\\n\\\\n\\\\n## Project setup\\\\n\\\\nBefore you start building you need to set up the Database and API. You can do this by starting a new Project in Supabase and then creating a \\\\\\\"schema\\\\\\\" inside the database.\\\\n\\\\n\\\\n### Create a project\\\\n\\\\n1. [Create a new project](/dashboard) in the Supabase Dashboard.\\\\n2. Enter your project details.\\\\n3. Wait for the new database to launch.\\\\n\\\\n\\\\n### Set up the database schema\\\\n\\\\nNow set up the database schema. You can use the \\\\\\\"User Management Starter\\\\\\\" quickstart in the SQL Editor, or you can copy/paste the SQL from below and run it.\\\\n\\\\n\\\\n \\\\n 1. Go to the [SQL Editor](/dashboard/project/_/sql) page in the Dashboard.\\\\n 2. Click **User Management Starter** under the **Community > Quickstarts** tab.\\\\n 3. Click **Run**.\\\\n\\\\n \\\\n You can pull the database schema down to your local project by running the `db pull` command. Read the [local development docs](/docs/guides/cli/local-development#link-your-project) for detailed instructions.\\\\n\\\\n ```bash\\\\n supabase link --project-ref \\\\n # You can get from your project's dashboard URL: https://supabase.com/dashboard/project/\\\\n supabase db pull\\\\n ```\\\\n \\\\n \\\\n\\\\n \\\\n \\\\n When working locally you can run the following command to create a new migration file:\\\\n \\\\n\\\\n ```bash\\\\n supabase migration new user_management_starter\\\\n ```\\\\n\\\\n ```sql\\\\n -- Create a table for public profiles\\\\n create table profiles (\\\\n id uuid references auth.users not null primary key,\\\\n updated_at timestamp with time zone,\\\\n username text unique,\\\\n full_name text,\\\\n avatar_url text,\\\\n website text,\\\\n\\\\n constraint username_length check (char_length(username) >= 3)\\\\n );\\\\n\\\\n -- Grant the privileges roles need\\\\n GRANT SELECT ON public.profiles TO anon;\\\\n GRANT SELECT, INSERT, UPDATE ON public.profiles TO authenticated;\\\\n\\\\n -- Set up Row Level Security (RLS)\\\\n -- See https://supabase.com/docs/guides/database/postgres/row-level-security for more details.\\\\n alter table profiles\\\\n enable row level security;\\\\n\\\\n create policy \\\\\\\"Public profiles are viewable by everyone.\\\\\\\" on profiles\\\\n for select using (true);\\\\n\\\\n create policy \\\\\\\"Users can insert their own profile.\\\\\\\" on profiles\\\\n for insert with check ((select auth.uid()) = id);\\\\n\\\\n create policy \\\\\\\"Users can update own profile.\\\\\\\" on profiles\\\\n for update using ((select auth.uid()) = id);\\\\n\\\\n -- This trigger automatically creates a profile entry when a new user signs up via Supabase Auth.\\\\n -- See https://supabase.com/docs/guides/auth/managing-user-data#using-triggers for more details.\\\\n create function public.handle_new_user()\\\\n returns trigger\\\\n set search_path = ''\\\\n as $$\\\\n begin\\\\n insert into public.profiles (id, full_name, avatar_url)\\\\n values (new.id, new.raw_user_meta_data->>'full_name', new.raw_user_meta_data->>'avatar_url');\\\\n return new;\\\\n end;\\\\n $$ language plpgsql security definer;\\\\n create trigger on_auth_user_created\\\\n after insert on auth.users\\\\n for each row execute procedure public.handle_new_user();\\\\n\\\\n -- Set up Storage!\\\\n insert into storage.buckets (id, name)\\\\n values ('avatars', 'avatars');\\\\n\\\\n -- Set up access controls for storage. Allows downloading object with public key\\\\n -- See https://supabase.com/docs/guides/storage/security/access-control#policy-examples for more details.\\\\n create policy \\\\\\\"Avatar images are publicly accessible.\\\\\\\" on storage.objects\\\\n for select using (bucket_id = 'avatars' and storage.allow_any_operation(array['object.get_authenticated_info', 'object.get_authenticated']));\\\\n\\\\n create policy \\\\\\\"Anyone can upload an avatar.\\\\\\\" on storage.objects\\\\n for insert with check (bucket_id = 'avatars');\\\\n\\\\n create policy \\\\\\\"Anyone can update their own avatar.\\\\\\\" on storage.objects\\\\n for update using ((select auth.uid()) = owner) with check (bucket_id = 'avatars');\\\\n ```\\\\n \\\\n\\\\n\\\\n\\\\n### Get API details\\\\n\\\\nNow that you've created some database tables, you are ready to insert data using the auto-generated API.\\\\n\\\\nTo do this, you need to get the Project URL and key from [the project **Connect** dialog](/dashboard/project/_?showConnect=true\\\\\\\\&connectTab=frameworks\\\\\\\\&framework=).\\\\n\\\\n[Read the API keys docs](/docs/guides/getting-started/api-keys) for a full explanation of all key types and their uses.\\\\n\\\\n\\\\n Supabase is changing the way keys work to improve project security and developer experience. You can [read the full announcement on GitHub](https://github.com/orgs/supabase/discussions/29260).\\\\n\\\\n The older `anon` and `service_role` keys will work until the end of 2026 but **we strongly encourage switching to and using** the new publishable (`sb_publishable_xxx`) and secret (`sb_secret_xxx`) keys now.\\\\n\\\\n In most cases, you can get keys from [the Project's **Connect** dialog](/dashboard/project/_?showConnect=true\\\\\\\\&connectTab=frameworks\\\\\\\\&framework=), but if you want a specific key, you can find them in the [**Settings > API Keys**](/dashboard/project/_/settings/api-keys/) section of the Dashboard.\\\\n\\\\n * **For legacy keys**, copy the `anon` key for client-side operations and the `service_role` key for server-side operations from the **Legacy API Keys** tab.\\\\n * **For new keys**, open the **API Keys** tab, if you don't have a publishable key already, click **Create new API Keys**, and copy the value from the **Publishable key** section.\\\\n\\\\n\\\\n\\\\n## Building the app\\\\n\\\\nLet's start building the RedwoodJS app from scratch.\\\\n\\\\n\\\\n RedwoodJS requires Node.js `>= 14.x <= 16.x` and Yarn `>= 1.15`.\\\\n\\\\n\\\\nMake sure you have installed yarn since RedwoodJS relies on it to [manage its packages in workspaces](https://classic.yarnpkg.com/lang/en/docs/workspaces/) for its `web` and `api` \\\\\\\"sides.\\\\\\\"\\\\n\\\\n\\\\n### Initialize a RedwoodJS app\\\\n\\\\nWe can use [Create Redwood App](https://redwoodjs.com/docs/quick-start) command to initialize\\\\nan app called `supabase-redwoodjs`:\\\\n\\\\n```bash\\\\nyarn create redwood-app supabase-redwoodjs\\\\ncd supabase-redwoodjs\\\\n```\\\\n\\\\nWhile the app is installing, you should see:\\\\n\\\\n```bash\\\\n✔ Creating Redwood app\\\\n ✔ Checking node and yarn compatibility\\\\n ✔ Creating directory 'supabase-redwoodjs'\\\\n✔ Installing packages\\\\n ✔ Running 'yarn install'... (This could take a while)\\\\n✔ Convert TypeScript files to JavaScript\\\\n✔ Generating types\\\\n\\\\nThanks for trying out Redwood!\\\\n```\\\\n\\\\nThen let's install the only additional dependency [supabase-js](https://github.com/supabase/supabase-js) by running the `setup auth` command:\\\\n\\\\n```bash\\\\nyarn redwood setup auth supabase\\\\n```\\\\n\\\\nWhen prompted:\\\\n\\\\n> Overwrite existing /api/src/lib/auth.\\\\\\\\[jt]s?\\\\n\\\\nSay, **yes** and it will setup the Supabase client in your app and also provide hooks used with Supabase authentication.\\\\n\\\\n```bash\\\\n✔ Generating auth lib...\\\\n ✔ Successfully wrote file `./api/src/lib/auth.js`\\\\n ✔ Adding auth config to web...\\\\n ✔ Adding auth config to GraphQL API...\\\\n ✔ Adding required web packages...\\\\n ✔ Installing packages...\\\\n ✔ One more thing...\\\\n\\\\n You will need to add your Supabase URL (SUPABASE_URL), public API KEY,\\\\n and JWT SECRET (SUPABASE_KEY, and SUPABASE_JWT_SECRET) to your .env file.\\\\n```\\\\n\\\\nNext, we want to save the environment variables in a `.env`.\\\\nWe need the `API URL` as well as the key and `jwt_secret` that you copied [earlier](#get-api-details).\\\\n\\\\n\\\\n \\\\n ```bash name=.env\\\\n SUPABASE_URL=YOUR_SUPABASE_URL\\\\n SUPABASE_KEY=YOUR_SUPABASE_PUBLISHABLE_KEY\\\\n SUPABASE_JWT_SECRET=YOUR_SUPABASE_JWT_SECRET\\\\n ```\\\\n \\\\n\\\\n\\\\nAnd finally, you will also need to save **just** the `web side` environment variables to the `redwood.toml`.\\\\n\\\\n\\\\n \\\\n ```bash name=redwood.toml\\\\n [web]\\\\n title = \\\\\\\"Supabase Redwood Tutorial\\\\\\\"\\\\n port = 8910\\\\n apiProxyPath = \\\\\\\"/.redwood/functions\\\\\\\"\\\\n includeEnvironmentVariables = [\\\\\\\"SUPABASE_URL\\\\\\\", \\\\\\\"SUPABASE_KEY\\\\\\\"]\\\\n [api]\\\\n port = 8911\\\\n [browser]\\\\n open = true\\\\n ```\\\\n \\\\n\\\\n\\\\nThese variables will be exposed on the browser, and that's completely fine.\\\\nThey allow your web app to initialize the Supabase client with your publishable key\\\\nsince we have [Row Level Security](/docs/guides/auth#row-level-security) enabled on our Database.\\\\n\\\\nYou'll see these being used to configure your Supabase client in `web/src/App.js`:\\\\n\\\\n\\\\n \\\\n ```js name=web/src/App.js\\\\n // ... Redwood imports\\\\n import { AuthProvider } from '@redwoodjs/auth'\\\\n import { createClient } from '@supabase/supabase-js'\\\\n\\\\n // ...\\\\n\\\\n const supabase = createClient(process.env.SUPABASE_URL, process.env.SUPABASE_KEY)\\\\n\\\\n const App = () => (\\\\n \\\\n \\\\n \\\\n \\\\n \\\\n \\\\n \\\\n \\\\n \\\\n )\\\\n\\\\n export default App\\\\n ```\\\\n \\\\n\\\\n\\\\n\\\\n### App styling (optional)\\\\n\\\\nAn optional step is to update the CSS file `web/src/index.css` to make the app look better.\\\\nYou can find the full contents of this file [in the example repository](https://raw.githubusercontent.com/supabase/supabase/master/examples/user-management/react-user-management/src/index.css).\\\\n\\\\n\\\\n### Start RedwoodJS and your first page\\\\n\\\\nLet's test our setup at the moment by starting up the app:\\\\n\\\\n```bash\\\\nyarn rw dev\\\\n```\\\\n\\\\n\\\\n `rw` is an alias for `redwood`, as in `yarn rw` to run Redwood CLI commands.\\\\n\\\\n\\\\nYou should see a \\\\\\\"Welcome to RedwoodJS\\\\\\\" page and a message about not having any pages yet.\\\\n\\\\nSo, let's create a \\\\\\\"home\\\\\\\" page:\\\\n\\\\n```bash\\\\nyarn rw generate page home /\\\\n\\\\n✔ Generating page files...\\\\n ✔ Successfully wrote file `./web/src/pages/HomePage/HomePage.stories.js`\\\\n ✔ Successfully wrote file `./web/src/pages/HomePage/HomePage.test.js`\\\\n ✔ Successfully wrote file `./web/src/pages/HomePage/HomePage.js`\\\\n✔ Updating routes file...\\\\n✔ Generating types ...\\\\n```\\\\n\\\\n\\\\n The `/` is important here as it creates a root level route.\\\\n\\\\n\\\\nYou can stop the `dev` server if you want; to see your changes, just be sure to run `yarn rw dev` again.\\\\n\\\\nYou should see the `Home` page route in `web/src/Routes.js`:\\\\n\\\\n\\\\n \\\\n ```bash name=web/src/Routes.js\\\\n import { Router, Route } from '@redwoodjs/router'\\\\n\\\\n const Routes = () => {\\\\n return (\\\\n \\\\n \\\\n \\\\n \\\\n )\\\\n }\\\\n\\\\n export default Routes\\\\n ```\\\\n \\\\n\\\\n\\\\n\\\\n### Set up a login component\\\\n\\\\nLet's set up a Redwood component to manage logins and sign ups. We'll use Magic Links, so users can sign in with their email without using passwords.\\\\n\\\\n```bash\\\\nyarn rw g component auth\\\\n\\\\n ✔ Generating component files...\\\\n ✔ Successfully wrote file `./web/src/components/Auth/Auth.test.js`\\\\n ✔ Successfully wrote file `./web/src/components/Auth/Auth.stories.js`\\\\n ✔ Successfully wrote file `./web/src/components/Auth/Auth.js`\\\\n\\\\n```\\\\n\\\\nNow, update the `Auth.js` component to contain:\\\\n\\\\n\\\\n \\\\n ```jsx name=/web/src/components/Auth/Auth.js\\\\n import { useState } from 'react'\\\\n import { useAuth } from '@redwoodjs/auth'\\\\n\\\\n const Auth = () => {\\\\n const { logIn } = useAuth()\\\\n const [loading, setLoading] = useState(false)\\\\n const [email, setEmail] = useState('')\\\\n\\\\n const handleLogin = async (email) => {\\\\n try {\\\\n setLoading(true)\\\\n const { error } = await logIn({ email })\\\\n if (error) throw error\\\\n alert('Check your email for the login link!')\\\\n } catch (error) {\\\\n alert(error.error_description || error.message)\\\\n } finally {\\\\n setLoading(false)\\\\n }\\\\n }\\\\n\\\\n return (\\\\n
    \\\\n
    \\\\n

    Supabase + RedwoodJS

    \\\\n

    Sign in via magic link with your email below

    \\\\n
    \\\\n setEmail(e.target.value)}\\\\n />\\\\n
    \\\\n
    \\\\n {\\\\n e.preventDefault()\\\\n handleLogin(email)\\\\n }}\\\\n className={'button block'}\\\\n disabled={loading}\\\\n >\\\\n {loading ? Loading : Send magic link}\\\\n \\\\n
    \\\\n
    \\\\n
    \\\\n )\\\\n }\\\\n\\\\n export default Auth\\\\n ```\\\\n
    \\\\n
    \\\\n\\\\n\\\\n### Set up an account component\\\\n\\\\nAfter a user is signed in we can allow them to edit their profile details and manage their account.\\\\n\\\\nLet's create a new component for that called `Account.js`.\\\\n\\\\n```bash\\\\nyarn rw g component account\\\\n\\\\n ✔ Generating component files...\\\\n ✔ Successfully wrote file `./web/src/components/Account/Account.test.js`\\\\n ✔ Successfully wrote file `./web/src/components/Account/Account.stories.js`\\\\n ✔ Successfully wrote file `./web/src/components/Account/Account.js`\\\\n```\\\\n\\\\nAnd then update the file to contain:\\\\n\\\\n\\\\n \\\\n ```jsx name=web/src/components/Account/Account.js\\\\n import { useState, useEffect } from 'react'\\\\n import { useAuth } from '@redwoodjs/auth'\\\\n\\\\n const Account = () => {\\\\n const { client: supabase, currentUser, logOut } = useAuth()\\\\n const [loading, setLoading] = useState(true)\\\\n const [username, setUsername] = useState(null)\\\\n const [website, setWebsite] = useState(null)\\\\n const [avatar_url, setAvatarUrl] = useState(null)\\\\n\\\\n useEffect(() => {\\\\n getProfile()\\\\n }, [supabase.auth.session])\\\\n\\\\n async function getProfile() {\\\\n try {\\\\n setLoading(true)\\\\n const user = supabase.auth.user()\\\\n\\\\n const { data, error, status } = await supabase\\\\n .from('profiles')\\\\n .select(`username, website, avatar_url`)\\\\n .eq('id', user.id)\\\\n .single()\\\\n\\\\n if (error && status !== 406) {\\\\n throw error\\\\n }\\\\n\\\\n if (data) {\\\\n setUsername(data.username)\\\\n setWebsite(data.website)\\\\n setAvatarUrl(data.avatar_url)\\\\n }\\\\n } catch (error) {\\\\n alert(error.message)\\\\n } finally {\\\\n setLoading(false)\\\\n }\\\\n }\\\\n\\\\n async function updateProfile({ username, website, avatar_url }) {\\\\n try {\\\\n setLoading(true)\\\\n const user = supabase.auth.user()\\\\n\\\\n const updates = {\\\\n id: user.id,\\\\n username,\\\\n website,\\\\n avatar_url,\\\\n updated_at: new Date(),\\\\n }\\\\n\\\\n const { error } = await supabase.from('profiles').upsert(updates, {\\\\n returning: 'minimal', // Don't return the value after inserting\\\\n })\\\\n\\\\n if (error) {\\\\n throw error\\\\n }\\\\n\\\\n alert('Updated profile!')\\\\n } catch (error) {\\\\n alert(error.message)\\\\n } finally {\\\\n setLoading(false)\\\\n }\\\\n }\\\\n\\\\n return (\\\\n
    \\\\n
    \\\\n

    Supabase + RedwoodJS

    \\\\n

    Your profile

    \\\\n
    \\\\n
    \\\\n \\\\n \\\\n
    \\\\n
    \\\\n \\\\n setUsername(e.target.value)}\\\\n />\\\\n
    \\\\n
    \\\\n \\\\n setWebsite(e.target.value)}\\\\n />\\\\n
    \\\\n\\\\n
    \\\\n updateProfile({ username, website, avatar_url })}\\\\n disabled={loading}\\\\n >\\\\n {loading ? 'Loading ...' : 'Update'}\\\\n \\\\n
    \\\\n\\\\n
    \\\\n \\\\n
    \\\\n
    \\\\n
    \\\\n
    \\\\n )\\\\n }\\\\n\\\\n export default Account\\\\n ```\\\\n
    \\\\n
    \\\\n\\\\nYou'll see the use of `useAuth()` several times. Redwood's `useAuth` hook provides convenient ways to access\\\\n`logIn`, `logOut`, `currentUser`, and access the `supabase` authenticate client. We'll use it to get an instance\\\\nof the Supabase client to interact with your API.\\\\n\\\\n\\\\n### Update home page\\\\n\\\\nWith all the components in place, update your `HomePage` page to use them:\\\\n\\\\n\\\\n \\\\n ```jsx name=web/src/pages/HomePage/HomePage.js\\\\n import { useAuth } from '@redwoodjs/auth'\\\\n import { MetaTags } from '@redwoodjs/web'\\\\n\\\\n import Account from 'src/components/Account'\\\\n import Auth from 'src/components/Auth'\\\\n\\\\n const HomePage = () => {\\\\n const { isAuthenticated } = useAuth()\\\\n\\\\n return (\\\\n <>\\\\n \\\\n {!isAuthenticated ? : }\\\\n \\\\n )\\\\n }\\\\n\\\\n export default HomePage\\\\n ```\\\\n \\\\n\\\\n\\\\n\\\\n What we're doing here is showing the sign in form if you aren't logged in and your account profile if you are.\\\\n\\\\n\\\\n\\\\n## Profile photos\\\\n\\\\nNext, add a way for users to upload a profile photo. Supabase configures every project with [Storage](/docs/guides/storage) for managing large files like photos and videos.\\\\n\\\\n\\\\n### Create an upload widget\\\\n\\\\nLet's create an avatar for the user so that they can upload a profile photo. We can start by creating a new component:\\\\n\\\\n```bash\\\\nyarn rw g component avatar\\\\n ✔ Generating component files...\\\\n ✔ Successfully wrote file `./web/src/components/Avatar/Avatar.test.js`\\\\n ✔ Successfully wrote file `./web/src/components/Avatar/Avatar.stories.js`\\\\n ✔ Successfully wrote file `./web/src/components/Avatar/Avatar.js`\\\\n```\\\\n\\\\nNow, update your Avatar component to contain the following widget:\\\\n\\\\n\\\\n \\\\n ```jsx name=web/src/components/Avatar/Avatar.js\\\\n import { useEffect, useState } from 'react'\\\\n import { useAuth } from '@redwoodjs/auth'\\\\n\\\\n const Avatar = ({ url, size, onUpload }) => {\\\\n const { client: supabase } = useAuth()\\\\n\\\\n const [avatarUrl, setAvatarUrl] = useState(null)\\\\n const [uploading, setUploading] = useState(false)\\\\n\\\\n useEffect(() => {\\\\n if (url) downloadImage(url)\\\\n }, [url])\\\\n\\\\n async function downloadImage(path) {\\\\n try {\\\\n const { data, error } = await supabase.storage.from('avatars').download(path)\\\\n if (error) {\\\\n throw error\\\\n }\\\\n const url = URL.createObjectURL(data)\\\\n setAvatarUrl(url)\\\\n } catch (error) {\\\\n console.log('Error downloading image: ', error.message)\\\\n }\\\\n }\\\\n\\\\n async function uploadAvatar(event) {\\\\n try {\\\\n setUploading(true)\\\\n\\\\n if (!event.target.files || event.target.files.length === 0) {\\\\n throw new Error('You must select an image to upload.')\\\\n }\\\\n\\\\n const file = event.target.files[0]\\\\n const fileExt = file.name.split('.').pop()\\\\n const fileName = `${Math.random()}.${fileExt}`\\\\n const filePath = `${fileName}`\\\\n\\\\n const { error: uploadError } = await supabase.storage.from('avatars').upload(filePath, file)\\\\n\\\\n if (uploadError) {\\\\n throw uploadError\\\\n }\\\\n\\\\n onUpload(filePath)\\\\n } catch (error) {\\\\n alert(error.message)\\\\n } finally {\\\\n setUploading(false)\\\\n }\\\\n }\\\\n\\\\n return (\\\\n
    \\\\n {avatarUrl ? (\\\\n \\\\n ) : (\\\\n
    \\\\n )}\\\\n
    \\\\n \\\\n \\\\n
    \\\\n
    \\\\n )\\\\n }\\\\n\\\\n export default Avatar\\\\n ```\\\\n \\\\n\\\\n\\\\n\\\\n### Launch!\\\\n\\\\nOnce that's done, run this in a terminal window to launch the `dev` server:\\\\n\\\\n```bash\\\\nyarn rw dev\\\\n```\\\\n\\\\nAnd then open the browser to [localhost:8910](http://localhost:8910) and you should see the completed app.\\\\n\\\\n![Supabase RedwoodJS](/docs/img/supabase-redwoodjs-demo.png)\\\\n\\\\nAt this stage you have a fully functional application!\\\\n\\\\n\\\\n## See also\\\\n\\\\n* Learn more about [RedwoodJS](https://redwoodjs.com)\\\\n* Visit the [RedwoodJS Discourse Community](https://community.redwoodjs.com)\\\\n\\\"},{\\\"title\\\":\\\"Row Level Security\\\",\\\"href\\\":\\\"https://supabase.com/docs/guides/database/postgres/row-level-security\\\",\\\"content\\\":\\\"# Row Level Security\\\\n\\\\nSecure your data using Postgres Row Level Security.\\\\n\\\\nWhen you need granular authorization rules, nothing beats Postgres's [Row Level Security (RLS)](https://www.postgresql.org/docs/current/ddl-rowsecurity.html).\\\\n\\\\n\\\\n## Row Level Security in Supabase\\\\n\\\\n\\\\n Supabase allows convenient and secure data access from the browser, as long as you enable RLS.\\\\n\\\\n RLS *must* always be enabled on any tables stored in an exposed schema. By default, this is the `public` schema.\\\\n\\\\n RLS is enabled by default on tables created with the Table Editor in the dashboard. If you create one in raw SQL or with the SQL editor, remember to enable RLS yourself and grant only the permissions each Postgres role needs.\\\\n\\\\n ```sql\\\\n GRANT SELECT ON . TO anon;\\\\n GRANT SELECT, INSERT, UPDATE, DELETE ON . TO authenticated;\\\\n GRANT SELECT, INSERT, UPDATE, DELETE ON . TO service_role;\\\\n\\\\n alter table .\\\\n enable row level security;\\\\n ```\\\\n\\\\n\\\\nRLS is incredibly powerful and flexible, allowing you to write complex SQL rules that fit your unique business needs. RLS can be combined with [Supabase Auth](/docs/guides/auth) for end-to-end user security from the browser to the database.\\\\n\\\\nRLS is a Postgres primitive and can provide \\\\\\\"[defense in depth](https://en.wikipedia.org/wiki/Defense_in_depth_\\\\\\\\(computing\\\\\\\\))\\\\\\\" to protect your data from malicious actors even when accessed through third-party tooling.\\\\n\\\\n\\\\n## Policies\\\\n\\\\n[Policies](https://www.postgresql.org/docs/current/sql-createpolicy.html) are Postgres's rule engine. Policies are easy to understand once you get the hang of them. Each policy is attached to a table, and the policy is executed every time a table is accessed.\\\\n\\\\nYou can just think of them as adding a `WHERE` clause to every query. For example a policy like this ...\\\\n\\\\n```sql\\\\ncreate policy \\\\\\\"Individuals can view their own todos.\\\\\\\"\\\\non todos for select\\\\nusing ( (select auth.uid()) = user_id );\\\\n```\\\\n\\\\n.. would translate to this whenever a user tries to select from the todos table:\\\\n\\\\n```sql\\\\nselect *\\\\nfrom todos\\\\nwhere auth.uid() = todos.user_id;\\\\n-- Policy is implicitly added.\\\\n```\\\\n\\\\n\\\\n## Enabling Row Level Security\\\\n\\\\nYou can enable RLS for any table using the `enable row level security` clause:\\\\n\\\\n```sql\\\\nalter table \\\\\\\"table_name\\\\\\\" enable row level security;\\\\n```\\\\n\\\\nOnce you have enabled RLS, no data will be accessible via the [API](/docs/guides/api) when using a publishable key, until you create policies.\\\\n\\\\n\\\\n## Auto-enable RLS for new tables\\\\n\\\\nIf you want RLS enabled automatically for new tables, you can create an event trigger that runs after table creation. This uses a Postgres [event trigger](/docs/guides/database/postgres/event-triggers) to call `ALTER TABLE ... ENABLE ROW LEVEL SECURITY` on each newly created table.\\\\n\\\\n```sql\\\\nCREATE OR REPLACE FUNCTION rls_auto_enable()\\\\nRETURNS EVENT_TRIGGER\\\\nLANGUAGE plpgsql\\\\nSECURITY DEFINER\\\\nSET search_path = pg_catalog\\\\nAS $$\\\\nDECLARE\\\\n cmd record;\\\\nBEGIN\\\\n FOR cmd IN\\\\n SELECT *\\\\n FROM pg_event_trigger_ddl_commands()\\\\n WHERE command_tag IN ('CREATE TABLE', 'CREATE TABLE AS', 'SELECT INTO')\\\\n AND object_type IN ('table','partitioned table')\\\\n LOOP\\\\n IF cmd.schema_name IS NOT NULL AND cmd.schema_name IN ('public') AND cmd.schema_name NOT IN ('pg_catalog','information_schema') AND cmd.schema_name NOT LIKE 'pg_toast%' AND cmd.schema_name NOT LIKE 'pg_temp%' THEN\\\\n BEGIN\\\\n EXECUTE format('alter table if exists %s enable row level security', cmd.object_identity);\\\\n RAISE LOG 'rls_auto_enable: enabled RLS on %', cmd.object_identity;\\\\n EXCEPTION\\\\n WHEN OTHERS THEN\\\\n RAISE LOG 'rls_auto_enable: failed to enable RLS on %', cmd.object_identity;\\\\n END;\\\\n ELSE\\\\n RAISE LOG 'rls_auto_enable: skip % (either system schema or not in enforced list: %.)', cmd.object_identity, cmd.schema_name;\\\\n END IF;\\\\n END LOOP;\\\\nEND;\\\\n$$;\\\\n\\\\nDROP EVENT TRIGGER IF EXISTS ensure_rls;\\\\nCREATE EVENT TRIGGER ensure_rls\\\\nON ddl_command_end\\\\nWHEN TAG IN ('CREATE TABLE', 'CREATE TABLE AS', 'SELECT INTO')\\\\nEXECUTE FUNCTION rls_auto_enable();\\\\n```\\\\n\\\\nNote that this applies to tables created after the trigger is installed. Existing tables still need RLS enabled manually.\\\\n\\\\n\\\\n When a request is made without an authenticated user (e.g., no access token is provided or the session has expired), `auth.uid()` returns `null`.\\\\n\\\\n This means that a policy like:\\\\n\\\\n ```sql\\\\n USING (auth.uid() = user_id)\\\\n ```\\\\n\\\\n will silently fail for unauthenticated users, because:\\\\n\\\\n ```sql\\\\n null = user_id\\\\n ```\\\\n\\\\n is always false in SQL.\\\\n\\\\n To avoid confusion and make your intention clear, we recommend explicitly checking for authentication:\\\\n\\\\n ```sql\\\\n USING (auth.uid() IS NOT NULL AND auth.uid() = user_id)\\\\n ```\\\\n\\\\n\\\\n\\\\n## Authenticated and unauthenticated roles\\\\n\\\\nSupabase maps every request to one of the roles:\\\\n\\\\n* `anon`: an unauthenticated request (the user is not logged in)\\\\n* `authenticated`: an authenticated request (the user is logged in)\\\\n\\\\nThese are actually [Postgres Roles](/docs/guides/database/postgres/roles). You can use these roles within your Policies using the `TO` clause:\\\\n\\\\n```sql\\\\ncreate policy \\\\\\\"Profiles are viewable by everyone\\\\\\\"\\\\non profiles for select\\\\nto authenticated, anon\\\\nusing ( true );\\\\n\\\\n-- OR\\\\n\\\\ncreate policy \\\\\\\"Public profiles are viewable only by authenticated users\\\\\\\"\\\\non profiles for select\\\\nto authenticated\\\\nusing ( true );\\\\n```\\\\n\\\\n\\\\n Using the `anon` Postgres role is different from an [anonymous user](/docs/guides/auth/auth-anonymous) in Supabase Auth. An anonymous user assumes the `authenticated` role to access the database and can be differentiated from a permanent user by checking the `is_anonymous` claim in the JWT.\\\\n\\\\n\\\\n\\\\n## Creating policies\\\\n\\\\nPolicies are SQL logic that you attach to a Postgres table. You can attach as many policies as you want to each table.\\\\n\\\\nSupabase provides some [helpers](#helper-functions) that simplify RLS if you're using Supabase Auth. We'll use these helpers to illustrate some basic policies:\\\\n\\\\n\\\\n### SELECT policies\\\\n\\\\nYou can specify select policies with the `using` clause.\\\\n\\\\nLet's say you have a table called `profiles` in the public schema and you want to enable read access to everyone.\\\\n\\\\n```sql\\\\n-- 1. Create table\\\\ncreate table profiles (\\\\n id uuid primary key,\\\\n user_id uuid references auth.users,\\\\n avatar_url text\\\\n);\\\\n\\\\n-- 2. Enable RLS\\\\nalter table profiles enable row level security;\\\\n\\\\n-- 3. Create Policy\\\\ncreate policy \\\\\\\"Public profiles are visible to everyone.\\\\\\\"\\\\non profiles for select\\\\nto anon -- the Postgres Role (recommended)\\\\nusing ( true ); -- the actual Policy\\\\n```\\\\n\\\\nAlternatively, if you only wanted users to be able to see their own profiles:\\\\n\\\\n```sql\\\\ncreate policy \\\\\\\"User can see their own profile only.\\\\\\\"\\\\non profiles\\\\nfor select using ( (select auth.uid()) = user_id );\\\\n```\\\\n\\\\n\\\\n### INSERT policies\\\\n\\\\nYou can specify insert policies with the `with check` clause. The `with check` expression ensures that any new row data adheres to the policy constraints.\\\\n\\\\nLet's say you have a table called `profiles` in the public schema and you only want users to be able to create a profile for themselves. In that case, we want to check their User ID matches the value that they are trying to insert:\\\\n\\\\n```sql\\\\n-- 1. Create table\\\\ncreate table profiles (\\\\n id uuid primary key,\\\\n user_id uuid references auth.users,\\\\n avatar_url text\\\\n);\\\\n\\\\n-- 2. Enable RLS\\\\nalter table profiles enable row level security;\\\\n\\\\n-- 3. Create Policy\\\\ncreate policy \\\\\\\"Users can create a profile.\\\\\\\"\\\\non profiles for insert\\\\nto authenticated -- the Postgres Role (recommended)\\\\nwith check ( (select auth.uid()) = user_id ); -- the actual Policy\\\\n```\\\\n\\\\n\\\\n### UPDATE policies\\\\n\\\\nYou can specify update policies by combining both the `using` and `with check` expressions.\\\\n\\\\nThe `using` clause represents the condition that must be true for the update to be allowed, and `with check` clause ensures that the updates made adhere to the policy constraints.\\\\n\\\\nLet's say you have a table called `profiles` in the public schema and you only want users to be able to update their own profile.\\\\n\\\\nYou can create a policy where the `using` clause checks if the user owns the profile being updated. And the `with check` clause ensures that, in the resultant row, users do not change the `user_id` to a value that is not equal to their User ID, maintaining that the modified profile still meets the ownership condition.\\\\n\\\\n```sql\\\\n-- 1. Create table\\\\ncreate table profiles (\\\\n id uuid primary key,\\\\n user_id uuid references auth.users,\\\\n avatar_url text\\\\n);\\\\n\\\\n-- 2. Enable RLS\\\\nalter table profiles enable row level security;\\\\n\\\\n-- 3. Create Policy\\\\ncreate policy \\\\\\\"Users can update their own profile.\\\\\\\"\\\\non profiles for update\\\\nto authenticated -- the Postgres Role (recommended)\\\\nusing ( (select auth.uid()) = user_id ) -- checks if the existing row complies with the policy expression\\\\nwith check ( (select auth.uid()) = user_id ); -- checks if the new row complies with the policy expression\\\\n```\\\\n\\\\nIf no `with check` expression is defined, then the `using` expression will be used both to determine which rows are visible (normal USING case) and which new rows will be allowed to be added (WITH CHECK case).\\\\n\\\\n\\\\n To perform an `UPDATE` operation, a corresponding [`SELECT` policy](#select-policies) is required. Without a `SELECT` policy, the `UPDATE` operation will not work as expected.\\\\n\\\\n\\\\n\\\\n### DELETE policies\\\\n\\\\nYou can specify delete policies with the `using` clause.\\\\n\\\\nLet's say you have a table called `profiles` in the public schema and you only want users to be able to delete their own profile:\\\\n\\\\n```sql\\\\n-- 1. Create table\\\\ncreate table profiles (\\\\n id uuid primary key,\\\\n user_id uuid references auth.users,\\\\n avatar_url text\\\\n);\\\\n\\\\n-- 2. Enable RLS\\\\nalter table profiles enable row level security;\\\\n\\\\n-- 3. Create Policy\\\\ncreate policy \\\\\\\"Users can delete a profile.\\\\\\\"\\\\non profiles for delete\\\\nto authenticated -- the Postgres Role (recommended)\\\\nusing ( (select auth.uid()) = user_id ); -- the actual Policy\\\\n```\\\\n\\\\n\\\\n### Views\\\\n\\\\nViews bypass RLS by default because they are usually created with the `postgres` user. This is a feature of Postgres, which automatically creates views with `security definer`.\\\\n\\\\nIn Postgres 15 and above, you can make a view obey the RLS policies of the underlying tables when invoked by `anon` and `authenticated` roles by setting `security_invoker = true`.\\\\n\\\\n```sql\\\\ncreate view \\\\nwith(security_invoker = true)\\\\nas select \\\\n```\\\\n\\\\nIn older versions of Postgres, protect your views by revoking access from the `anon` and `authenticated` roles, or by putting them in an unexposed schema.\\\\n\\\\n\\\\n## Helper functions\\\\n\\\\nSupabase provides some helper functions that make it easier to write Policies.\\\\n\\\\n\\\\n### `auth.uid()`\\\\n\\\\nReturns the ID of the user making the request.\\\\n\\\\n\\\\n### `auth.jwt()`\\\\n\\\\n\\\\n Not all information present in the JWT should be used in RLS policies. For instance, creating an RLS policy that relies on the `user_metadata` claim can create security issues in your application as this information can be modified by authenticated end users.\\\\n\\\\n\\\\nReturns the JWT of the user making the request. Anything that you store in the user's `raw_app_meta_data` column or the `raw_user_meta_data` column will be accessible using this function. It's important to know the distinction between these two:\\\\n\\\\n* `raw_user_meta_data` - can be updated by the authenticated user using the `supabase.auth.update()` function. It is not a good place to store authorization data.\\\\n* `raw_app_meta_data` - cannot be updated by the user, so it's a good place to store authorization data.\\\\n\\\\nThe `auth.jwt()` function is extremely versatile. For example, if you store some team data inside `app_metadata`, you can use it to determine whether a particular user belongs to a team. For example, if this was an array of IDs:\\\\n\\\\n```sql\\\\ncreate policy \\\\\\\"User is in team\\\\\\\"\\\\non my_table\\\\nto authenticated\\\\nusing ( team_id in (select auth.jwt() -> 'app_metadata' -> 'teams'));\\\\n```\\\\n\\\\n\\\\n Keep in mind that a JWT is not always \\\\\\\"fresh\\\\\\\". In the example above, even if you remove a user from a team and update the `app_metadata` field, that will not be reflected using `auth.jwt()` until the user's JWT is refreshed.\\\\n\\\\n Also, if you are using Cookies for Auth, then you must be mindful of the JWT size. Some browsers are limited to 4096 bytes for each cookie, and so the total size of your JWT should be small enough to fit inside this limitation.\\\\n\\\\n\\\\n\\\\n### MFA\\\\n\\\\nThe `auth.jwt()` function can be used to check for [Multi-Factor Authentication](/docs/guides/auth/auth-mfa#enforce-rules-for-mfa-logins). For example, you could restrict a user from updating their profile unless they have at least 2 levels of authentication (Assurance Level 2):\\\\n\\\\n```sql\\\\ncreate policy \\\\\\\"Restrict updates.\\\\\\\"\\\\non profiles\\\\nas restrictive\\\\nfor update\\\\nto authenticated using (\\\\n (select auth.jwt()->>'aal') = 'aal2'\\\\n);\\\\n```\\\\n\\\\n\\\\n## Bypassing Row Level Security\\\\n\\\\nSupabase provides special \\\\\\\"Service\\\\\\\" keys, which can be used to bypass RLS. These should never be used in the browser or exposed to customers, but they are useful for administrative tasks.\\\\n\\\\n\\\\n Supabase will adhere to the RLS policy of the signed-in user, even if the client library is initialized with a Service Key.\\\\n\\\\n\\\\nYou can also create new [Postgres Roles](/docs/guides/database/postgres/roles) which can bypass Row Level Security using the \\\\\\\"bypass RLS\\\\\\\" privilege:\\\\n\\\\n```sql\\\\nalter role \\\\\\\"role_name\\\\\\\" with bypassrls;\\\\n```\\\\n\\\\nThis can be useful for system-level access. You should *never* share login credentials for any Postgres Role with this privilege.\\\\n\\\\n\\\\n## RLS performance recommendations\\\\n\\\\nEvery authorization system has an impact on performance. While row level security is powerful, the performance impact is important to keep in mind. This is especially true for queries that scan every row in a table - like many `select` operations, including those using limit, offset, and ordering.\\\\n\\\\nBased on a series of [tests](https://github.com/GaryAustin1/RLS-Performance), we have a few recommendations for RLS:\\\\n\\\\n\\\\n### Add indexes\\\\n\\\\nMake sure you've added [indexes](/docs/guides/database/postgres/indexes) on any columns used within the Policies which are not already indexed (or primary keys). For a Policy like this:\\\\n\\\\n```sql\\\\ncreate policy \\\\\\\"rls_test_select\\\\\\\" on test_table\\\\nto authenticated\\\\nusing ( (select auth.uid()) = user_id );\\\\n```\\\\n\\\\nYou can add an index like:\\\\n\\\\n```sql\\\\ncreate index userid\\\\non test_table\\\\nusing btree (user_id);\\\\n```\\\\n\\\\n\\\\n#### Benchmarks\\\\n\\\\n| Test | Before (ms) | After (ms) | % Improvement | Change |\\\\n| --------------------------------------------------------------------------------------------- | ----------- | ---------- | ------------- | ------------------------------------------------------------------------------------------------------------ |\\\\n| [test1-indexed](https://github.com/GaryAustin1/RLS-Performance/tree/main/tests/test1-indexed) | 171 | \\\\\\\\< 0.1 | 99.94% |
    Before:
    No index

    After:
    `user_id` indexed
    |\\\\n\\\\n\\\\n### Call functions with `select`\\\\n\\\\nYou can use `select` statement to improve policies that use functions. For example, instead of this:\\\\n\\\\n```sql\\\\ncreate policy \\\\\\\"rls_test_select\\\\\\\" on test_table\\\\nto authenticated\\\\nusing ( auth.uid() = user_id );\\\\n```\\\\n\\\\nYou can do:\\\\n\\\\n```sql\\\\ncreate policy \\\\\\\"rls_test_select\\\\\\\" on test_table\\\\nto authenticated\\\\nusing ( (select auth.uid()) = user_id );\\\\n```\\\\n\\\\nThis method works well for JWT functions like `auth.uid()` and `auth.jwt()` as well as `security definer` Functions. Wrapping the function causes an `initPlan` to be run by the Postgres optimizer, which allows it to \\\\\\\"cache\\\\\\\" the results per-statement, rather than calling the function on each row.\\\\n\\\\n\\\\n You can only use this technique if the results of the query or function do not change based on the row data.\\\\n\\\\n\\\\n\\\\n#### Benchmarks\\\\n\\\\n| Test | Before (ms) | After (ms) | % Improvement | Change |\\\\n| --------------------------------------------------------------------------------------------------------------------------------- | ----------- | ---------- | ------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |\\\\n| [test2a-wrappedSQL-uid](https://github.com/GaryAustin1/RLS-Performance/tree/main/tests/test2a-wrappedSQL-uid\\\\\\\\(\\\\\\\\)) | 179 | 9 | 94.97% |
    Before:
    `auth.uid() = user_id`

    After:
    `(select auth.uid()) = user_id`
    |\\\\n| [test2b-wrappedSQL-isadmin](https://github.com/GaryAustin1/RLS-Performance/tree/main/tests/test2b-wrappedSQL-isadmin\\\\\\\\(\\\\\\\\)) | 11,000 | 7 | 99.94% |
    Before:
    `is_admin()` *table join*

    After:
    `(select is_admin())` *table join*
    |\\\\n| [test2c-wrappedSQL-two-functions](https://github.com/GaryAustin1/RLS-Performance/tree/main/tests/test2c-wrappedSQL-two-functions) | 11,000 | 10 | 99.91% |
    Before:
    `is_admin() OR auth.uid() = user_id`

    After:
    `(select is_admin()) OR (select auth.uid() = user_id)`
    |\\\\n| [test2d-wrappedSQL-sd-fun](https://github.com/GaryAustin1/RLS-Performance/tree/main/tests/test2d-wrappedSQL-sd-fun) | 178,000 | 12 | 99.993% |
    Before:
    `has_role() = role`

    After:
    (select has\\\\\\\\_role()) = role
    |\\\\n| [test2e-wrappedSQL-sd-fun-array](https://github.com/GaryAustin1/RLS-Performance/tree/main/tests/test2e-wrappedSQL-sd-fun-array) | 173000 | 16 | 99.991% |
    Before:
    `team_id=any(user_teams())`

    After:
    team\\\\\\\\_id=any(array(select user\\\\\\\\_teams()))
    |\\\\n\\\\n\\\\n### Add filters to every query\\\\n\\\\nPolicies are \\\\\\\"implicit where clauses,\\\\\\\" so it's common to run `select` statements without any filters. This is a bad pattern for performance. Instead of doing this (JS client example):\\\\n\\\\n{/* prettier-ignore */}\\\\n\\\\n```js\\\\nconst { data } = supabase\\\\n .from('table')\\\\n .select()\\\\n```\\\\n\\\\nYou should always add a filter:\\\\n\\\\n{/* prettier-ignore */}\\\\n\\\\n```js\\\\nconst { data } = supabase\\\\n .from('table')\\\\n .select()\\\\n .eq('user_id', userId)\\\\n```\\\\n\\\\nEven though this duplicates the contents of the Policy, Postgres can use the filter to construct a better query plan.\\\\n\\\\n\\\\n#### Benchmarks\\\\n\\\\n| Test | Before (ms) | After (ms) | % Improvement | Change |\\\\n| ------------------------------------------------------------------------------------------------- | ----------- | ---------- | ------------- | ------------------------------------------------------------------------------------------------------------------------------------------ |\\\\n| [test3-addfilter](https://github.com/GaryAustin1/RLS-Performance/tree/main/tests/test3-addfilter) | 171 | 9 | 94.74% |
    Before:
    `auth.uid() = user_id`

    After:
    add `.eq` or `where` on `user_id`
    |\\\\n\\\\n\\\\n### Use security definer functions\\\\n\\\\nA \\\\\\\"security definer\\\\\\\" function runs using the same role that *created* the function. This means that if you create a role with a superuser (like `postgres`), then that function will have `bypassrls` privileges. For example, if you had a policy like this:\\\\n\\\\n```sql\\\\ncreate policy \\\\\\\"rls_test_select\\\\\\\" on test_table\\\\nto authenticated\\\\nusing (\\\\n exists (\\\\n select 1 from roles_table\\\\n where (select auth.uid()) = user_id and role = 'good_role'\\\\n )\\\\n);\\\\n```\\\\n\\\\nWe can instead create a `security definer` function which can scan `roles_table` without any RLS penalties:\\\\n\\\\n```sql\\\\ncreate function private.has_good_role()\\\\nreturns boolean\\\\nlanguage plpgsql\\\\nsecurity definer -- will run as the creator\\\\nas $$\\\\nbegin\\\\n return exists (\\\\n select 1 from roles_table\\\\n where (select auth.uid()) = user_id and role = 'good_role'\\\\n );\\\\nend;\\\\n$$;\\\\n\\\\n-- Update our policy to use this function:\\\\ncreate policy \\\\\\\"rls_test_select\\\\\\\"\\\\non test_table\\\\nto authenticated\\\\nusing ( (select private.has_good_role()) );\\\\n```\\\\n\\\\n\\\\n Security-definer functions should never be created in a schema in the \\\\\\\"Exposed schemas\\\\\\\" inside your [API settings](/dashboard/project/_/settings/api)\\\\\\\\`.\\\\n\\\\n\\\\n\\\\n### Minimize joins\\\\n\\\\nYou can often rewrite your Policies to avoid joins between the source and the target table. Instead, try to organize your policy to fetch all the relevant data from the target table into an array or set, then you can use an `IN` or `ANY` operation in your filter.\\\\n\\\\nFor example, this is an example of a slow policy which joins the source `test_table` to the target `team_user`:\\\\n\\\\n```sql\\\\ncreate policy \\\\\\\"rls_test_select\\\\\\\" on test_table\\\\nto authenticated\\\\nusing (\\\\n (select auth.uid()) in (\\\\n select user_id\\\\n from team_user\\\\n where team_user.team_id = team_id -- joins to the source \\\\\\\"test_table.team_id\\\\\\\"\\\\n )\\\\n);\\\\n```\\\\n\\\\nWe can rewrite this to avoid this join, and instead select the filter criteria into a set:\\\\n\\\\n```sql\\\\ncreate policy \\\\\\\"rls_test_select\\\\\\\" on test_table\\\\nto authenticated\\\\nusing (\\\\n team_id in (\\\\n select team_id\\\\n from team_user\\\\n where user_id = (select auth.uid()) -- no join\\\\n )\\\\n);\\\\n```\\\\n\\\\nIn this case you can also consider [using a `security definer` function](#use-security-definer-functions) to bypass RLS on the join table:\\\\n\\\\n\\\\n If the list exceeds 1000 items, a different approach may be needed or you may need to analyze the approach to ensure that the performance is acceptable.\\\\n\\\\n\\\\n\\\\n#### Benchmarks\\\\n\\\\n| Test | Before (ms) | After (ms) | % Improvement | Change |\\\\n| --------------------------------------------------------------------------------------------------- | ----------- | ---------- | ------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------- |\\\\n| [test5-fixed-join](https://github.com/GaryAustin1/RLS-Performance/tree/main/tests/test5-fixed-join) | 9,000 | 20 | 99.78% |
    Before:
    `auth.uid()` in table join on col

    After:
    col in table join on `auth.uid()`
    |\\\\n\\\\n\\\\n### Specify roles in your policies\\\\n\\\\nAlways use the Role of inside your policies, specified by the `TO` operator. For example, instead of this query:\\\\n\\\\n```sql\\\\ncreate policy \\\\\\\"rls_test_select\\\\\\\" on rls_test\\\\nusing ( auth.uid() = user_id );\\\\n```\\\\n\\\\nUse:\\\\n\\\\n```sql\\\\ncreate policy \\\\\\\"rls_test_select\\\\\\\" on rls_test\\\\nto authenticated\\\\nusing ( (select auth.uid()) = user_id );\\\\n```\\\\n\\\\nThis prevents the policy `( (select auth.uid()) = user_id )` from running for any `anon` users, since the execution stops at the `to authenticated` step.\\\\n\\\\n\\\\n#### Benchmarks\\\\n\\\\n| Test | Before (ms) | After (ms) | % Improvement | Change |\\\\n| --------------------------------------------------------------------------------------------- | ----------- | ---------- | ------------- | ------------------------------------------------------------------------------------------------------------------------------------ |\\\\n| [test6-To-role](https://github.com/GaryAustin1/RLS-Performance/tree/main/tests/test6-To-role) | 170 | \\\\\\\\< 0.1 | 99.78% |
    Before:
    No `TO` policy

    After:
    `TO authenticated` (anon accessing)
    |\\\\n\\\\n\\\\n## More resources\\\\n\\\\n* [Testing your database](/docs/guides/database/testing)\\\\n* [RLS Guide and Best Practices](https://github.com/orgs/supabase/discussions/14576)\\\\n* Community repo on testing RLS using [pgTAP and dbdev](https://github.com/usebasejump/supabase-test-helpers/tree/main)\\\\n\\\"},{\\\"title\\\":\\\"Securing your data\\\",\\\"href\\\":\\\"https://supabase.com/docs/guides/database/secure-data\\\",\\\"content\\\":\\\"# Securing your data\\\\n\\\\n\\\\n\\\\nSupabase helps you control access to your data. With access policies, you can protect sensitive data and make sure users only access what they're allowed to see.\\\\n\\\\n\\\\n## Connecting your app securely\\\\n\\\\nSupabase gives you several ways to access your data. Each option has a different security model:\\\\n\\\\n\\\\n### Data API\\\\n\\\\nUse Supabase client libraries, REST, or GraphQL with a publishable key. Protect exposed tables with [Row Level Security](/docs/guides/database/postgres/row-level-security) (RLS) and grant only the privileges each role needs.\\\\n\\\\n\\\\n### Edge Functions\\\\n\\\\nPut custom server-side logic between your client and database with [Edge Functions](/docs/guides/functions). You can use secrets, API keys, or database connection strings inside the function, and you can [disable the Data API](/docs/guides/database/data-api#disable-the-data-api-completely) if your app only accesses data this way.\\\\n\\\\n\\\\n### Direct database connections\\\\n\\\\nConnect to Postgres with a connection string from trusted servers, workers, or tools. Keep database credentials secret and use the right [connection method](/docs/guides/database/connecting-to-postgres) for your environment. You can [disable the Data API](/docs/guides/api/securing-your-api#disable-the-data-api) if your app only uses direct connections.\\\\n\\\\n\\\\n## Frontend access\\\\n\\\\nFor frontend apps, the Data API is the usual choice. You can keep your data secure while accessing it from the frontend, so long as you:\\\\n\\\\n* Turn on [Row Level Security](/docs/guides/database/postgres/row-level-security) (RLS) for your tables and properly configure your access policies to grant the least privileges necessary for your app to function\\\\n* Use your Supabase **publishable key** when you create a Supabase client\\\\n\\\\nYour publishable key is safe to expose with RLS enabled, because row access permission is checked against your access policies and the user's [JSON Web Token (JWT)](/docs/learn/auth-deep-dive/auth-deep-dive-jwts). The JWT is automatically sent by the Supabase client libraries if the user is logged in using Supabase Auth.\\\\n\\\\nOlder projects may also show an `anon` key. Treat it like a publishable key: it can identify your project, but it is not a secret and must be paired with RLS and least-privilege grants.\\\\n\\\\n\\\\n Unlike your publishable key, your secret and service role keys are **never** safe to expose because they bypass RLS. Only use your secret and service role keys on the backend. Treat them as secrets (for example, import them as sensitive environment variables instead of hardcoding them).\\\\n\\\\n\\\\n\\\\n## More information\\\\n\\\\nSupabase and Postgres provide you with multiple ways to manage security, including but not limited to Row Level Security. See the Access and Security pages for more information:\\\\n\\\\n* [Row Level Security](/docs/guides/database/postgres/row-level-security)\\\\n* [Column Level Security](/docs/guides/database/postgres/column-level-security)\\\\n* [Securing your API](/docs/guides/api/securing-your-api)\\\\n* [Managing Postgres roles](/docs/guides/database/postgres/roles)\\\\n* [Managing secrets with Vault](/docs/guides/database/vault)\\\\n\\\"}]}}}\"\n }\n ],\n \"isError\": false\n}", + "input": "{\n \"graphql_query\": \"{ searchDocs(query: \\\"Supabase local development migrations row level security authenticated users anon API\\\", limit: 5) { nodes { ... on Guide { title href content } ... on CLICommandReference { title href content } } } }\"\n}", + "output": "{\n \"content\": [\n {\n \"type\": \"text\",\n \"text\": \"{\\\"result\\\":{\\\"searchDocs\\\":{\\\"nodes\\\":[{\\\"title\\\":\\\"Integrating With Supabase Auth\\\",\\\"href\\\":\\\"https://supabase.com/docs/guides/functions/auth-legacy-jwt\\\",\\\"content\\\":\\\"# Integrating With Supabase Auth\\\\n\\\\nIntegrate Supabase Auth with Edge Functions\\\\n\\\\nEdge Functions work with [Supabase Auth](/docs/guides/auth).\\\\n\\\\nThis allows you to:\\\\n\\\\n* Automatically identify users through Legacy JWT tokens\\\\n* Enforce Row Level Security policies\\\\n* Integrate with your existing auth flow\\\\n\\\\n\\\\n## Setting up auth context\\\\n\\\\nWhen a user makes a request to an Edge Function, you can use the `Authorization` header to set the Auth context in the Supabase client and enforce Row Level Security policies.\\\\n\\\\n```js\\\\nimport { createClient } from 'npm:@supabase/supabase-js@2'\\\\n\\\\nDeno.serve(async (req: Request) => {\\\\n const supabaseClient = createClient(\\\\n Deno.env.get('SUPABASE_URL') ?? '',\\\\n Deno.env.get('SUPABASE_ANON_KEY') ?? '',\\\\n // Create client with Auth context of the user that called the function.\\\\n // This way your row-level-security (RLS) policies are applied.\\\\n {\\\\n global: {\\\\n headers: { Authorization: req.headers.get('Authorization')! },\\\\n },\\\\n }\\\\n );\\\\n\\\\n //...\\\\n})\\\\n```\\\\n\\\\n\\\\n This context setting happens in the `Deno.serve()` callback argument, so that the `Authorization` header is set for each individual request scope.\\\\n\\\\n\\\\n***\\\\n\\\\n\\\\n## Fetching the user\\\\n\\\\nBy getting the JWT from the `Authorization` header, you can provide the token to `getUser()` to fetch the user object to obtain metadata for the logged in user.\\\\n\\\\n```js\\\\nDeno.serve(async (req: Request) => {\\\\n // ...\\\\n const authHeader = req.headers.get('Authorization')!\\\\n const token = authHeader.replace('Bearer ', '')\\\\n const { data } = await supabaseClient.auth.getUser(token)\\\\n // ...\\\\n})\\\\n```\\\\n\\\\n***\\\\n\\\\n\\\\n## Row Level Security\\\\n\\\\nAfter initializing a Supabase client with the Auth context, all queries will be executed with the context of the user. For database queries, this means [Row Level Security](/docs/guides/database/postgres/row-level-security) will be enforced.\\\\n\\\\n```js\\\\nimport { createClient } from 'npm:@supabase/supabase-js@2'\\\\n\\\\nDeno.serve(async (req: Request) => {\\\\n // ...\\\\n // This query respects RLS - users only see rows they have access to\\\\n const { data, error } = await supabaseClient.from('profiles').select('*');\\\\n\\\\n if (error) {\\\\n return new Response('Database error', { status: 500 })\\\\n }\\\\n\\\\n // ...\\\\n})\\\\n```\\\\n\\\\n***\\\\n\\\\n\\\\n## Example\\\\n\\\\nSee the full [example on GitHub](https://github.com/supabase/supabase/blob/master/examples/edge-functions/supabase/functions/select-from-table-with-auth-rls/index.ts).\\\\n\\\\n\\\\n ```typescript\\\\n // Follow this setup guide to integrate the Deno language server with your editor:\\\\n // https://deno.land/manual/getting_started/setup_your_environment\\\\n // This enables autocomplete, go to definition, etc.\\\\n\\\\n import { createClient } from 'npm:supabase-js@2'\\\\n // New approach (v2.95.0+)\\\\n import { corsHeaders } from 'jsr:@supabase/supabase-js@2/cors'\\\\n // For older versions:\\\\n // import { corsHeaders } from '../_shared/cors.ts'\\\\n\\\\n console.log(`Function \\\\\\\"select-from-table-with-auth-rls\\\\\\\" up and running!`)\\\\n\\\\n Deno.serve(async (req: Request) => {\\\\n // This is needed if you're planning to invoke your function from a browser.\\\\n if (req.method === 'OPTIONS') {\\\\n return new Response('ok', { headers: corsHeaders })\\\\n }\\\\n\\\\n try {\\\\n const SUPABASE_PUBLISHABLE_KEYS = JSON.parse(Deno.env.get('SUPABASE_PUBLISHABLE_KEYS')!)\\\\n // Create a Supabase client with the Auth context of the logged in user.\\\\n const supabaseClient = createClient(\\\\n // Supabase API URL - env var exported by default.\\\\n Deno.env.get('SUPABASE_URL') ?? '',\\\\n // Supabase API PUBLISHABLE KEY - env var exported by default.\\\\n Deno.env.get(SUPABASE_PUBLISHABLE_KEYS['default']) ?? '',\\\\n // Create client with Auth context of the user that called the function.\\\\n // This way your row-level-security (RLS) policies are applied.\\\\n {\\\\n global: {\\\\n headers: { Authorization: req.headers.get('Authorization')! },\\\\n },\\\\n }\\\\n )\\\\n\\\\n // First get the token from the Authorization header\\\\n const token = req.headers.get('Authorization').replace('Bearer ', '')\\\\n\\\\n // Now we can get the session or user object\\\\n const {\\\\n data: { user },\\\\n } = await supabaseClient.auth.getUser(token)\\\\n\\\\n // And we can run queries in the context of our authenticated user\\\\n const { data, error } = await supabaseClient.from('users').select('*')\\\\n if (error) throw error\\\\n\\\\n return new Response(JSON.stringify({ user, data }), {\\\\n headers: { ...corsHeaders, 'Content-Type': 'application/json' },\\\\n status: 200,\\\\n })\\\\n } catch (error) {\\\\n return new Response(JSON.stringify({ error: error.message }), {\\\\n headers: { ...corsHeaders, 'Content-Type': 'application/json' },\\\\n status: 400,\\\\n })\\\\n }\\\\n })\\\\n\\\\n // To invoke:\\\\n // curl -i --location --request POST 'http://localhost:54321/functions/v1/select-from-table-with-auth-rls' \\\\\\\\\\\\n // --header 'Authorization: Bearer REDACTED_JWT' \\\\\\\\\\\\n // --header 'Content-Type: application/json' \\\\\\\\\\\\n // --data '{\\\\\\\"name\\\\\\\":\\\\\\\"Functions\\\\\\\"}'\\\\n ```\\\\n\\\\n\\\"},{\\\"title\\\":\\\"Local development workflow\\\",\\\"href\\\":\\\"https://supabase.com/docs/guides/local-development/cli-workflows\\\",\\\"content\\\":\\\"# Local development workflow\\\\n\\\\nSet up and run your day-to-day local development workflow with the Supabase CLI.\\\\n\\\\nThis guide walks through two common starting points for local development with the Supabase CLI, and shows how they converge into the same daily workflow. By the end, you'll have a `./supabase` directory in your repo that anyone can clone to recreate the full project, locally or on a fresh remote instance.\\\\n\\\\nThere are two starting points, both leading to the same place: database schema and migrations tracked in version control, with seed data for local development.\\\\n\\\\n* **[Move an existing project to local development](#move-an-existing-project-to-local-development)**: you have a project on the Supabase platform and want to bring it into a proper local development workflow.\\\\n* **[Start a new project from scratch](#start-a-new-project-from-scratch)**: you're building locally and will eventually push to a remote instance.\\\\n\\\\n\\\\n## Before you begin\\\\n\\\\nYou need the Supabase CLI installed and a Docker-compatible runtime running. If you haven't set these up yet, see [Install and run the CLI](/docs/guides/local-development/cli/getting-started) for installation across macOS, Windows, and Linux, and for the details of what `supabase start` brings up and how to access each service.\\\\n\\\\nKeep in mind that **the local stack is for development only**. It is not hardened for production use and must never be exposed to external traffic. It has no TLS, no rate limiting, and default credentials. Use it to develop and test, then deploy to the [Supabase Platform](https://supabase.com) or a proper self-hosted setup for anything beyond that.\\\\n\\\\n\\\\n How you invoke the CLI depends on how you installed it:\\\\n\\\\n * Installed globally with **Homebrew or Scoop**: run `supabase `.\\\\n * Added as a **project dependency** with npm, pnpm, yarn, or bun: run it through your package runner instead, for example `npx supabase ` (or `pnpm supabase`, `yarn supabase`, `bunx supabase`).\\\\n\\\\n Every example in this guide is written as `supabase `. Translate it to whichever form matches your install. See [Install and run the CLI](/docs/guides/local-development/cli/getting-started) for the full setup.\\\\n\\\\n\\\\n\\\\n If you want a working project to explore rather than an empty one, `supabase bootstrap` scaffolds a starter application (Next.js, Flutter, and more) with schema, migrations, and config already wired up. It's an alternative entry point to `supabase init` when starting a new project from scratch.\\\\n\\\\n\\\\n\\\\n## The `./supabase` directory\\\\n\\\\nAfter `supabase init`, your project contains a `./supabase` directory. Here's what goes in it and what to commit:\\\\n\\\\n| Path | Purpose | Commit? |\\\\n| ---------------------- | ----------------------------------------------------------------- | ------- |\\\\n| `config.toml` | Local stack configuration (ports, auth settings, etc.) | Yes |\\\\n| `migrations/` | Timestamped SQL migration files, applied in order | Yes |\\\\n| `seed.sql` | Dev/test data, applied after migrations on `start` and `db reset` | Yes |\\\\n| `schemas/` | Declarative schema files (if using that approach) | Yes |\\\\n| `.temp/`, `.branches/` | CLI internal state | No |\\\\n\\\\nThe `config.toml` is safe to commit. It contains no secrets by default. If you add sensitive values (OAuth credentials, API keys), use the `env()` function to reference environment variables instead of hardcoding them. See [Managing config and secrets](/docs/guides/local-development/managing-config).\\\\n\\\\n\\\\n Many database commands accept `--local` and `--linked` flags to choose what they act on. The defaults are not the same across commands: `db diff` and `db reset` default to `--local`, while `db pull`, `db push`, and `db dump` default to `--linked`. When in doubt, pass the flag explicitly.\\\\n\\\\n\\\\n\\\\n## Move an existing project to local development\\\\n\\\\nYou've built a project on the Supabase platform, with tables created via the Dashboard, SQL editor, or client libraries. Now you want a local dev setup with everything in version control.\\\\n\\\\n\\\\n### Step 1: Initialize\\\\n\\\\nIn your project root:\\\\n\\\\n```bash\\\\nsupabase init\\\\n```\\\\n\\\\nThis creates `./supabase/config.toml`. If you already have a project directory with application code, run this at the root. The `supabase/` directory will sit alongside your app code.\\\\n\\\\n\\\\n### Step 2: Authenticate\\\\n\\\\n```bash\\\\nsupabase login\\\\n```\\\\n\\\\nOpens a browser to generate an access token. The token is stored locally and used for all subsequent CLI commands that interact with the platform.\\\\n\\\\n\\\\n### Step 3: Link to your remote project\\\\n\\\\n```bash\\\\nsupabase link --project-ref \\\\n```\\\\n\\\\nFind your project ID in the Supabase Dashboard URL: `https://supabase.com/dashboard/project/`.\\\\n\\\\nThis tells the CLI which remote project to connect to for `db pull`, `db push`, and other remote operations. You'll be prompted for the database password, which is the password set when you created the project.\\\\n\\\\n\\\\n### Step 4: Pull the remote schema\\\\n\\\\n```bash\\\\nsupabase db pull\\\\n```\\\\n\\\\nThis connects to your remote database, dumps the entire schema, and saves it as a migration file:\\\\n\\\\n```\\\\nsupabase/migrations/_remote_schema.sql\\\\n```\\\\n\\\\nThis initial migration is your baseline. It represents the current state of your database, and all future changes build on top of it. `db pull` also records this migration as already applied in the remote migration history (the `supabase_migrations.schema_migrations` table), so a later `db push` won't try to reapply it.\\\\n\\\\n\\\\n `db pull` diffs your remote database against the CLI's default local stack, so the generated file can include statements you didn't expect. A common example is `DROP EXTENSION pg_net;`, emitted when your remote project has an extension disabled that the local stack enables by default. These statements apply silently on `db reset` and change your local schema, so read the file before committing it. See [Cleaning up generated migrations](#cleaning-up-generated-migrations) for what to look for.\\\\n\\\\n\\\\n\\\\n If you also use Supabase Auth or Storage and have customized their schemas, pull them separately:\\\\n\\\\n ```bash\\\\n supabase db pull --schema auth -f pull-auth-schema\\\\n supabase db pull --schema storage -f pull-storage-schema\\\\n ```\\\\n\\\\n These schemas are managed by Supabase and typically don't need to be pulled unless you've made custom modifications.\\\\n\\\\n\\\\n\\\\n### Step 5: Create seed data\\\\n\\\\nYou have two options:\\\\n\\\\n**Option A: Dump existing data from remote** (then clean it up):\\\\n\\\\n```bash\\\\nsupabase db dump --data-only --linked > supabase/seed.sql\\\\n```\\\\n\\\\n\\\\n Review and clean up the dump before committing. Remove production user data, secrets, personal information, and anything sensitive. Keep only representative test data that a developer needs to work with the project.\\\\n\\\\n\\\\n**Option B: Write seed data by hand** (recommended for most projects):\\\\n\\\\nCreate `supabase/seed.sql` with INSERT statements that set up a useful local development state: a few test users, sample data, and so on. This is often better than dumping production data because you control exactly what's in it.\\\\n\\\\nFor more on organizing seed files, glob patterns, and generating realistic data, see [Seeding your database](/docs/guides/local-development/seeding-your-database).\\\\n\\\\n\\\\n### Step 6: Verify\\\\n\\\\n```bash\\\\nsupabase start\\\\nsupabase db reset\\\\n```\\\\n\\\\n`db reset` destroys the local database and recreates it from scratch: it applies all migrations in order, then runs `seed.sql`. If this succeeds, your setup is reproducible. Anyone who clones the repo can do the same.\\\\n\\\\n\\\\n### Step 7: Commit\\\\n\\\\n```bash\\\\ngit add supabase/\\\\ngit commit -m \\\\\\\"add supabase local development setup\\\\\\\"\\\\n```\\\\n\\\\nYour project now has a fully reproducible local development environment.\\\\n\\\\n\\\\n For an existing project, the pulled migration already serves as your schema baseline. You don't need to also create a `schemas/` directory, because that would mean maintaining two representations of the same schema. If you want to adopt declarative schemas later, see [Declarative database schemas](/docs/guides/local-development/declarative-database-schemas). For day-to-day changes going forward, see [The daily workflow](#the-daily-workflow) below.\\\\n\\\\n\\\\n\\\\n## Start a new project from scratch\\\\n\\\\nNo remote project yet. You're building from scratch and want to do it right from the start.\\\\n\\\\n\\\\n### Step 1: Initialize\\\\n\\\\n```bash\\\\nsupabase init\\\\n```\\\\n\\\\n\\\\n### Step 2: Start the local stack\\\\n\\\\n```bash\\\\nsupabase start\\\\n```\\\\n\\\\nOn first run, Docker images are pulled, which takes a few minutes. Subsequent starts are fast. Once running, the CLI outputs local service URLs and credentials, including the Studio URL for a local instance of the Dashboard. See [Install and run the CLI](/docs/guides/local-development/cli/getting-started#access-your-projects-services) for the full output and how to reach each service.\\\\n\\\\n\\\\n### Step 3: Create your schema\\\\n\\\\nTwo approaches, pick one:\\\\n\\\\n**Option A: Declarative schema** (recommended for new projects)\\\\n\\\\nDeclare the state you want your database to be in as a file in `supabase/schemas/`, for example:\\\\n\\\\n```sql title=\\\\\\\"supabase/schemas/schema.sql\\\\\\\"\\\\ncreate table public.todos (\\\\n id bigint generated by default as identity primary key,\\\\n created_at timestamptz default now() not null,\\\\n title text not null,\\\\n is_complete boolean default false not null,\\\\n user_id uuid references auth.users (id) default auth.uid() not null\\\\n);\\\\n\\\\nalter table public.todos enable row level security;\\\\n\\\\ncreate policy \\\\\\\"Users can read their own todos\\\\\\\"\\\\n on public.todos for select\\\\n using (auth.uid() = user_id);\\\\n\\\\ncreate policy \\\\\\\"Users can create their own todos\\\\\\\"\\\\n on public.todos for insert\\\\n with check (auth.uid() = user_id);\\\\n```\\\\n\\\\nThen generate a migration from it:\\\\n\\\\n```bash\\\\nsupabase db diff -f initial-schema\\\\n```\\\\n\\\\nThis compares your declared schema against the current (empty) database and generates a migration file in `supabase/migrations/`. For the full declarative workflow, including managing views and functions, ordering schema files, and known caveats, see [Declarative database schemas](/docs/guides/local-development/declarative-database-schemas).\\\\n\\\\n**Option B: Write the migration directly**\\\\n\\\\n```bash\\\\nsupabase migration new initial-schema\\\\n```\\\\n\\\\nThis creates an empty file at `supabase/migrations/_initial-schema.sql`. Write your SQL in it, then apply:\\\\n\\\\n```bash\\\\nsupabase db reset\\\\n```\\\\n\\\\n\\\\n### Step 4: Add seed data\\\\n\\\\nCreate `supabase/seed.sql`:\\\\n\\\\n```sql title=\\\\\\\"supabase/seed.sql\\\\\\\"\\\\n-- Create a test user (Supabase Auth)\\\\n-- Note: this is a placeholder row so seeded data has a user_id to reference.\\\\n-- It has no password, so it can't be used to sign in. To create a\\\\n-- login-capable user, use the Auth admin API or the local Studio.\\\\ninsert into auth.users (id, email, raw_user_meta_data)\\\\nvalues ('d0e3c8f0-1234-5678-9abc-def012345678', 'test@example.com', '{}');\\\\n\\\\n-- Seed application data\\\\ninsert into public.todos (title, user_id)\\\\nvalues\\\\n ('Buy groceries', 'd0e3c8f0-1234-5678-9abc-def012345678'),\\\\n ('Write documentation', 'd0e3c8f0-1234-5678-9abc-def012345678');\\\\n```\\\\n\\\\n\\\\n### Step 5: Verify\\\\n\\\\n```bash\\\\nsupabase db reset\\\\n```\\\\n\\\\nDrops everything, applies migrations, runs seed. If this passes, your project is reproducible.\\\\n\\\\n\\\\n### Step 6: Commit\\\\n\\\\n```bash\\\\ngit add supabase/\\\\ngit commit -m \\\\\\\"add supabase local development setup\\\\\\\"\\\\n```\\\\n\\\\n\\\\n## The daily workflow\\\\n\\\\nBoth starting points converge here. You have a working `./supabase` directory in your repo. Here's how day-to-day development works.\\\\n\\\\n\\\\n### Making schema changes\\\\n\\\\nWhich approach you use is a project-level decision, set when you first created your schema - not a per-change choice. It depends on whether you keep declarative files in `supabase/schemas/`. Pick the tab that matches your project.\\\\n\\\\n\\\\n \\\\n 1. Edit your schema file(s) in `supabase/schemas/` (add a table, a column, a policy, etc.)\\\\n 2. Generate a migration: `supabase db diff -f add-due-date-to-todo`\\\\n 3. Review the generated migration file. See [Cleaning up generated migrations](#cleaning-up-generated-migrations)\\\\n 4. Verify the full chain: `supabase db reset`\\\\n 5. Commit the schema file **and** the migration together\\\\n\\\\n \\\\n `db diff` compares your `supabase/schemas/` files against your existing migrations; it does **not** read the live local database. Changes you make directly in Studio or via SQL are ignored, so `db diff` reports \\\\\\\"No schema changes found\\\\\\\" and silently drops them. Always edit the schema files, then diff.\\\\n \\\\n \\\\n\\\\n \\\\n **If you made changes through the local Studio UI:**\\\\n\\\\n ```bash\\\\n supabase db diff -f add-due-date-to-todo\\\\n ```\\\\n\\\\n This captures your UI changes as a migration file. This works only when your project has **no** declarative files in `supabase/schemas/`: `db diff` then compares the live local database against your migrations. If you use declarative schemas, don't edit through Studio expecting `db diff` to catch it - see the **Declarative schemas** tab.\\\\n\\\\n **If you prefer to write SQL directly:**\\\\n\\\\n ```bash\\\\n supabase migration new add-due-date-to-todo\\\\n ```\\\\n\\\\n Write the SQL in the generated file. Then verify:\\\\n\\\\n ```bash\\\\n supabase db reset\\\\n ```\\\\n\\\\n Commit the migration.\\\\n \\\\n\\\\n\\\\n\\\\n### Generating types\\\\n\\\\nIf your app uses the generated TypeScript types, regenerate them whenever your schema changes:\\\\n\\\\n```bash\\\\nsupabase gen types --lang typescript --local > database.types.ts\\\\n```\\\\n\\\\nUse `--linked` instead of `--local` to generate from your remote project. TypeScript is the default language; pass `--lang go`, `--lang swift`, or `--lang python` for others.\\\\n\\\\nFor working with the generated types (helper types, JSON inference, type-safe queries) and automating regeneration in CI, see [Generating types](/docs/guides/api/rest/generating-types).\\\\n\\\\n\\\\n### Staying in sync with your team\\\\n\\\\nWhen someone else pushes new migrations:\\\\n\\\\n```bash\\\\ngit pull\\\\nsupabase db reset\\\\n```\\\\n\\\\n`db reset` replays all migrations from scratch, so you'll always match the current state of the repo.\\\\n\\\\n\\\\n## Pushing to a remote project\\\\n\\\\nWhen you're ready to deploy your schema to a remote Supabase instance:\\\\n\\\\n```bash\\\\n# Authenticate (if not already)\\\\nsupabase login\\\\n\\\\n# Link to the remote project (if not already)\\\\nsupabase link --project-ref \\\\n\\\\n# Preview what will be applied\\\\nsupabase db push --dry-run\\\\n\\\\n# Apply migrations\\\\nsupabase db push\\\\n```\\\\n\\\\n`db push` applies only migrations that haven't been applied to the remote yet. It tracks this via the `supabase_migrations.schema_migrations` table created automatically on the remote database.\\\\n\\\\nTo also seed a fresh remote instance (dev/staging environments only):\\\\n\\\\n```bash\\\\nsupabase db push --include-seed\\\\n```\\\\n\\\\n\\\\n Never use `--include-seed` on a production database. Seed data is for development and testing.\\\\n\\\\n\\\\n\\\\n### Resetting a remote dev or staging project\\\\n\\\\nIf a dev or staging remote drifts or gets into a messy state, you can wipe it and rebuild it from your local migrations:\\\\n\\\\n```bash\\\\nsupabase db reset --linked\\\\n```\\\\n\\\\nUnlike the default `supabase db reset`, which targets your local database, the `--linked` flag runs against the remote project you connected with `supabase link`: it drops the remote schema, then replays every local migration in order. Add `--include-seed` to reload seed data as well.\\\\n\\\\n\\\\n `db reset --linked` is destructive: it erases all data in the linked remote database. Only run it against throwaway dev or staging projects, and double-check which project you're linked to (`supabase projects list` shows the linked one) before running it. Never use it on production.\\\\n\\\\n\\\\nFor multi-environment setups with CI/CD (feature branches, staging, production), see [Managing Environments](/docs/guides/deployment/managing-environments).\\\\n\\\\n\\\\n## Key commands at a glance\\\\n\\\\n| Command | What it does |\\\\n| -------------------------------------- | ------------------------------------------------------------------------------------------------------- |\\\\n| `supabase init` | Creates `./supabase/config.toml` |\\\\n| `supabase start` | Starts the local stack, applies migrations + seed |\\\\n| `supabase stop` | Stops the local stack (data persists until `db reset`) |\\\\n| `supabase db reset` | Destroys local DB, applies all migrations + seed from scratch |\\\\n| `supabase db reset --linked` | Destroys the **linked remote** DB and rebuilds it from local migrations (destructive; dev/staging only) |\\\\n| `supabase db diff -f ` | Generates a migration by diffing current DB state against a shadow database |\\\\n| `supabase db pull` | Pulls remote schema into a new local migration file |\\\\n| `supabase db push` | Applies pending local migrations to the remote database |\\\\n| `supabase db dump` | Exports remote DB schema (or `--data-only` for data) via `pg_dump` |\\\\n| `supabase migration new ` | Creates an empty migration file |\\\\n| `supabase migration list` | Compares local migrations against remote migration history |\\\\n| `supabase gen types --lang typescript` | Generates TypeScript types from your database schema |\\\\n| `supabase link --project-ref` | Connects local project to a remote Supabase project |\\\\n| `supabase login` | Authenticates with the Supabase platform |\\\\n\\\\nFor the full command reference and every flag, see the [CLI reference](/docs/reference/cli).\\\\n\\\\n\\\\n## Cleaning up generated migrations\\\\n\\\\nWhen `supabase db diff` generates a migration, it may include statements that are technically correct but noisy. Review every generated migration before committing.\\\\n\\\\n\\\\n### Grants\\\\n\\\\nYou may see lines like:\\\\n\\\\n```sql\\\\nGRANT MAINTAIN, REFERENCES, TRIGGER, TRUNCATE ON public.todos TO anon;\\\\nGRANT MAINTAIN, REFERENCES, TRIGGER, TRUNCATE ON public.todos TO authenticated;\\\\nGRANT MAINTAIN, REFERENCES, TRIGGER, TRUNCATE ON public.todos TO service_role;\\\\n```\\\\n\\\\nThese appear because the diff tool treats permissions as part of the schema state. For tables in the `public` schema, these grants are applied by default and the lines are redundant. They're harmless, but if you want clean migrations, you can remove them. Be consistent across your team about whether you keep or remove them.\\\\n\\\\n\\\\n### Revoke/re-grant patterns\\\\n\\\\nSometimes a diff produces:\\\\n\\\\n```sql\\\\nREVOKE ALL ON TABLE public.todos FROM anon;\\\\nGRANT ALL ON TABLE public.todos TO anon;\\\\n```\\\\n\\\\nThis is the diff tool being overly cautious. If you haven't changed permissions, these lines can be safely removed.\\\\n\\\\n\\\\n### Extension statements\\\\n\\\\n`CREATE EXTENSION IF NOT EXISTS ...` may appear. Keep these if the extension is required by your migration. Remove them if the extension is already created by a previous migration or is part of the default Supabase setup.\\\\n\\\\n\\\\n### Known limitations of `db diff`\\\\n\\\\nThe diff is generated by `pg-delta`, the default schema diff engine. (The older [`migra`](https://github.com/djrobstep/migra) engine is still available: set `enabled = false` under `[experimental.pgdelta]` in `config.toml`, or pass `--use-migra`.) No diff engine captures everything. Most notably, DML (INSERT, UPDATE, DELETE) is not tracked, so data changes must be added to the migration manually, and some entities like RLS policy renames and certain view properties don't diff cleanly. See the [full list of caveats](/docs/guides/local-development/declarative-database-schemas#known-caveats) in the declarative schemas guide.\\\\n\\\\nTreat `db diff` output as a draft, not a final migration. When in doubt, review the generated SQL and adjust it manually.\\\\n\\\\n\\\\n## Troubleshooting\\\\n\\\\n**`db reset` fails with a migration error**\\\\n\\\\nThe output will show which migration file failed and the SQL error. Fix the migration file, then run `db reset` again.\\\\n\\\\n**`db push` says migrations are already applied**\\\\n\\\\nThe remote database already has those migrations in its history. Run `supabase migration list` to compare local vs. remote state. If they're out of sync, use `supabase migration repair` to correct the remote history.\\\\n\\\\n**Schema drift: remote was changed outside of migrations**\\\\n\\\\nIf someone modified the remote database directly (via Dashboard, SQL editor, etc.), run `supabase db pull` to capture those changes as a new migration file. Then `supabase db reset` locally to verify everything still works.\\\\n\\\\n**Docker issues on `supabase start`**\\\\n\\\\nEnsure Docker is running and has at least 7 GB of RAM allocated. If containers fail health checks, try:\\\\n\\\\n```bash\\\\nsupabase stop\\\\nsupabase start\\\\n```\\\\n\\\\nIf problems persist, `supabase stop --no-backup` for a clean restart (this removes local database data).\\\\n\\\"},{\\\"title\\\":\\\"Build a User Management App with RedwoodJS\\\",\\\"href\\\":\\\"https://supabase.com/docs/guides/getting-started/tutorials/with-redwoodjs\\\",\\\"content\\\":\\\"# Build a User Management App with RedwoodJS\\\\n\\\\n\\\\n\\\\nThis tutorial demonstrates how to build a basic user management app. The app authenticates and identifies the user, stores their profile information in the database, and allows the user to log in, update their profile details, and upload a profile photo. The app uses:\\\\n\\\\n* [Supabase Database](/docs/guides/database) - a Postgres database for storing your user data and [Row Level Security](/docs/guides/auth#row-level-security) so data is protected and users can only access their own information.\\\\n* [Supabase Auth](/docs/guides/auth) - allow users to sign up and log in.\\\\n* [Supabase Storage](/docs/guides/storage) - allow users to upload a profile photo.\\\\n\\\\n![Supabase User Management example](/docs/img/user-management-demo.png)\\\\n\\\\n\\\\n If you get stuck while working through this guide, you can find the [full example on GitHub](https://github.com/redwoodjs/redwoodjs-supabase-quickstart).\\\\n\\\\n\\\\n\\\\n## About RedwoodJS\\\\n\\\\nA Redwood application is split into two parts: a frontend and a backend. This is represented as two node projects within a single monorepo.\\\\n\\\\nThe frontend project is called **`web`** and the backend project is called **`api`**. For clarity, we will refer to these in prose as **\\\\\\\"sides,\\\\\\\"** that is, the `web side` and the `api side`.\\\\nThey are separate projects because code on the `web side` will end up running in the user's browser while code on the `api side` will run on a server somewhere.\\\\n\\\\n\\\\n Important: When this guide refers to \\\\\\\"API,\\\\\\\" that means the Supabase API and when it refers to `api side`, that means the RedwoodJS `api side`.\\\\n\\\\n\\\\nThe **`api side`** is an implementation of a GraphQL API. The business logic is organized into \\\\\\\"services\\\\\\\" that represent their own internal API and can be called both from external GraphQL requests and other internal services.\\\\n\\\\nThe **`web side`** is built with React. Redwood's router makes it simple to map URL paths to React \\\\\\\"Page\\\\\\\" components (and automatically code-split your app on each route).\\\\nPages may contain a \\\\\\\"Layout\\\\\\\" component to wrap content. They also contain \\\\\\\"Cells\\\\\\\" and regular React components.\\\\nCells allow you to declaratively manage the lifecycle of a component that fetches and displays data.\\\\n\\\\nFor the sake of consistency with the other framework tutorials, we'll build this app a little differently than normal.\\\\nWe ***won't use*** Prisma to connect to the Supabase Postgres database or [Prisma migrations](https://redwoodjs.com/docs/cli-commands#prisma-migrate) as one typically might in a Redwood app.\\\\nInstead, we'll rely on the Supabase client to do some of the work on the **`web`** side and use the client again on the **`api`** side to do data fetching as well.\\\\n\\\\nThat means you will want to refrain from running any `yarn rw prisma migrate` commands and also double check your build commands on deployment to ensure Prisma won't reset your database. Prisma currently doesn't support cross-schema foreign keys, so introspecting the schema fails due\\\\nto how your Supabase `public` schema references the `auth.users`.\\\\n\\\\n\\\\n## Project setup\\\\n\\\\nBefore you start building you need to set up the Database and API. You can do this by starting a new Project in Supabase and then creating a \\\\\\\"schema\\\\\\\" inside the database.\\\\n\\\\n\\\\n### Create a project\\\\n\\\\n1. [Create a new project](/dashboard) in the Supabase Dashboard.\\\\n2. Enter your project details.\\\\n3. Wait for the new database to launch.\\\\n\\\\n\\\\n### Set up the database schema\\\\n\\\\nNow set up the database schema. You can use the \\\\\\\"User Management Starter\\\\\\\" quickstart in the SQL Editor, or you can copy/paste the SQL from below and run it.\\\\n\\\\n\\\\n \\\\n 1. Go to the [SQL Editor](/dashboard/project/_/sql) page in the Dashboard.\\\\n 2. Click **User Management Starter** under the **Community > Quickstarts** tab.\\\\n 3. Click **Run**.\\\\n\\\\n \\\\n You can pull the database schema down to your local project by running the `db pull` command. Read the [local development docs](/docs/guides/cli/local-development#link-your-project) for detailed instructions.\\\\n\\\\n ```bash\\\\n supabase link --project-ref \\\\n # You can get from your project's dashboard URL: https://supabase.com/dashboard/project/\\\\n supabase db pull\\\\n ```\\\\n \\\\n \\\\n\\\\n \\\\n \\\\n When working locally you can run the following command to create a new migration file:\\\\n \\\\n\\\\n ```bash\\\\n supabase migration new user_management_starter\\\\n ```\\\\n\\\\n ```sql\\\\n -- Create a table for public profiles\\\\n create table profiles (\\\\n id uuid references auth.users not null primary key,\\\\n updated_at timestamp with time zone,\\\\n username text unique,\\\\n full_name text,\\\\n avatar_url text,\\\\n website text,\\\\n\\\\n constraint username_length check (char_length(username) >= 3)\\\\n );\\\\n\\\\n -- Grant the privileges roles need\\\\n GRANT SELECT ON public.profiles TO anon;\\\\n GRANT SELECT, INSERT, UPDATE ON public.profiles TO authenticated;\\\\n\\\\n -- Set up Row Level Security (RLS)\\\\n -- See https://supabase.com/docs/guides/database/postgres/row-level-security for more details.\\\\n alter table profiles\\\\n enable row level security;\\\\n\\\\n create policy \\\\\\\"Public profiles are viewable by everyone.\\\\\\\" on profiles\\\\n for select using (true);\\\\n\\\\n create policy \\\\\\\"Users can insert their own profile.\\\\\\\" on profiles\\\\n for insert with check ((select auth.uid()) = id);\\\\n\\\\n create policy \\\\\\\"Users can update own profile.\\\\\\\" on profiles\\\\n for update using ((select auth.uid()) = id);\\\\n\\\\n -- This trigger automatically creates a profile entry when a new user signs up via Supabase Auth.\\\\n -- See https://supabase.com/docs/guides/auth/managing-user-data#using-triggers for more details.\\\\n create function public.handle_new_user()\\\\n returns trigger\\\\n set search_path = ''\\\\n as $$\\\\n begin\\\\n insert into public.profiles (id, full_name, avatar_url)\\\\n values (new.id, new.raw_user_meta_data->>'full_name', new.raw_user_meta_data->>'avatar_url');\\\\n return new;\\\\n end;\\\\n $$ language plpgsql security definer;\\\\n create trigger on_auth_user_created\\\\n after insert on auth.users\\\\n for each row execute procedure public.handle_new_user();\\\\n\\\\n -- Set up Storage!\\\\n insert into storage.buckets (id, name)\\\\n values ('avatars', 'avatars');\\\\n\\\\n -- Set up access controls for storage. Allows downloading object with public key\\\\n -- See https://supabase.com/docs/guides/storage/security/access-control#policy-examples for more details.\\\\n create policy \\\\\\\"Avatar images are publicly accessible.\\\\\\\" on storage.objects\\\\n for select using (bucket_id = 'avatars' and storage.allow_any_operation(array['object.get_authenticated_info', 'object.get_authenticated']));\\\\n\\\\n create policy \\\\\\\"Anyone can upload an avatar.\\\\\\\" on storage.objects\\\\n for insert with check (bucket_id = 'avatars');\\\\n\\\\n create policy \\\\\\\"Anyone can update their own avatar.\\\\\\\" on storage.objects\\\\n for update using ((select auth.uid()) = owner) with check (bucket_id = 'avatars');\\\\n ```\\\\n \\\\n\\\\n\\\\n\\\\n### Get API details\\\\n\\\\nNow that you've created some database tables, you are ready to insert data using the auto-generated API.\\\\n\\\\nTo do this, you need to get the Project URL and key from [the project **Connect** dialog](/dashboard/project/_?showConnect=true\\\\\\\\&connectTab=frameworks\\\\\\\\&framework=).\\\\n\\\\n[Read the API keys docs](/docs/guides/getting-started/api-keys) for a full explanation of all key types and their uses.\\\\n\\\\n\\\\n Supabase is changing the way keys work to improve project security and developer experience. You can [read the full announcement on GitHub](https://github.com/orgs/supabase/discussions/29260).\\\\n\\\\n The older `anon` and `service_role` keys will work until the end of 2026 but **we strongly encourage switching to and using** the new publishable (`sb_publishable_xxx`) and secret (`sb_secret_xxx`) keys now.\\\\n\\\\n In most cases, you can get keys from [the Project's **Connect** dialog](/dashboard/project/_?showConnect=true\\\\\\\\&connectTab=frameworks\\\\\\\\&framework=), but if you want a specific key, you can find them in the [**Settings > API Keys**](/dashboard/project/_/settings/api-keys/) section of the Dashboard.\\\\n\\\\n * **For legacy keys**, copy the `anon` key for client-side operations and the `service_role` key for server-side operations from the **Legacy API Keys** tab.\\\\n * **For new keys**, open the **API Keys** tab, if you don't have a publishable key already, click **Create new API Keys**, and copy the value from the **Publishable key** section.\\\\n\\\\n\\\\n\\\\n## Building the app\\\\n\\\\nLet's start building the RedwoodJS app from scratch.\\\\n\\\\n\\\\n RedwoodJS requires Node.js `>= 14.x <= 16.x` and Yarn `>= 1.15`.\\\\n\\\\n\\\\nMake sure you have installed yarn since RedwoodJS relies on it to [manage its packages in workspaces](https://classic.yarnpkg.com/lang/en/docs/workspaces/) for its `web` and `api` \\\\\\\"sides.\\\\\\\"\\\\n\\\\n\\\\n### Initialize a RedwoodJS app\\\\n\\\\nWe can use [Create Redwood App](https://redwoodjs.com/docs/quick-start) command to initialize\\\\nan app called `supabase-redwoodjs`:\\\\n\\\\n```bash\\\\nyarn create redwood-app supabase-redwoodjs\\\\ncd supabase-redwoodjs\\\\n```\\\\n\\\\nWhile the app is installing, you should see:\\\\n\\\\n```bash\\\\n✔ Creating Redwood app\\\\n ✔ Checking node and yarn compatibility\\\\n ✔ Creating directory 'supabase-redwoodjs'\\\\n✔ Installing packages\\\\n ✔ Running 'yarn install'... (This could take a while)\\\\n✔ Convert TypeScript files to JavaScript\\\\n✔ Generating types\\\\n\\\\nThanks for trying out Redwood!\\\\n```\\\\n\\\\nThen let's install the only additional dependency [supabase-js](https://github.com/supabase/supabase-js) by running the `setup auth` command:\\\\n\\\\n```bash\\\\nyarn redwood setup auth supabase\\\\n```\\\\n\\\\nWhen prompted:\\\\n\\\\n> Overwrite existing /api/src/lib/auth.\\\\\\\\[jt]s?\\\\n\\\\nSay, **yes** and it will setup the Supabase client in your app and also provide hooks used with Supabase authentication.\\\\n\\\\n```bash\\\\n✔ Generating auth lib...\\\\n ✔ Successfully wrote file `./api/src/lib/auth.js`\\\\n ✔ Adding auth config to web...\\\\n ✔ Adding auth config to GraphQL API...\\\\n ✔ Adding required web packages...\\\\n ✔ Installing packages...\\\\n ✔ One more thing...\\\\n\\\\n You will need to add your Supabase URL (SUPABASE_URL), public API KEY,\\\\n and JWT SECRET (SUPABASE_KEY, and SUPABASE_JWT_SECRET) to your .env file.\\\\n```\\\\n\\\\nNext, we want to save the environment variables in a `.env`.\\\\nWe need the `API URL` as well as the key and `jwt_secret` that you copied [earlier](#get-api-details).\\\\n\\\\n\\\\n \\\\n ```bash name=.env\\\\n SUPABASE_URL=YOUR_SUPABASE_URL\\\\n SUPABASE_KEY=YOUR_SUPABASE_PUBLISHABLE_KEY\\\\n SUPABASE_JWT_SECRET=YOUR_SUPABASE_JWT_SECRET\\\\n ```\\\\n \\\\n\\\\n\\\\nAnd finally, you will also need to save **just** the `web side` environment variables to the `redwood.toml`.\\\\n\\\\n\\\\n \\\\n ```bash name=redwood.toml\\\\n [web]\\\\n title = \\\\\\\"Supabase Redwood Tutorial\\\\\\\"\\\\n port = 8910\\\\n apiProxyPath = \\\\\\\"/.redwood/functions\\\\\\\"\\\\n includeEnvironmentVariables = [\\\\\\\"SUPABASE_URL\\\\\\\", \\\\\\\"SUPABASE_KEY\\\\\\\"]\\\\n [api]\\\\n port = 8911\\\\n [browser]\\\\n open = true\\\\n ```\\\\n \\\\n\\\\n\\\\nThese variables will be exposed on the browser, and that's completely fine.\\\\nThey allow your web app to initialize the Supabase client with your publishable key\\\\nsince we have [Row Level Security](/docs/guides/auth#row-level-security) enabled on our Database.\\\\n\\\\nYou'll see these being used to configure your Supabase client in `web/src/App.js`:\\\\n\\\\n\\\\n \\\\n ```js name=web/src/App.js\\\\n // ... Redwood imports\\\\n import { AuthProvider } from '@redwoodjs/auth'\\\\n import { createClient } from '@supabase/supabase-js'\\\\n\\\\n // ...\\\\n\\\\n const supabase = createClient(process.env.SUPABASE_URL, process.env.SUPABASE_KEY)\\\\n\\\\n const App = () => (\\\\n \\\\n \\\\n \\\\n \\\\n \\\\n \\\\n \\\\n \\\\n \\\\n )\\\\n\\\\n export default App\\\\n ```\\\\n \\\\n\\\\n\\\\n\\\\n### App styling (optional)\\\\n\\\\nAn optional step is to update the CSS file `web/src/index.css` to make the app look better.\\\\nYou can find the full contents of this file [in the example repository](https://raw.githubusercontent.com/supabase/supabase/master/examples/user-management/react-user-management/src/index.css).\\\\n\\\\n\\\\n### Start RedwoodJS and your first page\\\\n\\\\nLet's test our setup at the moment by starting up the app:\\\\n\\\\n```bash\\\\nyarn rw dev\\\\n```\\\\n\\\\n\\\\n `rw` is an alias for `redwood`, as in `yarn rw` to run Redwood CLI commands.\\\\n\\\\n\\\\nYou should see a \\\\\\\"Welcome to RedwoodJS\\\\\\\" page and a message about not having any pages yet.\\\\n\\\\nSo, let's create a \\\\\\\"home\\\\\\\" page:\\\\n\\\\n```bash\\\\nyarn rw generate page home /\\\\n\\\\n✔ Generating page files...\\\\n ✔ Successfully wrote file `./web/src/pages/HomePage/HomePage.stories.js`\\\\n ✔ Successfully wrote file `./web/src/pages/HomePage/HomePage.test.js`\\\\n ✔ Successfully wrote file `./web/src/pages/HomePage/HomePage.js`\\\\n✔ Updating routes file...\\\\n✔ Generating types ...\\\\n```\\\\n\\\\n\\\\n The `/` is important here as it creates a root level route.\\\\n\\\\n\\\\nYou can stop the `dev` server if you want; to see your changes, just be sure to run `yarn rw dev` again.\\\\n\\\\nYou should see the `Home` page route in `web/src/Routes.js`:\\\\n\\\\n\\\\n \\\\n ```bash name=web/src/Routes.js\\\\n import { Router, Route } from '@redwoodjs/router'\\\\n\\\\n const Routes = () => {\\\\n return (\\\\n \\\\n \\\\n \\\\n \\\\n )\\\\n }\\\\n\\\\n export default Routes\\\\n ```\\\\n \\\\n\\\\n\\\\n\\\\n### Set up a login component\\\\n\\\\nLet's set up a Redwood component to manage logins and sign ups. We'll use Magic Links, so users can sign in with their email without using passwords.\\\\n\\\\n```bash\\\\nyarn rw g component auth\\\\n\\\\n ✔ Generating component files...\\\\n ✔ Successfully wrote file `./web/src/components/Auth/Auth.test.js`\\\\n ✔ Successfully wrote file `./web/src/components/Auth/Auth.stories.js`\\\\n ✔ Successfully wrote file `./web/src/components/Auth/Auth.js`\\\\n\\\\n```\\\\n\\\\nNow, update the `Auth.js` component to contain:\\\\n\\\\n\\\\n \\\\n ```jsx name=/web/src/components/Auth/Auth.js\\\\n import { useState } from 'react'\\\\n import { useAuth } from '@redwoodjs/auth'\\\\n\\\\n const Auth = () => {\\\\n const { logIn } = useAuth()\\\\n const [loading, setLoading] = useState(false)\\\\n const [email, setEmail] = useState('')\\\\n\\\\n const handleLogin = async (email) => {\\\\n try {\\\\n setLoading(true)\\\\n const { error } = await logIn({ email })\\\\n if (error) throw error\\\\n alert('Check your email for the login link!')\\\\n } catch (error) {\\\\n alert(error.error_description || error.message)\\\\n } finally {\\\\n setLoading(false)\\\\n }\\\\n }\\\\n\\\\n return (\\\\n
    \\\\n
    \\\\n

    Supabase + RedwoodJS

    \\\\n

    Sign in via magic link with your email below

    \\\\n
    \\\\n setEmail(e.target.value)}\\\\n />\\\\n
    \\\\n
    \\\\n {\\\\n e.preventDefault()\\\\n handleLogin(email)\\\\n }}\\\\n className={'button block'}\\\\n disabled={loading}\\\\n >\\\\n {loading ? Loading : Send magic link}\\\\n \\\\n
    \\\\n
    \\\\n
    \\\\n )\\\\n }\\\\n\\\\n export default Auth\\\\n ```\\\\n
    \\\\n
    \\\\n\\\\n\\\\n### Set up an account component\\\\n\\\\nAfter a user is signed in we can allow them to edit their profile details and manage their account.\\\\n\\\\nLet's create a new component for that called `Account.js`.\\\\n\\\\n```bash\\\\nyarn rw g component account\\\\n\\\\n ✔ Generating component files...\\\\n ✔ Successfully wrote file `./web/src/components/Account/Account.test.js`\\\\n ✔ Successfully wrote file `./web/src/components/Account/Account.stories.js`\\\\n ✔ Successfully wrote file `./web/src/components/Account/Account.js`\\\\n```\\\\n\\\\nAnd then update the file to contain:\\\\n\\\\n\\\\n \\\\n ```jsx name=web/src/components/Account/Account.js\\\\n import { useState, useEffect } from 'react'\\\\n import { useAuth } from '@redwoodjs/auth'\\\\n\\\\n const Account = () => {\\\\n const { client: supabase, currentUser, logOut } = useAuth()\\\\n const [loading, setLoading] = useState(true)\\\\n const [username, setUsername] = useState(null)\\\\n const [website, setWebsite] = useState(null)\\\\n const [avatar_url, setAvatarUrl] = useState(null)\\\\n\\\\n useEffect(() => {\\\\n getProfile()\\\\n }, [supabase.auth.session])\\\\n\\\\n async function getProfile() {\\\\n try {\\\\n setLoading(true)\\\\n const user = supabase.auth.user()\\\\n\\\\n const { data, error, status } = await supabase\\\\n .from('profiles')\\\\n .select(`username, website, avatar_url`)\\\\n .eq('id', user.id)\\\\n .single()\\\\n\\\\n if (error && status !== 406) {\\\\n throw error\\\\n }\\\\n\\\\n if (data) {\\\\n setUsername(data.username)\\\\n setWebsite(data.website)\\\\n setAvatarUrl(data.avatar_url)\\\\n }\\\\n } catch (error) {\\\\n alert(error.message)\\\\n } finally {\\\\n setLoading(false)\\\\n }\\\\n }\\\\n\\\\n async function updateProfile({ username, website, avatar_url }) {\\\\n try {\\\\n setLoading(true)\\\\n const user = supabase.auth.user()\\\\n\\\\n const updates = {\\\\n id: user.id,\\\\n username,\\\\n website,\\\\n avatar_url,\\\\n updated_at: new Date(),\\\\n }\\\\n\\\\n const { error } = await supabase.from('profiles').upsert(updates, {\\\\n returning: 'minimal', // Don't return the value after inserting\\\\n })\\\\n\\\\n if (error) {\\\\n throw error\\\\n }\\\\n\\\\n alert('Updated profile!')\\\\n } catch (error) {\\\\n alert(error.message)\\\\n } finally {\\\\n setLoading(false)\\\\n }\\\\n }\\\\n\\\\n return (\\\\n
    \\\\n
    \\\\n

    Supabase + RedwoodJS

    \\\\n

    Your profile

    \\\\n
    \\\\n
    \\\\n \\\\n \\\\n
    \\\\n
    \\\\n \\\\n setUsername(e.target.value)}\\\\n />\\\\n
    \\\\n
    \\\\n \\\\n setWebsite(e.target.value)}\\\\n />\\\\n
    \\\\n\\\\n
    \\\\n updateProfile({ username, website, avatar_url })}\\\\n disabled={loading}\\\\n >\\\\n {loading ? 'Loading ...' : 'Update'}\\\\n \\\\n
    \\\\n\\\\n
    \\\\n \\\\n
    \\\\n
    \\\\n
    \\\\n
    \\\\n )\\\\n }\\\\n\\\\n export default Account\\\\n ```\\\\n
    \\\\n
    \\\\n\\\\nYou'll see the use of `useAuth()` several times. Redwood's `useAuth` hook provides convenient ways to access\\\\n`logIn`, `logOut`, `currentUser`, and access the `supabase` authenticate client. We'll use it to get an instance\\\\nof the Supabase client to interact with your API.\\\\n\\\\n\\\\n### Update home page\\\\n\\\\nWith all the components in place, update your `HomePage` page to use them:\\\\n\\\\n\\\\n \\\\n ```jsx name=web/src/pages/HomePage/HomePage.js\\\\n import { useAuth } from '@redwoodjs/auth'\\\\n import { MetaTags } from '@redwoodjs/web'\\\\n\\\\n import Account from 'src/components/Account'\\\\n import Auth from 'src/components/Auth'\\\\n\\\\n const HomePage = () => {\\\\n const { isAuthenticated } = useAuth()\\\\n\\\\n return (\\\\n <>\\\\n \\\\n {!isAuthenticated ? : }\\\\n \\\\n )\\\\n }\\\\n\\\\n export default HomePage\\\\n ```\\\\n \\\\n\\\\n\\\\n\\\\n What we're doing here is showing the sign in form if you aren't logged in and your account profile if you are.\\\\n\\\\n\\\\n\\\\n## Profile photos\\\\n\\\\nNext, add a way for users to upload a profile photo. Supabase configures every project with [Storage](/docs/guides/storage) for managing large files like photos and videos.\\\\n\\\\n\\\\n### Create an upload widget\\\\n\\\\nLet's create an avatar for the user so that they can upload a profile photo. We can start by creating a new component:\\\\n\\\\n```bash\\\\nyarn rw g component avatar\\\\n ✔ Generating component files...\\\\n ✔ Successfully wrote file `./web/src/components/Avatar/Avatar.test.js`\\\\n ✔ Successfully wrote file `./web/src/components/Avatar/Avatar.stories.js`\\\\n ✔ Successfully wrote file `./web/src/components/Avatar/Avatar.js`\\\\n```\\\\n\\\\nNow, update your Avatar component to contain the following widget:\\\\n\\\\n\\\\n \\\\n ```jsx name=web/src/components/Avatar/Avatar.js\\\\n import { useEffect, useState } from 'react'\\\\n import { useAuth } from '@redwoodjs/auth'\\\\n\\\\n const Avatar = ({ url, size, onUpload }) => {\\\\n const { client: supabase } = useAuth()\\\\n\\\\n const [avatarUrl, setAvatarUrl] = useState(null)\\\\n const [uploading, setUploading] = useState(false)\\\\n\\\\n useEffect(() => {\\\\n if (url) downloadImage(url)\\\\n }, [url])\\\\n\\\\n async function downloadImage(path) {\\\\n try {\\\\n const { data, error } = await supabase.storage.from('avatars').download(path)\\\\n if (error) {\\\\n throw error\\\\n }\\\\n const url = URL.createObjectURL(data)\\\\n setAvatarUrl(url)\\\\n } catch (error) {\\\\n console.log('Error downloading image: ', error.message)\\\\n }\\\\n }\\\\n\\\\n async function uploadAvatar(event) {\\\\n try {\\\\n setUploading(true)\\\\n\\\\n if (!event.target.files || event.target.files.length === 0) {\\\\n throw new Error('You must select an image to upload.')\\\\n }\\\\n\\\\n const file = event.target.files[0]\\\\n const fileExt = file.name.split('.').pop()\\\\n const fileName = `${Math.random()}.${fileExt}`\\\\n const filePath = `${fileName}`\\\\n\\\\n const { error: uploadError } = await supabase.storage.from('avatars').upload(filePath, file)\\\\n\\\\n if (uploadError) {\\\\n throw uploadError\\\\n }\\\\n\\\\n onUpload(filePath)\\\\n } catch (error) {\\\\n alert(error.message)\\\\n } finally {\\\\n setUploading(false)\\\\n }\\\\n }\\\\n\\\\n return (\\\\n
    \\\\n {avatarUrl ? (\\\\n \\\\n ) : (\\\\n
    \\\\n )}\\\\n
    \\\\n \\\\n \\\\n
    \\\\n
    \\\\n )\\\\n }\\\\n\\\\n export default Avatar\\\\n ```\\\\n \\\\n\\\\n\\\\n\\\\n### Launch!\\\\n\\\\nOnce that's done, run this in a terminal window to launch the `dev` server:\\\\n\\\\n```bash\\\\nyarn rw dev\\\\n```\\\\n\\\\nAnd then open the browser to [localhost:8910](http://localhost:8910) and you should see the completed app.\\\\n\\\\n![Supabase RedwoodJS](/docs/img/supabase-redwoodjs-demo.png)\\\\n\\\\nAt this stage you have a fully functional application!\\\\n\\\\n\\\\n## See also\\\\n\\\\n* Learn more about [RedwoodJS](https://redwoodjs.com)\\\\n* Visit the [RedwoodJS Discourse Community](https://community.redwoodjs.com)\\\\n\\\"},{\\\"title\\\":\\\"Row Level Security\\\",\\\"href\\\":\\\"https://supabase.com/docs/guides/database/postgres/row-level-security\\\",\\\"content\\\":\\\"# Row Level Security\\\\n\\\\nSecure your data using Postgres Row Level Security.\\\\n\\\\nWhen you need granular authorization rules, nothing beats Postgres's [Row Level Security (RLS)](https://www.postgresql.org/docs/current/ddl-rowsecurity.html).\\\\n\\\\n\\\\n## Row Level Security in Supabase\\\\n\\\\n\\\\n Supabase allows convenient and secure data access from the browser, as long as you enable RLS.\\\\n\\\\n RLS *must* always be enabled on any tables stored in an exposed schema. By default, this is the `public` schema.\\\\n\\\\n RLS is enabled by default on tables created with the Table Editor in the dashboard. If you create one in raw SQL or with the SQL editor, remember to enable RLS yourself and grant only the permissions each Postgres role needs.\\\\n\\\\n ```sql\\\\n GRANT SELECT ON . TO anon;\\\\n GRANT SELECT, INSERT, UPDATE, DELETE ON . TO authenticated;\\\\n GRANT SELECT, INSERT, UPDATE, DELETE ON . TO service_role;\\\\n\\\\n alter table .\\\\n enable row level security;\\\\n ```\\\\n\\\\n\\\\nRLS is incredibly powerful and flexible, allowing you to write complex SQL rules that fit your unique business needs. RLS can be combined with [Supabase Auth](/docs/guides/auth) for end-to-end user security from the browser to the database.\\\\n\\\\nRLS is a Postgres primitive and can provide \\\\\\\"[defense in depth](https://en.wikipedia.org/wiki/Defense_in_depth_\\\\\\\\(computing\\\\\\\\))\\\\\\\" to protect your data from malicious actors even when accessed through third-party tooling.\\\\n\\\\n\\\\n## Policies\\\\n\\\\n[Policies](https://www.postgresql.org/docs/current/sql-createpolicy.html) are Postgres's rule engine. Policies are easy to understand once you get the hang of them. Each policy is attached to a table, and the policy is executed every time a table is accessed.\\\\n\\\\nYou can just think of them as adding a `WHERE` clause to every query. For example a policy like this ...\\\\n\\\\n```sql\\\\ncreate policy \\\\\\\"Individuals can view their own todos.\\\\\\\"\\\\non todos for select\\\\nusing ( (select auth.uid()) = user_id );\\\\n```\\\\n\\\\n.. would translate to this whenever a user tries to select from the todos table:\\\\n\\\\n```sql\\\\nselect *\\\\nfrom todos\\\\nwhere auth.uid() = todos.user_id;\\\\n-- Policy is implicitly added.\\\\n```\\\\n\\\\n\\\\n## Enabling Row Level Security\\\\n\\\\nYou can enable RLS for any table using the `enable row level security` clause:\\\\n\\\\n```sql\\\\nalter table \\\\\\\"table_name\\\\\\\" enable row level security;\\\\n```\\\\n\\\\nOnce you have enabled RLS, no data will be accessible via the [API](/docs/guides/api) when using a publishable key, until you create policies.\\\\n\\\\n\\\\n## Auto-enable RLS for new tables\\\\n\\\\nIf you want RLS enabled automatically for new tables, you can create an event trigger that runs after table creation. This uses a Postgres [event trigger](/docs/guides/database/postgres/event-triggers) to call `ALTER TABLE ... ENABLE ROW LEVEL SECURITY` on each newly created table.\\\\n\\\\n```sql\\\\nCREATE OR REPLACE FUNCTION rls_auto_enable()\\\\nRETURNS EVENT_TRIGGER\\\\nLANGUAGE plpgsql\\\\nSECURITY DEFINER\\\\nSET search_path = pg_catalog\\\\nAS $$\\\\nDECLARE\\\\n cmd record;\\\\nBEGIN\\\\n FOR cmd IN\\\\n SELECT *\\\\n FROM pg_event_trigger_ddl_commands()\\\\n WHERE command_tag IN ('CREATE TABLE', 'CREATE TABLE AS', 'SELECT INTO')\\\\n AND object_type IN ('table','partitioned table')\\\\n LOOP\\\\n IF cmd.schema_name IS NOT NULL AND cmd.schema_name IN ('public') AND cmd.schema_name NOT IN ('pg_catalog','information_schema') AND cmd.schema_name NOT LIKE 'pg_toast%' AND cmd.schema_name NOT LIKE 'pg_temp%' THEN\\\\n BEGIN\\\\n EXECUTE format('alter table if exists %s enable row level security', cmd.object_identity);\\\\n RAISE LOG 'rls_auto_enable: enabled RLS on %', cmd.object_identity;\\\\n EXCEPTION\\\\n WHEN OTHERS THEN\\\\n RAISE LOG 'rls_auto_enable: failed to enable RLS on %', cmd.object_identity;\\\\n END;\\\\n ELSE\\\\n RAISE LOG 'rls_auto_enable: skip % (either system schema or not in enforced list: %.)', cmd.object_identity, cmd.schema_name;\\\\n END IF;\\\\n END LOOP;\\\\nEND;\\\\n$$;\\\\n\\\\nDROP EVENT TRIGGER IF EXISTS ensure_rls;\\\\nCREATE EVENT TRIGGER ensure_rls\\\\nON ddl_command_end\\\\nWHEN TAG IN ('CREATE TABLE', 'CREATE TABLE AS', 'SELECT INTO')\\\\nEXECUTE FUNCTION rls_auto_enable();\\\\n```\\\\n\\\\nNote that this applies to tables created after the trigger is installed. Existing tables still need RLS enabled manually.\\\\n\\\\n\\\\n When a request is made without an authenticated user (e.g., no access token is provided or the session has expired), `auth.uid()` returns `null`.\\\\n\\\\n This means that a policy like:\\\\n\\\\n ```sql\\\\n USING (auth.uid() = user_id)\\\\n ```\\\\n\\\\n will silently fail for unauthenticated users, because:\\\\n\\\\n ```sql\\\\n null = user_id\\\\n ```\\\\n\\\\n is always false in SQL.\\\\n\\\\n To avoid confusion and make your intention clear, we recommend explicitly checking for authentication:\\\\n\\\\n ```sql\\\\n USING (auth.uid() IS NOT NULL AND auth.uid() = user_id)\\\\n ```\\\\n\\\\n\\\\n\\\\n## Authenticated and unauthenticated roles\\\\n\\\\nSupabase maps every request to one of the roles:\\\\n\\\\n* `anon`: an unauthenticated request (the user is not logged in)\\\\n* `authenticated`: an authenticated request (the user is logged in)\\\\n\\\\nThese are actually [Postgres Roles](/docs/guides/database/postgres/roles). You can use these roles within your Policies using the `TO` clause:\\\\n\\\\n```sql\\\\ncreate policy \\\\\\\"Profiles are viewable by everyone\\\\\\\"\\\\non profiles for select\\\\nto authenticated, anon\\\\nusing ( true );\\\\n\\\\n-- OR\\\\n\\\\ncreate policy \\\\\\\"Public profiles are viewable only by authenticated users\\\\\\\"\\\\non profiles for select\\\\nto authenticated\\\\nusing ( true );\\\\n```\\\\n\\\\n\\\\n Using the `anon` Postgres role is different from an [anonymous user](/docs/guides/auth/auth-anonymous) in Supabase Auth. An anonymous user assumes the `authenticated` role to access the database and can be differentiated from a permanent user by checking the `is_anonymous` claim in the JWT.\\\\n\\\\n\\\\n\\\\n## Creating policies\\\\n\\\\nPolicies are SQL logic that you attach to a Postgres table. You can attach as many policies as you want to each table.\\\\n\\\\nSupabase provides some [helpers](#helper-functions) that simplify RLS if you're using Supabase Auth. We'll use these helpers to illustrate some basic policies:\\\\n\\\\n\\\\n### SELECT policies\\\\n\\\\nYou can specify select policies with the `using` clause.\\\\n\\\\nLet's say you have a table called `profiles` in the public schema and you want to enable read access to everyone.\\\\n\\\\n```sql\\\\n-- 1. Create table\\\\ncreate table profiles (\\\\n id uuid primary key,\\\\n user_id uuid references auth.users,\\\\n avatar_url text\\\\n);\\\\n\\\\n-- 2. Enable RLS\\\\nalter table profiles enable row level security;\\\\n\\\\n-- 3. Create Policy\\\\ncreate policy \\\\\\\"Public profiles are visible to everyone.\\\\\\\"\\\\non profiles for select\\\\nto anon -- the Postgres Role (recommended)\\\\nusing ( true ); -- the actual Policy\\\\n```\\\\n\\\\nAlternatively, if you only wanted users to be able to see their own profiles:\\\\n\\\\n```sql\\\\ncreate policy \\\\\\\"User can see their own profile only.\\\\\\\"\\\\non profiles\\\\nfor select using ( (select auth.uid()) = user_id );\\\\n```\\\\n\\\\n\\\\n### INSERT policies\\\\n\\\\nYou can specify insert policies with the `with check` clause. The `with check` expression ensures that any new row data adheres to the policy constraints.\\\\n\\\\nLet's say you have a table called `profiles` in the public schema and you only want users to be able to create a profile for themselves. In that case, we want to check their User ID matches the value that they are trying to insert:\\\\n\\\\n```sql\\\\n-- 1. Create table\\\\ncreate table profiles (\\\\n id uuid primary key,\\\\n user_id uuid references auth.users,\\\\n avatar_url text\\\\n);\\\\n\\\\n-- 2. Enable RLS\\\\nalter table profiles enable row level security;\\\\n\\\\n-- 3. Create Policy\\\\ncreate policy \\\\\\\"Users can create a profile.\\\\\\\"\\\\non profiles for insert\\\\nto authenticated -- the Postgres Role (recommended)\\\\nwith check ( (select auth.uid()) = user_id ); -- the actual Policy\\\\n```\\\\n\\\\n\\\\n### UPDATE policies\\\\n\\\\nYou can specify update policies by combining both the `using` and `with check` expressions.\\\\n\\\\nThe `using` clause represents the condition that must be true for the update to be allowed, and `with check` clause ensures that the updates made adhere to the policy constraints.\\\\n\\\\nLet's say you have a table called `profiles` in the public schema and you only want users to be able to update their own profile.\\\\n\\\\nYou can create a policy where the `using` clause checks if the user owns the profile being updated. And the `with check` clause ensures that, in the resultant row, users do not change the `user_id` to a value that is not equal to their User ID, maintaining that the modified profile still meets the ownership condition.\\\\n\\\\n```sql\\\\n-- 1. Create table\\\\ncreate table profiles (\\\\n id uuid primary key,\\\\n user_id uuid references auth.users,\\\\n avatar_url text\\\\n);\\\\n\\\\n-- 2. Enable RLS\\\\nalter table profiles enable row level security;\\\\n\\\\n-- 3. Create Policy\\\\ncreate policy \\\\\\\"Users can update their own profile.\\\\\\\"\\\\non profiles for update\\\\nto authenticated -- the Postgres Role (recommended)\\\\nusing ( (select auth.uid()) = user_id ) -- checks if the existing row complies with the policy expression\\\\nwith check ( (select auth.uid()) = user_id ); -- checks if the new row complies with the policy expression\\\\n```\\\\n\\\\nIf no `with check` expression is defined, then the `using` expression will be used both to determine which rows are visible (normal USING case) and which new rows will be allowed to be added (WITH CHECK case).\\\\n\\\\n\\\\n To perform an `UPDATE` operation, a corresponding [`SELECT` policy](#select-policies) is required. Without a `SELECT` policy, the `UPDATE` operation will not work as expected.\\\\n\\\\n\\\\n\\\\n### DELETE policies\\\\n\\\\nYou can specify delete policies with the `using` clause.\\\\n\\\\nLet's say you have a table called `profiles` in the public schema and you only want users to be able to delete their own profile:\\\\n\\\\n```sql\\\\n-- 1. Create table\\\\ncreate table profiles (\\\\n id uuid primary key,\\\\n user_id uuid references auth.users,\\\\n avatar_url text\\\\n);\\\\n\\\\n-- 2. Enable RLS\\\\nalter table profiles enable row level security;\\\\n\\\\n-- 3. Create Policy\\\\ncreate policy \\\\\\\"Users can delete a profile.\\\\\\\"\\\\non profiles for delete\\\\nto authenticated -- the Postgres Role (recommended)\\\\nusing ( (select auth.uid()) = user_id ); -- the actual Policy\\\\n```\\\\n\\\\n\\\\n### Views\\\\n\\\\nViews bypass RLS by default because they are usually created with the `postgres` user. This is a feature of Postgres, which automatically creates views with `security definer`.\\\\n\\\\nIn Postgres 15 and above, you can make a view obey the RLS policies of the underlying tables when invoked by `anon` and `authenticated` roles by setting `security_invoker = true`.\\\\n\\\\n```sql\\\\ncreate view \\\\nwith(security_invoker = true)\\\\nas select \\\\n```\\\\n\\\\nIn older versions of Postgres, protect your views by revoking access from the `anon` and `authenticated` roles, or by putting them in an unexposed schema.\\\\n\\\\n\\\\n## Helper functions\\\\n\\\\nSupabase provides some helper functions that make it easier to write Policies.\\\\n\\\\n\\\\n### `auth.uid()`\\\\n\\\\nReturns the ID of the user making the request.\\\\n\\\\n\\\\n### `auth.jwt()`\\\\n\\\\n\\\\n Not all information present in the JWT should be used in RLS policies. For instance, creating an RLS policy that relies on the `user_metadata` claim can create security issues in your application as this information can be modified by authenticated end users.\\\\n\\\\n\\\\nReturns the JWT of the user making the request. Anything that you store in the user's `raw_app_meta_data` column or the `raw_user_meta_data` column will be accessible using this function. It's important to know the distinction between these two:\\\\n\\\\n* `raw_user_meta_data` - can be updated by the authenticated user using the `supabase.auth.update()` function. It is not a good place to store authorization data.\\\\n* `raw_app_meta_data` - cannot be updated by the user, so it's a good place to store authorization data.\\\\n\\\\nThe `auth.jwt()` function is extremely versatile. For example, if you store some team data inside `app_metadata`, you can use it to determine whether a particular user belongs to a team. For example, if this was an array of IDs:\\\\n\\\\n```sql\\\\ncreate policy \\\\\\\"User is in team\\\\\\\"\\\\non my_table\\\\nto authenticated\\\\nusing ( team_id in (select auth.jwt() -> 'app_metadata' -> 'teams'));\\\\n```\\\\n\\\\n\\\\n Keep in mind that a JWT is not always \\\\\\\"fresh\\\\\\\". In the example above, even if you remove a user from a team and update the `app_metadata` field, that will not be reflected using `auth.jwt()` until the user's JWT is refreshed.\\\\n\\\\n Also, if you are using Cookies for Auth, then you must be mindful of the JWT size. Some browsers are limited to 4096 bytes for each cookie, and so the total size of your JWT should be small enough to fit inside this limitation.\\\\n\\\\n\\\\n\\\\n### MFA\\\\n\\\\nThe `auth.jwt()` function can be used to check for [Multi-Factor Authentication](/docs/guides/auth/auth-mfa#enforce-rules-for-mfa-logins). For example, you could restrict a user from updating their profile unless they have at least 2 levels of authentication (Assurance Level 2):\\\\n\\\\n```sql\\\\ncreate policy \\\\\\\"Restrict updates.\\\\\\\"\\\\non profiles\\\\nas restrictive\\\\nfor update\\\\nto authenticated using (\\\\n (select auth.jwt()->>'aal') = 'aal2'\\\\n);\\\\n```\\\\n\\\\n\\\\n## Bypassing Row Level Security\\\\n\\\\nSupabase provides special \\\\\\\"Service\\\\\\\" keys, which can be used to bypass RLS. These should never be used in the browser or exposed to customers, but they are useful for administrative tasks.\\\\n\\\\n\\\\n Supabase will adhere to the RLS policy of the signed-in user, even if the client library is initialized with a Service Key.\\\\n\\\\n\\\\nYou can also create new [Postgres Roles](/docs/guides/database/postgres/roles) which can bypass Row Level Security using the \\\\\\\"bypass RLS\\\\\\\" privilege:\\\\n\\\\n```sql\\\\nalter role \\\\\\\"role_name\\\\\\\" with bypassrls;\\\\n```\\\\n\\\\nThis can be useful for system-level access. You should *never* share login credentials for any Postgres Role with this privilege.\\\\n\\\\n\\\\n## RLS performance recommendations\\\\n\\\\nEvery authorization system has an impact on performance. While row level security is powerful, the performance impact is important to keep in mind. This is especially true for queries that scan every row in a table - like many `select` operations, including those using limit, offset, and ordering.\\\\n\\\\nBased on a series of [tests](https://github.com/GaryAustin1/RLS-Performance), we have a few recommendations for RLS:\\\\n\\\\n\\\\n### Add indexes\\\\n\\\\nMake sure you've added [indexes](/docs/guides/database/postgres/indexes) on any columns used within the Policies which are not already indexed (or primary keys). For a Policy like this:\\\\n\\\\n```sql\\\\ncreate policy \\\\\\\"rls_test_select\\\\\\\" on test_table\\\\nto authenticated\\\\nusing ( (select auth.uid()) = user_id );\\\\n```\\\\n\\\\nYou can add an index like:\\\\n\\\\n```sql\\\\ncreate index userid\\\\non test_table\\\\nusing btree (user_id);\\\\n```\\\\n\\\\n\\\\n#### Benchmarks\\\\n\\\\n| Test | Before (ms) | After (ms) | % Improvement | Change |\\\\n| --------------------------------------------------------------------------------------------- | ----------- | ---------- | ------------- | ------------------------------------------------------------------------------------------------------------ |\\\\n| [test1-indexed](https://github.com/GaryAustin1/RLS-Performance/tree/main/tests/test1-indexed) | 171 | \\\\\\\\< 0.1 | 99.94% |
    Before:
    No index

    After:
    `user_id` indexed
    |\\\\n\\\\n\\\\n### Call functions with `select`\\\\n\\\\nYou can use `select` statement to improve policies that use functions. For example, instead of this:\\\\n\\\\n```sql\\\\ncreate policy \\\\\\\"rls_test_select\\\\\\\" on test_table\\\\nto authenticated\\\\nusing ( auth.uid() = user_id );\\\\n```\\\\n\\\\nYou can do:\\\\n\\\\n```sql\\\\ncreate policy \\\\\\\"rls_test_select\\\\\\\" on test_table\\\\nto authenticated\\\\nusing ( (select auth.uid()) = user_id );\\\\n```\\\\n\\\\nThis method works well for JWT functions like `auth.uid()` and `auth.jwt()` as well as `security definer` Functions. Wrapping the function causes an `initPlan` to be run by the Postgres optimizer, which allows it to \\\\\\\"cache\\\\\\\" the results per-statement, rather than calling the function on each row.\\\\n\\\\n\\\\n You can only use this technique if the results of the query or function do not change based on the row data.\\\\n\\\\n\\\\n\\\\n#### Benchmarks\\\\n\\\\n| Test | Before (ms) | After (ms) | % Improvement | Change |\\\\n| --------------------------------------------------------------------------------------------------------------------------------- | ----------- | ---------- | ------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |\\\\n| [test2a-wrappedSQL-uid](https://github.com/GaryAustin1/RLS-Performance/tree/main/tests/test2a-wrappedSQL-uid\\\\\\\\(\\\\\\\\)) | 179 | 9 | 94.97% |
    Before:
    `auth.uid() = user_id`

    After:
    `(select auth.uid()) = user_id`
    |\\\\n| [test2b-wrappedSQL-isadmin](https://github.com/GaryAustin1/RLS-Performance/tree/main/tests/test2b-wrappedSQL-isadmin\\\\\\\\(\\\\\\\\)) | 11,000 | 7 | 99.94% |
    Before:
    `is_admin()` *table join*

    After:
    `(select is_admin())` *table join*
    |\\\\n| [test2c-wrappedSQL-two-functions](https://github.com/GaryAustin1/RLS-Performance/tree/main/tests/test2c-wrappedSQL-two-functions) | 11,000 | 10 | 99.91% |
    Before:
    `is_admin() OR auth.uid() = user_id`

    After:
    `(select is_admin()) OR (select auth.uid() = user_id)`
    |\\\\n| [test2d-wrappedSQL-sd-fun](https://github.com/GaryAustin1/RLS-Performance/tree/main/tests/test2d-wrappedSQL-sd-fun) | 178,000 | 12 | 99.993% |
    Before:
    `has_role() = role`

    After:
    (select has\\\\\\\\_role()) = role
    |\\\\n| [test2e-wrappedSQL-sd-fun-array](https://github.com/GaryAustin1/RLS-Performance/tree/main/tests/test2e-wrappedSQL-sd-fun-array) | 173000 | 16 | 99.991% |
    Before:
    `team_id=any(user_teams())`

    After:
    team\\\\\\\\_id=any(array(select user\\\\\\\\_teams()))
    |\\\\n\\\\n\\\\n### Add filters to every query\\\\n\\\\nPolicies are \\\\\\\"implicit where clauses,\\\\\\\" so it's common to run `select` statements without any filters. This is a bad pattern for performance. Instead of doing this (JS client example):\\\\n\\\\n{/* prettier-ignore */}\\\\n\\\\n```js\\\\nconst { data } = supabase\\\\n .from('table')\\\\n .select()\\\\n```\\\\n\\\\nYou should always add a filter:\\\\n\\\\n{/* prettier-ignore */}\\\\n\\\\n```js\\\\nconst { data } = supabase\\\\n .from('table')\\\\n .select()\\\\n .eq('user_id', userId)\\\\n```\\\\n\\\\nEven though this duplicates the contents of the Policy, Postgres can use the filter to construct a better query plan.\\\\n\\\\n\\\\n#### Benchmarks\\\\n\\\\n| Test | Before (ms) | After (ms) | % Improvement | Change |\\\\n| ------------------------------------------------------------------------------------------------- | ----------- | ---------- | ------------- | ------------------------------------------------------------------------------------------------------------------------------------------ |\\\\n| [test3-addfilter](https://github.com/GaryAustin1/RLS-Performance/tree/main/tests/test3-addfilter) | 171 | 9 | 94.74% |
    Before:
    `auth.uid() = user_id`

    After:
    add `.eq` or `where` on `user_id`
    |\\\\n\\\\n\\\\n### Use security definer functions\\\\n\\\\nA \\\\\\\"security definer\\\\\\\" function runs using the same role that *created* the function. This means that if you create a role with a superuser (like `postgres`), then that function will have `bypassrls` privileges. For example, if you had a policy like this:\\\\n\\\\n```sql\\\\ncreate policy \\\\\\\"rls_test_select\\\\\\\" on test_table\\\\nto authenticated\\\\nusing (\\\\n exists (\\\\n select 1 from roles_table\\\\n where (select auth.uid()) = user_id and role = 'good_role'\\\\n )\\\\n);\\\\n```\\\\n\\\\nWe can instead create a `security definer` function which can scan `roles_table` without any RLS penalties:\\\\n\\\\n```sql\\\\ncreate function private.has_good_role()\\\\nreturns boolean\\\\nlanguage plpgsql\\\\nsecurity definer -- will run as the creator\\\\nas $$\\\\nbegin\\\\n return exists (\\\\n select 1 from roles_table\\\\n where (select auth.uid()) = user_id and role = 'good_role'\\\\n );\\\\nend;\\\\n$$;\\\\n\\\\n-- Update our policy to use this function:\\\\ncreate policy \\\\\\\"rls_test_select\\\\\\\"\\\\non test_table\\\\nto authenticated\\\\nusing ( (select private.has_good_role()) );\\\\n```\\\\n\\\\n\\\\n Security-definer functions should never be created in a schema in the \\\\\\\"Exposed schemas\\\\\\\" inside your [API settings](/dashboard/project/_/settings/api)\\\\\\\\`.\\\\n\\\\n\\\\n\\\\n### Minimize joins\\\\n\\\\nYou can often rewrite your Policies to avoid joins between the source and the target table. Instead, try to organize your policy to fetch all the relevant data from the target table into an array or set, then you can use an `IN` or `ANY` operation in your filter.\\\\n\\\\nFor example, this is an example of a slow policy which joins the source `test_table` to the target `team_user`:\\\\n\\\\n```sql\\\\ncreate policy \\\\\\\"rls_test_select\\\\\\\" on test_table\\\\nto authenticated\\\\nusing (\\\\n (select auth.uid()) in (\\\\n select user_id\\\\n from team_user\\\\n where team_user.team_id = team_id -- joins to the source \\\\\\\"test_table.team_id\\\\\\\"\\\\n )\\\\n);\\\\n```\\\\n\\\\nWe can rewrite this to avoid this join, and instead select the filter criteria into a set:\\\\n\\\\n```sql\\\\ncreate policy \\\\\\\"rls_test_select\\\\\\\" on test_table\\\\nto authenticated\\\\nusing (\\\\n team_id in (\\\\n select team_id\\\\n from team_user\\\\n where user_id = (select auth.uid()) -- no join\\\\n )\\\\n);\\\\n```\\\\n\\\\nIn this case you can also consider [using a `security definer` function](#use-security-definer-functions) to bypass RLS on the join table:\\\\n\\\\n\\\\n If the list exceeds 1000 items, a different approach may be needed or you may need to analyze the approach to ensure that the performance is acceptable.\\\\n\\\\n\\\\n\\\\n#### Benchmarks\\\\n\\\\n| Test | Before (ms) | After (ms) | % Improvement | Change |\\\\n| --------------------------------------------------------------------------------------------------- | ----------- | ---------- | ------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------- |\\\\n| [test5-fixed-join](https://github.com/GaryAustin1/RLS-Performance/tree/main/tests/test5-fixed-join) | 9,000 | 20 | 99.78% |
    Before:
    `auth.uid()` in table join on col

    After:
    col in table join on `auth.uid()`
    |\\\\n\\\\n\\\\n### Specify roles in your policies\\\\n\\\\nAlways use the Role of inside your policies, specified by the `TO` operator. For example, instead of this query:\\\\n\\\\n```sql\\\\ncreate policy \\\\\\\"rls_test_select\\\\\\\" on rls_test\\\\nusing ( auth.uid() = user_id );\\\\n```\\\\n\\\\nUse:\\\\n\\\\n```sql\\\\ncreate policy \\\\\\\"rls_test_select\\\\\\\" on rls_test\\\\nto authenticated\\\\nusing ( (select auth.uid()) = user_id );\\\\n```\\\\n\\\\nThis prevents the policy `( (select auth.uid()) = user_id )` from running for any `anon` users, since the execution stops at the `to authenticated` step.\\\\n\\\\n\\\\n#### Benchmarks\\\\n\\\\n| Test | Before (ms) | After (ms) | % Improvement | Change |\\\\n| --------------------------------------------------------------------------------------------- | ----------- | ---------- | ------------- | ------------------------------------------------------------------------------------------------------------------------------------ |\\\\n| [test6-To-role](https://github.com/GaryAustin1/RLS-Performance/tree/main/tests/test6-To-role) | 170 | \\\\\\\\< 0.1 | 99.78% |
    Before:
    No `TO` policy

    After:
    `TO authenticated` (anon accessing)
    |\\\\n\\\\n\\\\n## More resources\\\\n\\\\n* [Testing your database](/docs/guides/database/testing)\\\\n* [RLS Guide and Best Practices](https://github.com/orgs/supabase/discussions/14576)\\\\n* Community repo on testing RLS using [pgTAP and dbdev](https://github.com/usebasejump/supabase-test-helpers/tree/main)\\\\n\\\"},{\\\"title\\\":\\\"Securing your data\\\",\\\"href\\\":\\\"https://supabase.com/docs/guides/database/secure-data\\\",\\\"content\\\":\\\"# Securing your data\\\\n\\\\n\\\\n\\\\nSupabase helps you control access to your data. With access policies, you can protect sensitive data and make sure users only access what they're allowed to see.\\\\n\\\\n\\\\n## Connecting your app securely\\\\n\\\\nSupabase gives you several ways to access your data. Each option has a different security model:\\\\n\\\\n\\\\n### Data API\\\\n\\\\nUse Supabase client libraries, REST, or GraphQL with a publishable key. Protect exposed tables with [Row Level Security](/docs/guides/database/postgres/row-level-security) (RLS) and grant only the privileges each role needs.\\\\n\\\\n\\\\n### Edge Functions\\\\n\\\\nPut custom server-side logic between your client and database with [Edge Functions](/docs/guides/functions). You can use secrets, API keys, or database connection strings inside the function, and you can [disable the Data API](/docs/guides/database/data-api#disable-the-data-api-completely) if your app only accesses data this way.\\\\n\\\\n\\\\n### Direct database connections\\\\n\\\\nConnect to Postgres with a connection string from trusted servers, workers, or tools. Keep database credentials secret and use the right [connection method](/docs/guides/database/connecting-to-postgres) for your environment. You can [disable the Data API](/docs/guides/api/securing-your-api#disable-the-data-api) if your app only uses direct connections.\\\\n\\\\n\\\\n## Frontend access\\\\n\\\\nFor frontend apps, the Data API is the usual choice. You can keep your data secure while accessing it from the frontend, so long as you:\\\\n\\\\n* Turn on [Row Level Security](/docs/guides/database/postgres/row-level-security) (RLS) for your tables and properly configure your access policies to grant the least privileges necessary for your app to function\\\\n* Use your Supabase **publishable key** when you create a Supabase client\\\\n\\\\nYour publishable key is safe to expose with RLS enabled, because row access permission is checked against your access policies and the user's [JSON Web Token (JWT)](/docs/learn/auth-deep-dive/auth-deep-dive-jwts). The JWT is automatically sent by the Supabase client libraries if the user is logged in using Supabase Auth.\\\\n\\\\nOlder projects may also show an `anon` key. Treat it like a publishable key: it can identify your project, but it is not a secret and must be paired with RLS and least-privilege grants.\\\\n\\\\n\\\\n Unlike your publishable key, your secret and service role keys are **never** safe to expose because they bypass RLS. Only use your secret and service role keys on the backend. Treat them as secrets (for example, import them as sensitive environment variables instead of hardcoding them).\\\\n\\\\n\\\\n\\\\n## More information\\\\n\\\\nSupabase and Postgres provide you with multiple ways to manage security, including but not limited to Row Level Security. See the Access and Security pages for more information:\\\\n\\\\n* [Row Level Security](/docs/guides/database/postgres/row-level-security)\\\\n* [Column Level Security](/docs/guides/database/postgres/column-level-security)\\\\n* [Securing your API](/docs/guides/api/securing-your-api)\\\\n* [Managing Postgres roles](/docs/guides/database/postgres/roles)\\\\n* [Managing secrets with Vault](/docs/guides/database/vault)\\\\n\\\"}]}}}\"\n }\n ],\n \"isError\": false\n}" + }, + { + "id": "build-cli-001-bootstrap-app-tool-7", + "title": "Tool: bash", + "startTime": "2026-08-01T15:56:01.177Z", + "endTime": "2026-08-01T15:56:02.645Z", + "duration": 1468, + "type": "tool_execution", + "status": "success", + "raw": "{\n \"exit_code\": 0,\n \"stdout\": \"2.67.1\\nInitialize a local project\\n\\nUsage:\\n supabase init [flags]\\n\\nFlags:\\n --force Overwrite existing supabase/config.toml.\\n -h, --help help for init\\n -i, --interactive Enables interactive mode to configure IDE settings.\\n --use-orioledb Use OrioleDB storage engine for Postgres.\\n\\nGlobal Flags:\\n --create-ticket create a support ticket for any CLI error\\n --debug output debug logs to stderr\\n --dns-resolver [ native | https ] lookup domain names using the specified resolver (default native)\\n --experimental enable experimental features\\n --network-id string use the specified docker network instead of a generated one\\n -o, --output [ env | pretty | json | toml | yaml ] output format of status variables (default pretty)\\n --profile string use a specific profile for connecting to Supabase API (default \\\"supabase\\\")\\n --workdir string path to a Supabase project directory\\n --yes answer yes to all prompts\\nCreate an empty migration script\\n\\nUsage:\\n supabase migration new [flags]\\n\\nFlags:\\n -h, --help help for new\\n\\nGlobal Flags:\\n --create-ticket create a support ticket for any CLI error\\n --debug output debug logs to stderr\\n --dns-resolver [ native | https ] lookup domain names using the specified resolver (default native)\\n --experimental enable experimental features\\n --network-id string use the specified docker network instead of a generated one\\n -o, --output [ env | pretty | json | toml | yaml ] output format of status variables (default pretty)\\n --profile string use a specific profile for connecting to Supabase API (default \\\"supabase\\\")\\n --workdir string path to a Supabase project directory\\n --yes answer yes to all prompts\\n\",\n \"stderr\": \"A new version of Supabase CLI is available: v2.111.0 (currently installed v2.67.1)\\nWe recommend updating regularly for new features and bug fixes: https://supabase.com/docs/guides/cli/getting-started#updating-the-supabase-cli\\nA new version of Supabase CLI is available: v2.111.0 (currently installed v2.67.1)\\nWe recommend updating regularly for new features and bug fixes: https://supabase.com/docs/guides/cli/getting-started#updating-the-supabase-cli\\nA new version of Supabase CLI is available: v2.111.0 (currently installed v2.67.1)\\nWe recommend updating regularly for new features and bug fixes: https://supabase.com/docs/guides/cli/getting-started#updating-the-supabase-cli\\n\"\n}", + "input": "{\n \"command\": \"supabase --version && supabase init --help && supabase migration new --help\",\n \"timeout_sec\": 240\n}", + "output": "{\n \"exit_code\": 0,\n \"stdout\": \"2.67.1\\nInitialize a local project\\n\\nUsage:\\n supabase init [flags]\\n\\nFlags:\\n --force Overwrite existing supabase/config.toml.\\n -h, --help help for init\\n -i, --interactive Enables interactive mode to configure IDE settings.\\n --use-orioledb Use OrioleDB storage engine for Postgres.\\n\\nGlobal Flags:\\n --create-ticket create a support ticket for any CLI error\\n --debug output debug logs to stderr\\n --dns-resolver [ native | https ] lookup domain names using the specified resolver (default native)\\n --experimental enable experimental features\\n --network-id string use the specified docker network instead of a generated one\\n -o, --output [ env | pretty | json | toml | yaml ] output format of status variables (default pretty)\\n --profile string use a specific profile for connecting to Supabase API (default \\\"supabase\\\")\\n --workdir string path to a Supabase project directory\\n --yes answer yes to all prompts\\nCreate an empty migration script\\n\\nUsage:\\n supabase migration new [flags]\\n\\nFlags:\\n -h, --help help for new\\n\\nGlobal Flags:\\n --create-ticket create a support ticket for any CLI error\\n --debug output debug logs to stderr\\n --dns-resolver [ native | https ] lookup domain names using the specified resolver (default native)\\n --experimental enable experimental features\\n --network-id string use the specified docker network instead of a generated one\\n -o, --output [ env | pretty | json | toml | yaml ] output format of status variables (default pretty)\\n --profile string use a specific profile for connecting to Supabase API (default \\\"supabase\\\")\\n --workdir string path to a Supabase project directory\\n --yes answer yes to all prompts\\n\",\n \"stderr\": \"A new version of Supabase CLI is available: v2.111.0 (currently installed v2.67.1)\\nWe recommend updating regularly for new features and bug fixes: https://supabase.com/docs/guides/cli/getting-started#updating-the-supabase-cli\\nA new version of Supabase CLI is available: v2.111.0 (currently installed v2.67.1)\\nWe recommend updating regularly for new features and bug fixes: https://supabase.com/docs/guides/cli/getting-started#updating-the-supabase-cli\\nA new version of Supabase CLI is available: v2.111.0 (currently installed v2.67.1)\\nWe recommend updating regularly for new features and bug fixes: https://supabase.com/docs/guides/cli/getting-started#updating-the-supabase-cli\\n\"\n}" + }, + { + "id": "build-cli-001-bootstrap-app-tool-8", + "title": "Tool: bash", + "startTime": "2026-08-01T15:56:02.645Z", + "endTime": "2026-08-01T15:56:05.846Z", + "duration": 3201, + "type": "tool_execution", + "status": "success", + "raw": "{\n \"exit_code\": 0,\n \"stdout\": \"data/skills/supabase-postgres-best-practices/CHANGELOG.md\\n# Changelog\\n\\n## [1.6.0](https://github.com/supabase/agent-skills/compare/supabase-postgres-best-practices-v1.5.0...supabase-postgres-best-practices-v1.6.0) (2026-07-30)\\n\\n\\n### Features\\n\\n* add schema-constraints reference for safe migration patterns ([#30](https://github.com/supabase/agent-skills/issues/30)) ([9b236f3](https://github.com/supabase/agent-skills/commit/9b236f3ebd65d76a2c570f19931353da9c858d5a))\\n* using Supabase agent skills ([#12](https://github.com/supabase/agent-skills/issues/12)) ([7c2e389](https://github.com/supabase/agent-skills/commit/7c2e3894fddfde8eb6c77d2a8921904543b9be7a))\\n\\n\\n### Bug Fixes\\n\\n* correct broken reference link in postgres best practices skill ([#58](https://github.com/supabase/agent-skills/issues/58)) ([f4e2277](https://github.com/supabase/agent-skills/commit/f4e22777fd8573537297b568c16e5a45a25927da))\\n* cover SECURITY DEFINER, auth.role() deprecation, and BOLA in security checklist ([#85](https://github.com/supabase/agent-skills/issues/85)) ([133f43e](https://github.com/supabase/agent-skills/commit/133f43e8c2ffc48823ff0630c692cabecea3e3a3))\\n* **supabase-postgres-best-practices:** make skill description trigger-rich ([#194](https://github.com/supabase/agent-skills/issues/194)) ([3291216](https://github.com/supabase/agent-skills/commit/32912161e2732c3e5001c6811a76c1f8308ed0da))\\n\\n## [1.5.0](https://github.com/supabase/agent-skills/compare/supabase-postgres-best-practices-v1.4.0...supabase-postgres-best-practices-v1.5.0) (2026-07-30)\\n\\n\\n### Features\\n\\n* add schema-constraints reference for safe migration patterns ([#30](https://github.com/supabase/agent-skills/issues/30)) ([9b236f3](https://github.com/supabase/agent-skills/commit/9b236f3ebd65d76a2c570f19931353da9c858d5a))\\n* using Supabase agent skills ([#12](https://github.com/supabase/agent-skills/issues/12)) ([7c2e389](https://github.com/supabase/agent-skills/commit/7c2e3894fddfde8eb6c77d2a8921904543b9be7a))\\n\\n\\n### Bug Fixes\\n\\n* correct broken reference link in postgres best practices skill ([#58](https://github.com/supabase/agent-skills/issues/58)) ([f4e2277](https://github.com/supabase/agent-skills/commit/f4e22777fd8573537297b568c16e5a45a25927da))\\n* cover SECURITY DEFINER, auth.role() deprecation, and BOLA in security checklist ([#85](https://github.com/supabase/agent-skills/issues/85)) ([133f43e](https://github.com/supabase/agent-skills/commit/133f43e8c2ffc48823ff0630c692cabecea3e3a3))\\n* **supabase-postgres-best-practices:** make skill description trigger-rich ([#194](https://github.com/supabase/agent-skills/issues/194)) ([3291216](https://github.com/supabase/agent-skills/commit/32912161e2732c3e5001c6811a76c1f8308ed0da))\\n\\n## [1.4.0](https://github.com/supabase/agent-skills/compare/v1.3.0...v1.4.0) (2026-07-10)\\n\\n\\n### Features\\n\\n* add schema-constraints reference for safe migration patterns ([#30](https://github.com/supabase/agent-skills/issues/30)) ([9b236f3](https://github.com/supabase/agent-skills/commit/9b236f3ebd65d76a2c570f19931353da9c858d5a))\\n* using Supabase agent skills ([#12](https://github.com/supabase/agent-skills/issues/12)) ([7c2e389](https://github.com/supabase/agent-skills/commit/7c2e3894fddfde8eb6c77d2a8921904543b9be7a))\\n\\n\\n### Bug Fixes\\n\\n* correct broken reference link in postgres best practices skill ([#58](https://github.com/supabase/agent-skills/issues/58)) ([f4e2277](https://github.com/supabase/agent-skills/commit/f4e22777fd8573537297b568c16e5a45a25927da))\\n* cover SECURITY DEFINER, auth.role() deprecation, and BOLA in security checklist ([#85](https://github.com/supabase/agent-skills/issues/85)) ([133f43e](https://github.com/supabase/agent-skills/commit/133f43e8c2ffc48823ff0630c692cabecea3e3a3))\\n\\n## [1.3.0](https://github.com/supabase/agent-skills/compare/v1.2.0...v1.3.0) (2026-06-05)\\n\\n\\n### Features\\n\\n* add schema-constraints reference for safe migration patterns ([#30](https://github.com/supabase/agent-skills/issues/30)) ([9b236f3](https://github.com/supabase/agent-skills/commit/9b236f3ebd65d76a2c570f19931353da9c858d5a))\\n* using Supabase agent skills ([#12](https://github.com/supabase/agent-skills/issues/12)) ([7c2e389](https://github.com/supabase/agent-skills/commit/7c2e3894fddfde8eb6c77d2a8921904543b9be7a))\\n\\n\\n### Bug Fixes\\n\\n* correct broken reference link in postgres best practices skill ([#58](https://github.com/supabase/agent-skills/issues/58)) ([f4e2277](https://github.com/supabase/agent-skills/commit/f4e22777fd8573537297b568c16e5a45a25927da))\\n* cover SECURITY DEFINER, auth.role() deprecation, and BOLA in security checklist ([#85](https://github.com/supabase/agent-skills/issues/85)) ([133f43e](https://github.com/supabase/agent-skills/commit/133f43e8c2ffc48823ff0630c692cabecea3e3a3))\\n\\n## [1.2.0](https://github.com/supabase/agent-skills/compare/v1.1.1...v1.2.0) (2026-06-02)\\n\\n\\n### Features\\n\\n* add schema-constraints reference for safe migration patterns ([#30](https://github.com/supabase/agent-skills/issues/30)) ([9b236f3](https://github.com/supabase/agent-skills/commit/9b236f3ebd65d76a2c570f19931353da9c858d5a))\\n* using Supabase agent skills ([#12](https://github.com/supabase/agent-skills/issues/12)) ([7c2e389](https://github.com/supabase/agent-skills/commit/7c2e3894fddfde8eb6c77d2a8921904543b9be7a))\\n\\n\\n### Bug Fixes\\n\\n* correct broken reference link in postgres best practices skill ([#58](https://github.com/supabase/agent-skills/issues/58)) ([f4e2277](https://github.com/supabase/agent-skills/commit/f4e22777fd8573537297b568c16e5a45a25927da))\\n* cover SECURITY DEFINER, auth.role() deprecation, and BOLA in security checklist ([#85](https://github.com/supabase/agent-skills/issues/85)) ([133f43e](https://github.com/supabase/agent-skills/commit/133f43e8c2ffc48823ff0630c692cabecea3e3a3))\\ndata/skills/supabase-postgres-best-practices/SKILL.md\\n---\\nname: supabase-postgres-best-practices\\ndescription: \\\"Postgres best practices maintained by Supabase, for Postgres running anywhere. Load this skill BEFORE writing or changing anything that lives in a Postgres database: creating or altering tables and columns (including choosing column types), schema design, migrations and declarative schema files, RLS policies and the tests that verify them, indexes, triggers, database functions, queues and scheduled jobs (pg_cron, pgmq), vector/semantic search (pgvector), and restoring dumps (pg_restore) or importing data. Also load it when diagnosing slow queries, high CPU, timeouts, EXPLAIN plans, connection exhaustion, locking, bloat, or rows visible to the wrong user or tenant. This is not just a performance guide — schema, migration, security, and SQL authoring tasks need these rules too, even for a one-column change or a single query.\\\"\\nlicense: MIT\\nmetadata:\\n author: supabase\\n version: \\\"1.1.1\\\"\\n organization: Supabase\\n date: January 2026\\n abstract: Comprehensive Postgres performance optimization guide for developers using Supabase and Postgres. Contains performance rules across 8 categories, prioritized by impact from critical (query performance, connection management) to incremental (advanced features). Each rule includes detailed explanations, incorrect vs. correct SQL examples, query plan analysis, and specific performance metrics to guide automated optimization and code generation.\\n---\\n\\n# Supabase Postgres Best Practices\\n\\nComprehensive performance optimization guide for Postgres, maintained by Supabase. Contains rules across 8 categories, prioritized by impact to guide automated query optimization and schema design.\\n\\n## When to Apply\\n\\nReference these guidelines when:\\n- Writing SQL queries or designing schemas\\n- Implementing indexes or query optimization\\n- Reviewing database performance issues\\n- Configuring connection pooling or scaling\\n- Optimizing for Postgres-specific features\\n- Working with Row-Level Security (RLS)\\n\\n## Rule Categories by Priority\\n\\n| Priority | Category | Impact | Prefix |\\n|----------|----------|--------|--------|\\n| 1 | Query Performance | CRITICAL | `query-` |\\n| 2 | Connection Management | CRITICAL | `conn-` |\\n| 3 | Security & RLS | CRITICAL | `security-` |\\n| 4 | Schema Design | HIGH | `schema-` |\\n| 5 | Concurrency & Locking | MEDIUM-HIGH | `lock-` |\\n| 6 | Data Access Patterns | MEDIUM | `data-` |\\n| 7 | Monitoring & Diagnostics | LOW-MEDIUM | `monitor-` |\\n| 8 | Advanced Features | LOW | `advanced-` |\\n\\n## How to Use\\n\\nRead individual rule files for detailed explanations and SQL examples:\\n\\n```\\nreferences/query-missing-indexes.md\\nreferences/query-partial-indexes.md\\nreferences/_sections.md\\n```\\n\\nEach rule file contains:\\n- Brief explanation of why it matters\\n- Incorrect SQL example with explanation\\n- Correct SQL example with explanation\\n- Optional EXPLAIN output or metrics\\n- Additional context and references\\n- Supabase-specific notes (when applicable)\\n\\n## References\\n\\n- https://www.postgresql.org/docs/current/\\n- https://supabase.com/docs\\n- https://wiki.postgresql.org/wiki/Performance_Optimization\\n- https://supabase.com/docs/guides/database/overview\\n- https://supabase.com/docs/guides/auth/row-level-security\\ndata/skills/supabase/CHANGELOG.md\\n# Changelog\\n\\n## [0.1.6](https://github.com/supabase/agent-skills/compare/v0.1.5...supabase-v0.1.6) (2026-07-30)\\n\\n\\n### Features\\n\\n* add declarative schema section to schema management ([#120](https://github.com/supabase/agent-skills/issues/120)) ([94cf44f](https://github.com/supabase/agent-skills/commit/94cf44ff3a123edcf54a9af373929798cddefed8))\\n* add instructions to check changelog ([#74](https://github.com/supabase/agent-skills/issues/74)) ([4bb13d8](https://github.com/supabase/agent-skills/commit/4bb13d858d19f1f848505a66f46fc9603fdcde95))\\n* add npm supply-chain security guidance to supabase skill ([#94](https://github.com/supabase/agent-skills/issues/94)) ([82df90a](https://github.com/supabase/agent-skills/commit/82df90a5de1cd84386d8bc192746e50343b86dc0))\\n* instructions on exposing tables to the data api ([#71](https://github.com/supabase/agent-skills/issues/71)) ([f15a5a4](https://github.com/supabase/agent-skills/commit/f15a5a40779072a530c9e53c3f14ec4131118ea6))\\n* using Supabase agent skills ([#12](https://github.com/supabase/agent-skills/issues/12)) ([7c2e389](https://github.com/supabase/agent-skills/commit/7c2e3894fddfde8eb6c77d2a8921904543b9be7a))\\n\\n\\n### Bug Fixes\\n\\n* bump supabase skill to v0.1.1 and fix Data API broken link ([#72](https://github.com/supabase/agent-skills/issues/72)) ([5a6542e](https://github.com/supabase/agent-skills/commit/5a6542e08fc026d90c9a6a0f5a67749e9ceb9946))\\n* cover SECURITY DEFINER, auth.role() deprecation, and BOLA in security checklist ([#85](https://github.com/supabase/agent-skills/issues/85)) ([133f43e](https://github.com/supabase/agent-skills/commit/133f43e8c2ffc48823ff0630c692cabecea3e3a3))\\n* update Data API doc link and bump supabase skill to v0.1.1 ([#73](https://github.com/supabase/agent-skills/issues/73)) ([e5f7a7c](https://github.com/supabase/agent-skills/commit/e5f7a7cfd697765848ffd6a4505f3c02e1ee17ee))\\n\\n## [0.1.5](https://github.com/supabase/agent-skills/compare/v0.1.4...v0.1.5) (2026-07-10)\\n\\n\\n### Features\\n\\n* add declarative schema section to schema management ([#120](https://github.com/supabase/agent-skills/issues/120)) ([94cf44f](https://github.com/supabase/agent-skills/commit/94cf44ff3a123edcf54a9af373929798cddefed8))\\n* add instructions to check changelog ([#74](https://github.com/supabase/agent-skills/issues/74)) ([4bb13d8](https://github.com/supabase/agent-skills/commit/4bb13d858d19f1f848505a66f46fc9603fdcde95))\\n* add npm supply-chain security guidance to supabase skill ([#94](https://github.com/supabase/agent-skills/issues/94)) ([82df90a](https://github.com/supabase/agent-skills/commit/82df90a5de1cd84386d8bc192746e50343b86dc0))\\n* instructions on exposing tables to the data api ([#71](https://github.com/supabase/agent-skills/issues/71)) ([f15a5a4](https://github.com/supabase/agent-skills/commit/f15a5a40779072a530c9e53c3f14ec4131118ea6))\\n* using Supabase agent skills ([#12](https://github.com/supabase/agent-skills/issues/12)) ([7c2e389](http\\n...[truncated 11333 chars]...\\n three.\\n\\n- **Dependency and supply-chain security**\\n - **Always pin package versions and commit lockfiles** when installing Supabase packages (`supabase-js`, `@supabase/ssr`, `supabase-py`, etc.). See the [npm security guide](https://supabase.com/docs/guides/security/npm-security.md) for the full checklist.\\n\\nFor any security concern not covered above, fetch the Supabase product security index: `https://supabase.com/docs/guides/security/product-security.md`\\n\\n## Supabase CLI\\n\\nAlways discover commands via `--help` — never guess. The CLI structure changes between versions.\\n\\n```bash\\nsupabase --help # All top-level commands\\nsupabase --help # Subcommands (e.g., supabase db --help)\\nsupabase --help # Flags for a specific command\\n```\\n\\n**Supabase CLI Known gotchas:**\\n\\n- `supabase db query` requires **CLI v2.79.0+** → use MCP `execute_sql` or `psql` as fallback\\n- `supabase db advisors` requires **CLI v2.81.3+** → use MCP `get_advisors` as fallback\\n- In imperative migration projects, create new hand-authored migration files with `supabase migration new ` first. Never invent a migration filename or rely on memory for the expected format. Declarative schema projects generate migrations from `supabase/schemas/`; see \\\"Making and Committing Schema Changes\\\" below.\\n\\n**Version check and upgrade:** Run `supabase --version` to check. For CLI changelogs and version-specific features, consult the [CLI documentation](https://supabase.com/docs/reference/cli/introduction) or [GitHub releases](https://github.com/supabase/cli/releases).\\n\\n## Supabase MCP Server\\n\\nFor setup instructions, server URL, and configuration, see the [MCP setup guide](https://supabase.com/docs/guides/getting-started/mcp).\\n\\n**Troubleshooting connection issues** — follow these steps in order:\\n\\n1. **Check if the server is reachable:**\\n `curl -so /dev/null -w \\\"%{http_code}\\\" https://mcp.supabase.com/mcp`\\n A `401` is expected (no token) and means the server is up. Timeout or \\\"connection refused\\\" means it may be down.\\n\\n2. **Check `.mcp.json` configuration:**\\n Verify the project root has a valid `.mcp.json` with the correct server URL. If missing, create one pointing to `https://mcp.supabase.com/mcp`.\\n\\n3. **Authenticate the MCP server:**\\n If the server is reachable and `.mcp.json` is correct but tools aren't visible, the user needs to authenticate. The Supabase MCP server uses OAuth 2.1 — tell the user to trigger the auth flow in their agent, complete it in the browser, and reload the session.\\n\\n## Supabase Documentation\\n\\nBefore implementing any Supabase feature, find the relevant documentation. Use these methods in priority order:\\n\\n1. **MCP `search_docs` tool** (preferred — returns relevant snippets directly)\\n2. **Fetch docs pages as markdown** — any docs page can be fetched by appending `.md` to the URL path.\\n3. **Web search** for Supabase-specific topics when you don't know which page to look at.\\n\\n## Making and Committing Schema Changes\\n\\n\",\n \"stderr\": \"\"\n}", + "input": "{\n \"command\": \"find data -maxdepth 3 -type f -print -exec sed -n '1,120p' {} \\\\;\",\n \"timeout_sec\": 240\n}", + "output": "{\n \"exit_code\": 0,\n \"stdout\": \"data/skills/supabase-postgres-best-practices/CHANGELOG.md\\n# Changelog\\n\\n## [1.6.0](https://github.com/supabase/agent-skills/compare/supabase-postgres-best-practices-v1.5.0...supabase-postgres-best-practices-v1.6.0) (2026-07-30)\\n\\n\\n### Features\\n\\n* add schema-constraints reference for safe migration patterns ([#30](https://github.com/supabase/agent-skills/issues/30)) ([9b236f3](https://github.com/supabase/agent-skills/commit/9b236f3ebd65d76a2c570f19931353da9c858d5a))\\n* using Supabase agent skills ([#12](https://github.com/supabase/agent-skills/issues/12)) ([7c2e389](https://github.com/supabase/agent-skills/commit/7c2e3894fddfde8eb6c77d2a8921904543b9be7a))\\n\\n\\n### Bug Fixes\\n\\n* correct broken reference link in postgres best practices skill ([#58](https://github.com/supabase/agent-skills/issues/58)) ([f4e2277](https://github.com/supabase/agent-skills/commit/f4e22777fd8573537297b568c16e5a45a25927da))\\n* cover SECURITY DEFINER, auth.role() deprecation, and BOLA in security checklist ([#85](https://github.com/supabase/agent-skills/issues/85)) ([133f43e](https://github.com/supabase/agent-skills/commit/133f43e8c2ffc48823ff0630c692cabecea3e3a3))\\n* **supabase-postgres-best-practices:** make skill description trigger-rich ([#194](https://github.com/supabase/agent-skills/issues/194)) ([3291216](https://github.com/supabase/agent-skills/commit/32912161e2732c3e5001c6811a76c1f8308ed0da))\\n\\n## [1.5.0](https://github.com/supabase/agent-skills/compare/supabase-postgres-best-practices-v1.4.0...supabase-postgres-best-practices-v1.5.0) (2026-07-30)\\n\\n\\n### Features\\n\\n* add schema-constraints reference for safe migration patterns ([#30](https://github.com/supabase/agent-skills/issues/30)) ([9b236f3](https://github.com/supabase/agent-skills/commit/9b236f3ebd65d76a2c570f19931353da9c858d5a))\\n* using Supabase agent skills ([#12](https://github.com/supabase/agent-skills/issues/12)) ([7c2e389](https://github.com/supabase/agent-skills/commit/7c2e3894fddfde8eb6c77d2a8921904543b9be7a))\\n\\n\\n### Bug Fixes\\n\\n* correct broken reference link in postgres best practices skill ([#58](https://github.com/supabase/agent-skills/issues/58)) ([f4e2277](https://github.com/supabase/agent-skills/commit/f4e22777fd8573537297b568c16e5a45a25927da))\\n* cover SECURITY DEFINER, auth.role() deprecation, and BOLA in security checklist ([#85](https://github.com/supabase/agent-skills/issues/85)) ([133f43e](https://github.com/supabase/agent-skills/commit/133f43e8c2ffc48823ff0630c692cabecea3e3a3))\\n* **supabase-postgres-best-practices:** make skill description trigger-rich ([#194](https://github.com/supabase/agent-skills/issues/194)) ([3291216](https://github.com/supabase/agent-skills/commit/32912161e2732c3e5001c6811a76c1f8308ed0da))\\n\\n## [1.4.0](https://github.com/supabase/agent-skills/compare/v1.3.0...v1.4.0) (2026-07-10)\\n\\n\\n### Features\\n\\n* add schema-constraints reference for safe migration patterns ([#30](https://github.com/supabase/agent-skills/issues/30)) ([9b236f3](https://github.com/supabase/agent-skills/commit/9b236f3ebd65d76a2c570f19931353da9c858d5a))\\n* using Supabase agent skills ([#12](https://github.com/supabase/agent-skills/issues/12)) ([7c2e389](https://github.com/supabase/agent-skills/commit/7c2e3894fddfde8eb6c77d2a8921904543b9be7a))\\n\\n\\n### Bug Fixes\\n\\n* correct broken reference link in postgres best practices skill ([#58](https://github.com/supabase/agent-skills/issues/58)) ([f4e2277](https://github.com/supabase/agent-skills/commit/f4e22777fd8573537297b568c16e5a45a25927da))\\n* cover SECURITY DEFINER, auth.role() deprecation, and BOLA in security checklist ([#85](https://github.com/supabase/agent-skills/issues/85)) ([133f43e](https://github.com/supabase/agent-skills/commit/133f43e8c2ffc48823ff0630c692cabecea3e3a3))\\n\\n## [1.3.0](https://github.com/supabase/agent-skills/compare/v1.2.0...v1.3.0) (2026-06-05)\\n\\n\\n### Features\\n\\n* add schema-constraints reference for safe migration patterns ([#30](https://github.com/supabase/agent-skills/issues/30)) ([9b236f3](https://github.com/supabase/agent-skills/commit/9b236f3ebd65d76a2c570f19931353da9c858d5a))\\n* using Supabase agent skills ([#12](https://github.com/supabase/agent-skills/issues/12)) ([7c2e389](https://github.com/supabase/agent-skills/commit/7c2e3894fddfde8eb6c77d2a8921904543b9be7a))\\n\\n\\n### Bug Fixes\\n\\n* correct broken reference link in postgres best practices skill ([#58](https://github.com/supabase/agent-skills/issues/58)) ([f4e2277](https://github.com/supabase/agent-skills/commit/f4e22777fd8573537297b568c16e5a45a25927da))\\n* cover SECURITY DEFINER, auth.role() deprecation, and BOLA in security checklist ([#85](https://github.com/supabase/agent-skills/issues/85)) ([133f43e](https://github.com/supabase/agent-skills/commit/133f43e8c2ffc48823ff0630c692cabecea3e3a3))\\n\\n## [1.2.0](https://github.com/supabase/agent-skills/compare/v1.1.1...v1.2.0) (2026-06-02)\\n\\n\\n### Features\\n\\n* add schema-constraints reference for safe migration patterns ([#30](https://github.com/supabase/agent-skills/issues/30)) ([9b236f3](https://github.com/supabase/agent-skills/commit/9b236f3ebd65d76a2c570f19931353da9c858d5a))\\n* using Supabase agent skills ([#12](https://github.com/supabase/agent-skills/issues/12)) ([7c2e389](https://github.com/supabase/agent-skills/commit/7c2e3894fddfde8eb6c77d2a8921904543b9be7a))\\n\\n\\n### Bug Fixes\\n\\n* correct broken reference link in postgres best practices skill ([#58](https://github.com/supabase/agent-skills/issues/58)) ([f4e2277](https://github.com/supabase/agent-skills/commit/f4e22777fd8573537297b568c16e5a45a25927da))\\n* cover SECURITY DEFINER, auth.role() deprecation, and BOLA in security checklist ([#85](https://github.com/supabase/agent-skills/issues/85)) ([133f43e](https://github.com/supabase/agent-skills/commit/133f43e8c2ffc48823ff0630c692cabecea3e3a3))\\ndata/skills/supabase-postgres-best-practices/SKILL.md\\n---\\nname: supabase-postgres-best-practices\\ndescription: \\\"Postgres best practices maintained by Supabase, for Postgres running anywhere. Load this skill BEFORE writing or changing anything that lives in a Postgres database: creating or altering tables and columns (including choosing column types), schema design, migrations and declarative schema files, RLS policies and the tests that verify them, indexes, triggers, database functions, queues and scheduled jobs (pg_cron, pgmq), vector/semantic search (pgvector), and restoring dumps (pg_restore) or importing data. Also load it when diagnosing slow queries, high CPU, timeouts, EXPLAIN plans, connection exhaustion, locking, bloat, or rows visible to the wrong user or tenant. This is not just a performance guide — schema, migration, security, and SQL authoring tasks need these rules too, even for a one-column change or a single query.\\\"\\nlicense: MIT\\nmetadata:\\n author: supabase\\n version: \\\"1.1.1\\\"\\n organization: Supabase\\n date: January 2026\\n abstract: Comprehensive Postgres performance optimization guide for developers using Supabase and Postgres. Contains performance rules across 8 categories, prioritized by impact from critical (query performance, connection management) to incremental (advanced features). Each rule includes detailed explanations, incorrect vs. correct SQL examples, query plan analysis, and specific performance metrics to guide automated optimization and code generation.\\n---\\n\\n# Supabase Postgres Best Practices\\n\\nComprehensive performance optimization guide for Postgres, maintained by Supabase. Contains rules across 8 categories, prioritized by impact to guide automated query optimization and schema design.\\n\\n## When to Apply\\n\\nReference these guidelines when:\\n- Writing SQL queries or designing schemas\\n- Implementing indexes or query optimization\\n- Reviewing database performance issues\\n- Configuring connection pooling or scaling\\n- Optimizing for Postgres-specific features\\n- Working with Row-Level Security (RLS)\\n\\n## Rule Categories by Priority\\n\\n| Priority | Category | Impact | Prefix |\\n|----------|----------|--------|--------|\\n| 1 | Query Performance | CRITICAL | `query-` |\\n| 2 | Connection Management | CRITICAL | `conn-` |\\n| 3 | Security & RLS | CRITICAL | `security-` |\\n| 4 | Schema Design | HIGH | `schema-` |\\n| 5 | Concurrency & Locking | MEDIUM-HIGH | `lock-` |\\n| 6 | Data Access Patterns | MEDIUM | `data-` |\\n| 7 | Monitoring & Diagnostics | LOW-MEDIUM | `monitor-` |\\n| 8 | Advanced Features | LOW | `advanced-` |\\n\\n## How to Use\\n\\nRead individual rule files for detailed explanations and SQL examples:\\n\\n```\\nreferences/query-missing-indexes.md\\nreferences/query-partial-indexes.md\\nreferences/_sections.md\\n```\\n\\nEach rule file contains:\\n- Brief explanation of why it matters\\n- Incorrect SQL example with explanation\\n- Correct SQL example with explanation\\n- Optional EXPLAIN output or metrics\\n- Additional context and references\\n- Supabase-specific notes (when applicable)\\n\\n## References\\n\\n- https://www.postgresql.org/docs/current/\\n- https://supabase.com/docs\\n- https://wiki.postgresql.org/wiki/Performance_Optimization\\n- https://supabase.com/docs/guides/database/overview\\n- https://supabase.com/docs/guides/auth/row-level-security\\ndata/skills/supabase/CHANGELOG.md\\n# Changelog\\n\\n## [0.1.6](https://github.com/supabase/agent-skills/compare/v0.1.5...supabase-v0.1.6) (2026-07-30)\\n\\n\\n### Features\\n\\n* add declarative schema section to schema management ([#120](https://github.com/supabase/agent-skills/issues/120)) ([94cf44f](https://github.com/supabase/agent-skills/commit/94cf44ff3a123edcf54a9af373929798cddefed8))\\n* add instructions to check changelog ([#74](https://github.com/supabase/agent-skills/issues/74)) ([4bb13d8](https://github.com/supabase/agent-skills/commit/4bb13d858d19f1f848505a66f46fc9603fdcde95))\\n* add npm supply-chain security guidance to supabase skill ([#94](https://github.com/supabase/agent-skills/issues/94)) ([82df90a](https://github.com/supabase/agent-skills/commit/82df90a5de1cd84386d8bc192746e50343b86dc0))\\n* instructions on exposing tables to the data api ([#71](https://github.com/supabase/agent-skills/issues/71)) ([f15a5a4](https://github.com/supabase/agent-skills/commit/f15a5a40779072a530c9e53c3f14ec4131118ea6))\\n* using Supabase agent skills ([#12](https://github.com/supabase/agent-skills/issues/12)) ([7c2e389](https://github.com/supabase/agent-skills/commit/7c2e3894fddfde8eb6c77d2a8921904543b9be7a))\\n\\n\\n### Bug Fixes\\n\\n* bump supabase skill to v0.1.1 and fix Data API broken link ([#72](https://github.com/supabase/agent-skills/issues/72)) ([5a6542e](https://github.com/supabase/agent-skills/commit/5a6542e08fc026d90c9a6a0f5a67749e9ceb9946))\\n* cover SECURITY DEFINER, auth.role() deprecation, and BOLA in security checklist ([#85](https://github.com/supabase/agent-skills/issues/85)) ([133f43e](https://github.com/supabase/agent-skills/commit/133f43e8c2ffc48823ff0630c692cabecea3e3a3))\\n* update Data API doc link and bump supabase skill to v0.1.1 ([#73](https://github.com/supabase/agent-skills/issues/73)) ([e5f7a7c](https://github.com/supabase/agent-skills/commit/e5f7a7cfd697765848ffd6a4505f3c02e1ee17ee))\\n\\n## [0.1.5](https://github.com/supabase/agent-skills/compare/v0.1.4...v0.1.5) (2026-07-10)\\n\\n\\n### Features\\n\\n* add declarative schema section to schema management ([#120](https://github.com/supabase/agent-skills/issues/120)) ([94cf44f](https://github.com/supabase/agent-skills/commit/94cf44ff3a123edcf54a9af373929798cddefed8))\\n* add instructions to check changelog ([#74](https://github.com/supabase/agent-skills/issues/74)) ([4bb13d8](https://github.com/supabase/agent-skills/commit/4bb13d858d19f1f848505a66f46fc9603fdcde95))\\n* add npm supply-chain security guidance to supabase skill ([#94](https://github.com/supabase/agent-skills/issues/94)) ([82df90a](https://github.com/supabase/agent-skills/commit/82df90a5de1cd84386d8bc192746e50343b86dc0))\\n* instructions on exposing tables to the data api ([#71](https://github.com/supabase/agent-skills/issues/71)) ([f15a5a4](https://github.com/supabase/agent-skills/commit/f15a5a40779072a530c9e53c3f14ec4131118ea6))\\n* using Supabase agent skills ([#12](https://github.com/supabase/agent-skills/issues/12)) ([7c2e389](http\\n...[truncated 11333 chars]...\\n three.\\n\\n- **Dependency and supply-chain security**\\n - **Always pin package versions and commit lockfiles** when installing Supabase packages (`supabase-js`, `@supabase/ssr`, `supabase-py`, etc.). See the [npm security guide](https://supabase.com/docs/guides/security/npm-security.md) for the full checklist.\\n\\nFor any security concern not covered above, fetch the Supabase product security index: `https://supabase.com/docs/guides/security/product-security.md`\\n\\n## Supabase CLI\\n\\nAlways discover commands via `--help` — never guess. The CLI structure changes between versions.\\n\\n```bash\\nsupabase --help # All top-level commands\\nsupabase --help # Subcommands (e.g., supabase db --help)\\nsupabase --help # Flags for a specific command\\n```\\n\\n**Supabase CLI Known gotchas:**\\n\\n- `supabase db query` requires **CLI v2.79.0+** → use MCP `execute_sql` or `psql` as fallback\\n- `supabase db advisors` requires **CLI v2.81.3+** → use MCP `get_advisors` as fallback\\n- In imperative migration projects, create new hand-authored migration files with `supabase migration new ` first. Never invent a migration filename or rely on memory for the expected format. Declarative schema projects generate migrations from `supabase/schemas/`; see \\\"Making and Committing Schema Changes\\\" below.\\n\\n**Version check and upgrade:** Run `supabase --version` to check. For CLI changelogs and version-specific features, consult the [CLI documentation](https://supabase.com/docs/reference/cli/introduction) or [GitHub releases](https://github.com/supabase/cli/releases).\\n\\n## Supabase MCP Server\\n\\nFor setup instructions, server URL, and configuration, see the [MCP setup guide](https://supabase.com/docs/guides/getting-started/mcp).\\n\\n**Troubleshooting connection issues** — follow these steps in order:\\n\\n1. **Check if the server is reachable:**\\n `curl -so /dev/null -w \\\"%{http_code}\\\" https://mcp.supabase.com/mcp`\\n A `401` is expected (no token) and means the server is up. Timeout or \\\"connection refused\\\" means it may be down.\\n\\n2. **Check `.mcp.json` configuration:**\\n Verify the project root has a valid `.mcp.json` with the correct server URL. If missing, create one pointing to `https://mcp.supabase.com/mcp`.\\n\\n3. **Authenticate the MCP server:**\\n If the server is reachable and `.mcp.json` is correct but tools aren't visible, the user needs to authenticate. The Supabase MCP server uses OAuth 2.1 — tell the user to trigger the auth flow in their agent, complete it in the browser, and reload the session.\\n\\n## Supabase Documentation\\n\\nBefore implementing any Supabase feature, find the relevant documentation. Use these methods in priority order:\\n\\n1. **MCP `search_docs` tool** (preferred — returns relevant snippets directly)\\n2. **Fetch docs pages as markdown** — any docs page can be fetched by appending `.md` to the URL path.\\n3. **Web search** for Supabase-specific topics when you don't know which page to look at.\\n\\n## Making and Committing Schema Changes\\n\\n\",\n \"stderr\": \"\"\n}" + }, + { + "id": "build-cli-001-bootstrap-app-tool-9", + "title": "Tool: bash", + "startTime": "2026-08-01T15:56:05.846Z", + "endTime": "2026-08-01T15:56:11.255Z", + "duration": 5409, + "type": "tool_execution", + "status": "success", + "raw": "{\n \"exit_code\": 0,\n \"stdout\": \"2026-07-29 · Bug Fix · Observability, Platform · [supabase.com/changelog/admin-api-metrics-panic-fix](https://supabase.com/changelog/admin-api-metrics-panic-fix)\\n## Migration of Supabase Management API `logs.all` analytics endpoint to `logs` endpoint\\n2026-07-23 · Breaking Change · Platform · [supabase.com/changelog/48235-migration-of-supabase-management-api-logs-all-analytics-endpoint-to-logs-endpoint](https://supabase.com/changelog/48235-migration-of-supabase-management-api-logs-all-analytics-endpoint-to-logs-endpoint)\\nThe Supabase Management API `logs.all` endpoint is removed on 2026-09-23. Scripts calling it must migrate to the new ClickHouse-backed `logs` endpoint, which accepts ClickHouse SQL only.\\n2026-07-22 · Breaking Change · Database, Security · [supabase.com/changelog/extension-version-pinning-ignored](https://supabase.com/changelog/extension-version-pinning-ignored)\\n## Self-hosted Supabase: Envoy becomes the default API gateway (breaking change)\\n2026-07-17 · Breaking Change · Platform · [supabase.com/changelog/48048-self-hosted-supabase-envoy-becomes-the-default-api-gateway-b](https://supabase.com/changelog/48048-self-hosted-supabase-envoy-becomes-the-default-api-gateway-b)\\nSelf-hosted Supabase makes Envoy the default API gateway, replacing Kong, the week of 2026-08-09. Opt back into Kong if you rely on its HTTPS listener, a custom `kong.yml`, or the kong service name.\\n2026-07-09 · Improvement · Data APIs, Database, Realtime · [supabase.com/changelog/47796-developer-update-july-2026](https://supabase.com/changelog/47796-developer-update-july-2026)\\nPostgres `log_connections` defaults to off for new projects and existing Free and Pro projects from 2026-07-09, cutting log noise. Re-enable it via the dashboard or Management API.\\n## Self-hosted Supabase: API_EXTERNAL_URL to include /auth/v1\\n2026-06-18 · Breaking Change · Auth · [supabase.com/changelog/47093-self-hosted-supabase-api-external-url-to-include-auth-v1](https://supabase.com/changelog/47093-self-hosted-supabase-api-external-url-to-include-auth-v1)\\nSelf-hosted Supabase's default `API_EXTERNAL_URL` now includes `/auth/v1` from the week of 2026-07-06. SAML SSO users must repoint their IdP to the new `/auth/v1/sso/saml/*` endpoints.\\nSupabase Realtime Broadcast now sends and receives binary payloads (bytea) over WebSockets, the REST API, and the database, cutting JSON overhead for sensor streams and image frames.\\n2026-06-06 · Improvement · Auth, CLI, Database · [supabase.com/changelog/46689-developer-update-june-2026](https://supabase.com/changelog/46689-developer-update-june-2026)\\nSupabase's June developer update: $500M Series F led by GIC, Auth passkeys in beta, the Supabase ChatGPT app, the Supabase plugin for AI coding agents, and Multigres 0.1 alpha released.\\nFrom 2026-06-03, new Free plan projects using Supabase's default SMTP cannot customize auth email templates. Existing projects keep theirs. Configure a custom SMTP provider to customize.\\n## Passkeys for Supabase Auth (Beta)\\n2026-05-28 · New Feature · Platform · [supabase.com/changelog/46458-passkeys-for-supabase-auth-beta](https://supabase.com/changelog/46458-passkeys-for-supabase-auth-beta)\\nSupabase Auth now supports passkeys (Beta): passwordless, phishing-resistant sign-in built on WebAuthn using biometrics, a device PIN, or a hardware security key. No action required.\\n2026-05-25 · Breaking Change · Database · [supabase.com/changelog/46320-breaking-change-in-pg-graphql-1-6-0-graphql-introspection-disabled-by-default](https://supabase.com/changelog/46320-breaking-change-in-pg-graphql-1-6-0-graphql-introspection-disabled-by-default)\\n## Feature Preview: Temporary token-based database access\\n2026-05-25 · New Feature · Database · [supabase.com/changelog/46346-feature-preview-temporary-token-based-database-access](https://supabase.com/changelog/46346-feature-preview-temporary-token-based-database-access)\\nSupabase project owners and admins can grant temporary database access via Personal Access Tokens (Feature Preview), scoped by role with expiry up to 90 days. No password disclosure.\\n2026-05-18 · Breaking Change · Database · [supabase.com/changelog/46081-self-hosted-supabase-switching-studio-from-supabase-admin-to-postgres-breaking-change](https://supabase.com/changelog/46081-self-hosted-supabase-switching-studio-from-supabase-admin-to-postgres-breaking-change)\\n2026-05-18 · Breaking Change · Database · [supabase.com/changelog/46080-self-hosted-supabase-upgrading-from-pg-15-to-17-breaking-change](https://supabase.com/changelog/46080-self-hosted-supabase-upgrading-from-pg-15-to-17-breaking-change)\\nSupabase client libraries (supabase-js, auth-js, realtime-js, functions-js, storage-js, postgrest-js) drop Node.js 20 support on 2026-06-30. Upgrade to Node.js 22 or later before that date.\\n2026-05-07 · Improvement · Auth, Data APIs, Database, Dev Workflows, Edge Functions, Security, Studio · [supabase.com/changelog/45702-developer-update-may-2026](https://supabase.com/changelog/45702-developer-update-may-2026)\\nSupabase's May developer update: custom OAuth/OIDC providers for Auth, ISO 27001 certification, @supabase/server SDK, branching without Git by default, plus Data API auto-exposure changes.\\n## Breaking Change: OAuth token endpoint will return HTTP 200 instead of 201\\nFinished supabase init.\\nCreated new migration at supabase/migrations/20260801155605_create_todos.sql\\n\\n--- files ---\\nsupabase/migrations/20260801155605_create_todos.sql\\nsupabase/.temp/cli-latest\\nsupabase/config.toml\\n\",\n \"stderr\": \"curl: (23) Failure writing output to destination\\nA new version of Supabase CLI is available: v2.111.0 (currently installed v2.67.1)\\nWe recommend updating regularly for new features and bug fixes: https://supabase.com/docs/guides/cli/getting-started#updating-the-supabase-cli\\nA new version of Supabase CLI is available: v2.111.0 (currently installed v2.67.1)\\nWe recommend updating regularly for new features and bug fixes: https://supabase.com/docs/guides/cli/getting-started#updating-the-supabase-cli\\n\"\n}", + "input": "{\n \"command\": \"curl -fsSL https://supabase.com/changelog.md | grep -i -E 'breaking-change|auth|database|api' | head -30 || true\\nsupabase init --force\\nsupabase migration new create_todos\\nprintf '\\\\n--- files ---\\\\n'; find supabase -maxdepth 2 -type f -print\",\n \"timeout_sec\": 240\n}", + "output": "{\n \"exit_code\": 0,\n \"stdout\": \"2026-07-29 · Bug Fix · Observability, Platform · [supabase.com/changelog/admin-api-metrics-panic-fix](https://supabase.com/changelog/admin-api-metrics-panic-fix)\\n## Migration of Supabase Management API `logs.all` analytics endpoint to `logs` endpoint\\n2026-07-23 · Breaking Change · Platform · [supabase.com/changelog/48235-migration-of-supabase-management-api-logs-all-analytics-endpoint-to-logs-endpoint](https://supabase.com/changelog/48235-migration-of-supabase-management-api-logs-all-analytics-endpoint-to-logs-endpoint)\\nThe Supabase Management API `logs.all` endpoint is removed on 2026-09-23. Scripts calling it must migrate to the new ClickHouse-backed `logs` endpoint, which accepts ClickHouse SQL only.\\n2026-07-22 · Breaking Change · Database, Security · [supabase.com/changelog/extension-version-pinning-ignored](https://supabase.com/changelog/extension-version-pinning-ignored)\\n## Self-hosted Supabase: Envoy becomes the default API gateway (breaking change)\\n2026-07-17 · Breaking Change · Platform · [supabase.com/changelog/48048-self-hosted-supabase-envoy-becomes-the-default-api-gateway-b](https://supabase.com/changelog/48048-self-hosted-supabase-envoy-becomes-the-default-api-gateway-b)\\nSelf-hosted Supabase makes Envoy the default API gateway, replacing Kong, the week of 2026-08-09. Opt back into Kong if you rely on its HTTPS listener, a custom `kong.yml`, or the kong service name.\\n2026-07-09 · Improvement · Data APIs, Database, Realtime · [supabase.com/changelog/47796-developer-update-july-2026](https://supabase.com/changelog/47796-developer-update-july-2026)\\nPostgres `log_connections` defaults to off for new projects and existing Free and Pro projects from 2026-07-09, cutting log noise. Re-enable it via the dashboard or Management API.\\n## Self-hosted Supabase: API_EXTERNAL_URL to include /auth/v1\\n2026-06-18 · Breaking Change · Auth · [supabase.com/changelog/47093-self-hosted-supabase-api-external-url-to-include-auth-v1](https://supabase.com/changelog/47093-self-hosted-supabase-api-external-url-to-include-auth-v1)\\nSelf-hosted Supabase's default `API_EXTERNAL_URL` now includes `/auth/v1` from the week of 2026-07-06. SAML SSO users must repoint their IdP to the new `/auth/v1/sso/saml/*` endpoints.\\nSupabase Realtime Broadcast now sends and receives binary payloads (bytea) over WebSockets, the REST API, and the database, cutting JSON overhead for sensor streams and image frames.\\n2026-06-06 · Improvement · Auth, CLI, Database · [supabase.com/changelog/46689-developer-update-june-2026](https://supabase.com/changelog/46689-developer-update-june-2026)\\nSupabase's June developer update: $500M Series F led by GIC, Auth passkeys in beta, the Supabase ChatGPT app, the Supabase plugin for AI coding agents, and Multigres 0.1 alpha released.\\nFrom 2026-06-03, new Free plan projects using Supabase's default SMTP cannot customize auth email templates. Existing projects keep theirs. Configure a custom SMTP provider to customize.\\n## Passkeys for Supabase Auth (Beta)\\n2026-05-28 · New Feature · Platform · [supabase.com/changelog/46458-passkeys-for-supabase-auth-beta](https://supabase.com/changelog/46458-passkeys-for-supabase-auth-beta)\\nSupabase Auth now supports passkeys (Beta): passwordless, phishing-resistant sign-in built on WebAuthn using biometrics, a device PIN, or a hardware security key. No action required.\\n2026-05-25 · Breaking Change · Database · [supabase.com/changelog/46320-breaking-change-in-pg-graphql-1-6-0-graphql-introspection-disabled-by-default](https://supabase.com/changelog/46320-breaking-change-in-pg-graphql-1-6-0-graphql-introspection-disabled-by-default)\\n## Feature Preview: Temporary token-based database access\\n2026-05-25 · New Feature · Database · [supabase.com/changelog/46346-feature-preview-temporary-token-based-database-access](https://supabase.com/changelog/46346-feature-preview-temporary-token-based-database-access)\\nSupabase project owners and admins can grant temporary database access via Personal Access Tokens (Feature Preview), scoped by role with expiry up to 90 days. No password disclosure.\\n2026-05-18 · Breaking Change · Database · [supabase.com/changelog/46081-self-hosted-supabase-switching-studio-from-supabase-admin-to-postgres-breaking-change](https://supabase.com/changelog/46081-self-hosted-supabase-switching-studio-from-supabase-admin-to-postgres-breaking-change)\\n2026-05-18 · Breaking Change · Database · [supabase.com/changelog/46080-self-hosted-supabase-upgrading-from-pg-15-to-17-breaking-change](https://supabase.com/changelog/46080-self-hosted-supabase-upgrading-from-pg-15-to-17-breaking-change)\\nSupabase client libraries (supabase-js, auth-js, realtime-js, functions-js, storage-js, postgrest-js) drop Node.js 20 support on 2026-06-30. Upgrade to Node.js 22 or later before that date.\\n2026-05-07 · Improvement · Auth, Data APIs, Database, Dev Workflows, Edge Functions, Security, Studio · [supabase.com/changelog/45702-developer-update-may-2026](https://supabase.com/changelog/45702-developer-update-may-2026)\\nSupabase's May developer update: custom OAuth/OIDC providers for Auth, ISO 27001 certification, @supabase/server SDK, branching without Git by default, plus Data API auto-exposure changes.\\n## Breaking Change: OAuth token endpoint will return HTTP 200 instead of 201\\nFinished supabase init.\\nCreated new migration at supabase/migrations/20260801155605_create_todos.sql\\n\\n--- files ---\\nsupabase/migrations/20260801155605_create_todos.sql\\nsupabase/.temp/cli-latest\\nsupabase/config.toml\\n\",\n \"stderr\": \"curl: (23) Failure writing output to destination\\nA new version of Supabase CLI is available: v2.111.0 (currently installed v2.67.1)\\nWe recommend updating regularly for new features and bug fixes: https://supabase.com/docs/guides/cli/getting-started#updating-the-supabase-cli\\nA new version of Supabase CLI is available: v2.111.0 (currently installed v2.67.1)\\nWe recommend updating regularly for new features and bug fixes: https://supabase.com/docs/guides/cli/getting-started#updating-the-supabase-cli\\n\"\n}" + }, + { + "id": "build-cli-001-bootstrap-app-tool-10", + "title": "Tool: files_write", + "startTime": "2026-08-01T15:56:11.255Z", + "endTime": "2026-08-01T15:56:12.836Z", + "duration": 1581, + "type": "tool_execution", + "status": "success", + "raw": "{\n \"ok\": true\n}", + "input": "{\n \"path\": \"supabase/migrations/20260801155605_create_todos.sql\",\n \"contents\": \"create table public.todos (\\n id bigint generated by default as identity primary key,\\n task text not null,\\n is_complete boolean not null default false,\\n created_at timestamptz not null default now()\\n);\\n\\nalter table public.todos enable row level security;\\n\\n-- Todos are readable by signed-in users, but are not writable through the Data API.\\ncreate policy \\\"Authenticated users can read todos\\\"\\n on public.todos\\n for select\\n to authenticated\\n using (true);\\n\\nrevoke all on table public.todos from anon, authenticated;\\ngrant select on table public.todos to authenticated;\\n\\n-- Identity sequences are not needed by clients while writes are disabled.\\nrevoke all on sequence public.todos_id_seq from anon, authenticated;\\n\"\n}", + "output": "{\n \"ok\": true\n}" + }, + { + "id": "build-cli-001-bootstrap-app-tool-11", + "title": "Tool: files_write", + "startTime": "2026-08-01T15:56:12.836Z", + "endTime": "2026-08-01T15:56:39.874Z", + "duration": 27038, + "type": "tool_execution", + "status": "success", + "raw": "{\n \"ok\": true\n}", + "input": "{\n \"path\": \"supabase/seed.sql\",\n \"contents\": \"insert into public.todos (task, is_complete)\\nvalues\\n ('Review the project brief', false),\\n ('Set up the local development environment', true);\\n\"\n}", + "output": "{\n \"ok\": true\n}" + }, + { + "id": "build-cli-001-bootstrap-app-tool-12", + "title": "Tool: bash", + "startTime": "2026-08-01T15:56:39.874Z", + "endTime": "2026-08-01T15:56:42.141Z", + "duration": 2267, + "type": "tool_execution", + "status": "success", + "raw": "{\n \"exit_code\": 0,\n \"stdout\": \"\\n╭─────────────────────────────────────────────────╮\\n│ 🌐 APIs │\\n├─────────────┬───────────────────────────────────┤\\n│ Project URL │ http://127.0.0.1:54321 │\\n│ REST │ http://127.0.0.1:54321/rest/v1 │\\n│ GraphQL │ http://127.0.0.1:54321/graphql/v1 │\\n╰─────────────┴───────────────────────────────────╯\\n\\n╭───────────────────────────────────────────────────────────────╮\\n│ ⛁ Database │\\n├─────┬─────────────────────────────────────────────────────────┤\\n│ URL │ postgresql://postgres:REDACTED@127.0.0.1:54322/postgres │\\n╰─────┴─────────────────────────────────────────────────────────╯\\n\\n╭──────────────────────────────────────────────────────────────╮\\n│ 🔑 Authentication Keys │\\n├─────────────┬────────────────────────────────────────────────┤\\n│ Publishable │ sb_publishable_ACJWlzQHlZjBrEguHvfOxg_3BJgxAaH │\\n│ Secret │ sb_secret_REDACTED │\\n╰─────────────┴────────────────────────────────────────────────╯\\n\\n\\n\",\n \"stderr\": \" auth Skipped - Image is already present locally \\n db Skipped - Image is already present locally \\n gateway Skipped - Image is already present locally \\n api Skipped - Image is already present locally \\nStarting database...\\nInitialising schema...\\nSeeding globals from roles.sql...\\nApplying migration 20260801155605_create_todos.sql...\\nSeeding data from supabase/seed.sql...\\nStarting containers...\\nWaiting for health checks...\\nStarted supabase local development setup.\\n\\nA new version of Supabase CLI is available: v2.111.0 (currently installed v2.67.1)\\nWe recommend updating regularly for new features and bug fixes: https://supabase.com/docs/guides/cli/getting-started#updating-the-supabase-cli\\n\"\n}", + "input": "{\n \"command\": \"supabase start\",\n \"timeout_sec\": 600\n}", + "output": "{\n \"exit_code\": 0,\n \"stdout\": \"\\n╭─────────────────────────────────────────────────╮\\n│ 🌐 APIs │\\n├─────────────┬───────────────────────────────────┤\\n│ Project URL │ http://127.0.0.1:54321 │\\n│ REST │ http://127.0.0.1:54321/rest/v1 │\\n│ GraphQL │ http://127.0.0.1:54321/graphql/v1 │\\n╰─────────────┴───────────────────────────────────╯\\n\\n╭───────────────────────────────────────────────────────────────╮\\n│ ⛁ Database │\\n├─────┬─────────────────────────────────────────────────────────┤\\n│ URL │ postgresql://postgres:REDACTED@127.0.0.1:54322/postgres │\\n╰─────┴─────────────────────────────────────────────────────────╯\\n\\n╭──────────────────────────────────────────────────────────────╮\\n│ 🔑 Authentication Keys │\\n├─────────────┬────────────────────────────────────────────────┤\\n│ Publishable │ sb_publishable_ACJWlzQHlZjBrEguHvfOxg_3BJgxAaH │\\n│ Secret │ sb_secret_REDACTED │\\n╰─────────────┴────────────────────────────────────────────────╯\\n\\n\\n\",\n \"stderr\": \" auth Skipped - Image is already present locally \\n db Skipped - Image is already present locally \\n gateway Skipped - Image is already present locally \\n api Skipped - Image is already present locally \\nStarting database...\\nInitialising schema...\\nSeeding globals from roles.sql...\\nApplying migration 20260801155605_create_todos.sql...\\nSeeding data from supabase/seed.sql...\\nStarting containers...\\nWaiting for health checks...\\nStarted supabase local development setup.\\n\\nA new version of Supabase CLI is available: v2.111.0 (currently installed v2.67.1)\\nWe recommend updating regularly for new features and bug fixes: https://supabase.com/docs/guides/cli/getting-started#updating-the-supabase-cli\\n\"\n}" + }, + { + "id": "build-cli-001-bootstrap-app-tool-13", + "title": "Tool: bash", + "startTime": "2026-08-01T15:56:42.141Z", + "endTime": "2026-08-01T15:56:47.007Z", + "duration": 4866, + "type": "tool_execution", + "status": "success", + "raw": "{\n \"exit_code\": 0,\n \"stdout\": \"ANON_KEY=\\\"REDACTED_JWT\\\"\\nAPI_URL=\\\"http://127.0.0.1:54321\\\"\\nDB_URL=\\\"postgresql://postgres:REDACTED@127.0.0.1:54322/postgres\\\"\\nGRAPHQL_URL=\\\"http://127.0.0.1:54321/graphql/v1\\\"\\nJWT_SECRET=\\\"super-secret-jwt-token-with-at-least-32-characters-long\\\"\\nPUBLISHABLE_KEY=\\\"sb_publishable_ACJWlzQHlZjBrEguHvfOxg_3BJgxAaH\\\"\\nREST_URL=\\\"http://127.0.0.1:54321/rest/v1\\\"\\nSECRET_KEY=\\\"sb_secret_REDACTED\\\"\\nSERVICE_ROLE_KEY=\\\"REDACTED_JWT\\\"\\n\",\n \"stderr\": \"Stopped services: [supabase_inbucket_sandbox-0cf0bd36 supabase_realtime_sandbox-0cf0bd36 supabase_storage_sandbox-0cf0bd36 supabase_imgproxy_sandbox-0cf0bd36 supabase_pg_meta_sandbox-0cf0bd36 supabase_studio_sandbox-0cf0bd36 supabase_edge_runtime_sandbox-0cf0bd36 supabase_analytics_sandbox-0cf0bd36 supabase_vector_sandbox-0cf0bd36 supabase_pooler_sandbox-0cf0bd36]\\nA new version of Supabase CLI is available: v2.111.0 (currently installed v2.67.1)\\nWe recommend updating regularly for new features and bug fixes: https://supabase.com/docs/guides/cli/getting-started#updating-the-supabase-cli\\n\"\n}", + "input": "{\n \"command\": \"supabase status -o env\",\n \"timeout_sec\": 240\n}", + "output": "{\n \"exit_code\": 0,\n \"stdout\": \"ANON_KEY=\\\"REDACTED_JWT\\\"\\nAPI_URL=\\\"http://127.0.0.1:54321\\\"\\nDB_URL=\\\"postgresql://postgres:REDACTED@127.0.0.1:54322/postgres\\\"\\nGRAPHQL_URL=\\\"http://127.0.0.1:54321/graphql/v1\\\"\\nJWT_SECRET=\\\"super-secret-jwt-token-with-at-least-32-characters-long\\\"\\nPUBLISHABLE_KEY=\\\"sb_publishable_ACJWlzQHlZjBrEguHvfOxg_3BJgxAaH\\\"\\nREST_URL=\\\"http://127.0.0.1:54321/rest/v1\\\"\\nSECRET_KEY=\\\"sb_secret_REDACTED\\\"\\nSERVICE_ROLE_KEY=\\\"REDACTED_JWT\\\"\\n\",\n \"stderr\": \"Stopped services: [supabase_inbucket_sandbox-0cf0bd36 supabase_realtime_sandbox-0cf0bd36 supabase_storage_sandbox-0cf0bd36 supabase_imgproxy_sandbox-0cf0bd36 supabase_pg_meta_sandbox-0cf0bd36 supabase_studio_sandbox-0cf0bd36 supabase_edge_runtime_sandbox-0cf0bd36 supabase_analytics_sandbox-0cf0bd36 supabase_vector_sandbox-0cf0bd36 supabase_pooler_sandbox-0cf0bd36]\\nA new version of Supabase CLI is available: v2.111.0 (currently installed v2.67.1)\\nWe recommend updating regularly for new features and bug fixes: https://supabase.com/docs/guides/cli/getting-started#updating-the-supabase-cli\\n\"\n}" + }, + { + "id": "build-cli-001-bootstrap-app-tool-14", + "title": "Tool: bash", + "startTime": "2026-08-01T15:56:47.007Z", + "endTime": "2026-08-01T15:56:53.614Z", + "duration": 6607, + "type": "tool_execution", + "status": "success", + "raw": "{\n \"exit_code\": 127,\n \"stdout\": \"--- signed out GET ---\\nHTTP/1.1 401 Unauthorized\\r\\nContent-Type: application/json; charset=utf-8\\r\\nContent-Length: 89\\r\\nConnection: keep-alive\\r\\nDate: Sat, 01 Aug 2026 15:56:46 GMT\\r\\nServer: postgrest/14.1\\r\\nProxy-Status: PostgREST; error=42501\\r\\nWWW-Authenticate: Bearer\\r\\nAccess-Control-Allow-Origin: *\\r\\nX-Kong-Upstream-Latency: 17\\r\\nX-Kong-Proxy-Latency: 1\\r\\nVia: kong/2.8.1\\r\\n\\r\\n{\\\"code\\\":\\\"42501\\\",\\\"details\\\":null,\\\"hint\\\":null,\\\"message\\\":\\\"permission denied for table todos\\\"}--- create test user ---\\n\",\n \"stderr\": \"bash: line 8: python3: command not found\\n\"\n}", + "input": "{\n \"command\": \"set -e\\nAPI=http://127.0.0.1:54321\\nKEY='REDACTED_JWT'\\nprintf '%s\\\\n' '--- signed out GET ---'\\ncurl -sS -i \\\"$API/rest/v1/todos?select=*\\\" -H \\\"apikey: $KEY\\\" | head -20\\nprintf '%s\\\\n' '--- create test user ---'\\nSIGNUP=$(curl -sS \\\"$API/auth/v1/signup\\\" -H \\\"apikey: $KEY\\\" -H 'Content-Type: application/json' --data '{\\\"email\\\":\\\"todos-test@example.com\\\",\\\"password\\\":\\\"password123\\\"}')\\nprintf '%s\\\\n' \\\"$SIGNUP\\\" | python3 -c 'import json,sys; d=json.load(sys.stdin); print(\\\"user\\\", d.get(\\\"user\\\",{}).get(\\\"id\\\"), \\\"token\\\", bool(d.get(\\\"access_token\\\")))'\\nTOKEN=$(printf '%s' \\\"$SIGNUP\\\" | python3 -c 'import json,sys; print(json.load(sys.stdin).get(\\\"access_token\\\", \\\"\\\"))')\\nprintf '%s\\\\n' '--- signed in GET ---'\\ncurl -sS -i \\\"$API/rest/v1/todos?select=*\\\" -H \\\"apikey: $KEY\\\" -H \\\"Authorization: Bearer $TOKEN\\\" | head -20\\nprintf '%s\\\\n' '--- signed in POST (must fail) ---'\\ncurl -sS -i -X POST \\\"$API/rest/v1/todos\\\" -H \\\"apikey: $KEY\\\" -H \\\"Authorization: Bearer $TOKEN\\\" -H 'Content-Type: application/json' --data '{\\\"task\\\":\\\"should fail\\\"}' | head -20\",\n \"timeout_sec\": 240\n}", + "output": "{\n \"exit_code\": 127,\n \"stdout\": \"--- signed out GET ---\\nHTTP/1.1 401 Unauthorized\\r\\nContent-Type: application/json; charset=utf-8\\r\\nContent-Length: 89\\r\\nConnection: keep-alive\\r\\nDate: Sat, 01 Aug 2026 15:56:46 GMT\\r\\nServer: postgrest/14.1\\r\\nProxy-Status: PostgREST; error=42501\\r\\nWWW-Authenticate: Bearer\\r\\nAccess-Control-Allow-Origin: *\\r\\nX-Kong-Upstream-Latency: 17\\r\\nX-Kong-Proxy-Latency: 1\\r\\nVia: kong/2.8.1\\r\\n\\r\\n{\\\"code\\\":\\\"42501\\\",\\\"details\\\":null,\\\"hint\\\":null,\\\"message\\\":\\\"permission denied for table todos\\\"}--- create test user ---\\n\",\n \"stderr\": \"bash: line 8: python3: command not found\\n\"\n}" + }, + { + "id": "build-cli-001-bootstrap-app-tool-15", + "title": "Tool: bash", + "startTime": "2026-08-01T15:56:53.614Z", + "endTime": "2026-08-01T15:57:12.494Z", + "duration": 18880, + "type": "tool_execution", + "status": "success", + "raw": "{\n \"exit_code\": 0,\n \"stdout\": \"{\\\"access_token\\\":\\\"REDACTED_JWT\\\",\\\"token_type\\\":\\\"bearer\\\",\\\"expires_in\\\":3600,\\\"expires_at\\\":1785603413,\\\"refresh_token\\\":\\\"a6fojjtgf3od\\\",\\\"user\\\":{\\\"id\\\":\\\"0a531c9a-e449-439f-89a9-fa8b9e56a719\\\",\\\"aud\\\":\\\"authenticated\\\",\\\"role\\\":\\\"authenticated\\\",\\\"email\\\":\\\"todos-test2@example.com\\\",\\\"email_confirmed_at\\\":\\\"2026-08-01T15:56:53.578427185Z\\\",\\\"phone\\\":\\\"\\\",\\\"last_sign_in_at\\\":\\\"2026-08-01T15:56:53.592228173Z\\\",\\\"app_metadata\\\":{\\\"provider\\\":\\\"email\\\",\\\"providers\\\":[\\\"email\\\"]},\\\"user_metadata\\\":{\\\"email\\\":\\\"todos-test2@example.com\\\",\\\"email_verified\\\":true,\\\"phone_verified\\\":false,\\\"sub\\\":\\\"0a531c9a-e449-439f-89a9-fa8b9e56a719\\\"},\\\"identities\\\":[{\\\"identity_id\\\":\\\"476290fc-c8b2-4f0e-8e2a-66b6fae80ac1\\\",\\\"id\\\":\\\"0a531c9a-e449-439f-89a9-fa8b9e56a719\\\",\\\"user_id\\\":\\\"0a531c9a-e449-439f-89a9-fa8b9e56a719\\\",\\\"identity_data\\\":{\\\"email\\\":\\\"todos-test2@example.com\\\",\\\"email_verified\\\":true,\\\"phone_verified\\\":false,\\\"sub\\\":\\\"0a531c9a-e449-439f-89a9-fa8b9e56a719\\\"},\\\"provider\\\":\\\"email\\\",\\\"last_sign_in_at\\\":\\\"2026-08-01T15:56:53.577772144Z\\\",\\\"created_at\\\":\\\"2026-08-01T15:56:53.577785Z\\\",\\\"updated_at\\\":\\\"2026-08-01T15:56:53.577785Z\\\",\\\"email\\\":\\\"todos-test2@example.com\\\"}],\\\"created_at\\\":\\\"2026-08-01T15:56:53.576485Z\\\",\\\"updated_at\\\":\\\"2026-08-01T15:56:53.593075Z\\\",\\\"is_anonymous\\\":false}}\\ntoken_length=812\\nHTTP/1.1 200 OK\\r\\nContent-Type: application/json; charset=utf-8\\r\\nContent-Length: 241\\r\\nConnection: keep-alive\\r\\nDate: Sat, 01 Aug 2026 15:56:53 GMT\\r\\nServer: postgrest/14.1\\r\\nContent-Range: 0-1/*\\r\\nContent-Location: /todos?select=%2A\\r\\nContent-Profile: public\\r\\nAccess-Control-Allow-Origin: *\\r\\nX-Kong-Upstream-Latency: 1\\r\\nX-Kong-Proxy-Latency: 0\\r\\nVia: kong/2.8.1\\r\\n\\r\\n[{\\\"id\\\":1,\\\"task\\\":\\\"Review the project brief\\\",\\\"is_complete\\\":false,\\\"created_at\\\":\\\"2026-08-01T15:56:29.205815+00:00\\\"}, \\n {\\\"id\\\":2,\\\"task\\\":\\\"Set up the local development environment\\\",\\\"is_complete\\\":true,\\\"created_at\\\":\\\"2026-08-01T15:56:29.205815+00:00\\\"}]POST\\nHTTP/1.1 403 Forbidden\\r\\nContent-Type: application/json; charset=utf-8\\r\\nContent-Length: 89\\r\\nConnection: keep-alive\\r\\nDate: Sat, 01 Aug 2026 15:56:53 GMT\\r\\nServer: postgrest/14.1\\r\\nProxy-Status: PostgREST; error=42501\\r\\nAccess-Control-Allow-Origin: *\\r\\nX-Kong-Upstream-Latency: 1\\r\\nX-Kong-Proxy-Latency: 0\\r\\nVia: kong/2.8.1\\r\\n\\r\\n{\\\"code\\\":\\\"42501\\\",\\\"details\\\":null,\\\"hint\\\":null,\\\"message\\\":\\\"permission denied for table todos\\\"}\",\n \"stderr\": \"\"\n}", + "input": "{\n \"command\": \"command -v jq || true; KEY='REDACTED_JWT'; API=http://127.0.0.1:54321; curl -sS \\\"$API/auth/v1/signup\\\" -H \\\"apikey: $KEY\\\" -H 'Content-Type: application/json' --data '{\\\"email\\\":\\\"todos-test2@example.com\\\",\\\"password\\\":\\\"password123\\\"}' | tee /tmp/signup.json; echo; cat /tmp/signup.json | sed -n 's/.*\\\"access_token\\\":\\\"\\\\([^\\\"]*\\\\)\\\".*/\\\\1/p' > /tmp/token; TOKEN=$(cat /tmp/token); echo token_length=${#TOKEN}; curl -sS -i \\\"$API/rest/v1/todos?select=*\\\" -H \\\"apikey: $KEY\\\" -H \\\"Authorization: Bearer $TOKEN\\\" | head -20; echo POST; curl -sS -i -X POST \\\"$API/rest/v1/todos\\\" -H \\\"apikey: $KEY\\\" -H \\\"Authorization: Bearer $TOKEN\\\" -H 'Content-Type: application/json' --data '{\\\"task\\\":\\\"should fail\\\"}' | head -20\",\n \"timeout_sec\": 240\n}", + "output": "{\n \"exit_code\": 0,\n \"stdout\": \"{\\\"access_token\\\":\\\"REDACTED_JWT\\\",\\\"token_type\\\":\\\"bearer\\\",\\\"expires_in\\\":3600,\\\"expires_at\\\":1785603413,\\\"refresh_token\\\":\\\"a6fojjtgf3od\\\",\\\"user\\\":{\\\"id\\\":\\\"0a531c9a-e449-439f-89a9-fa8b9e56a719\\\",\\\"aud\\\":\\\"authenticated\\\",\\\"role\\\":\\\"authenticated\\\",\\\"email\\\":\\\"todos-test2@example.com\\\",\\\"email_confirmed_at\\\":\\\"2026-08-01T15:56:53.578427185Z\\\",\\\"phone\\\":\\\"\\\",\\\"last_sign_in_at\\\":\\\"2026-08-01T15:56:53.592228173Z\\\",\\\"app_metadata\\\":{\\\"provider\\\":\\\"email\\\",\\\"providers\\\":[\\\"email\\\"]},\\\"user_metadata\\\":{\\\"email\\\":\\\"todos-test2@example.com\\\",\\\"email_verified\\\":true,\\\"phone_verified\\\":false,\\\"sub\\\":\\\"0a531c9a-e449-439f-89a9-fa8b9e56a719\\\"},\\\"identities\\\":[{\\\"identity_id\\\":\\\"476290fc-c8b2-4f0e-8e2a-66b6fae80ac1\\\",\\\"id\\\":\\\"0a531c9a-e449-439f-89a9-fa8b9e56a719\\\",\\\"user_id\\\":\\\"0a531c9a-e449-439f-89a9-fa8b9e56a719\\\",\\\"identity_data\\\":{\\\"email\\\":\\\"todos-test2@example.com\\\",\\\"email_verified\\\":true,\\\"phone_verified\\\":false,\\\"sub\\\":\\\"0a531c9a-e449-439f-89a9-fa8b9e56a719\\\"},\\\"provider\\\":\\\"email\\\",\\\"last_sign_in_at\\\":\\\"2026-08-01T15:56:53.577772144Z\\\",\\\"created_at\\\":\\\"2026-08-01T15:56:53.577785Z\\\",\\\"updated_at\\\":\\\"2026-08-01T15:56:53.577785Z\\\",\\\"email\\\":\\\"todos-test2@example.com\\\"}],\\\"created_at\\\":\\\"2026-08-01T15:56:53.576485Z\\\",\\\"updated_at\\\":\\\"2026-08-01T15:56:53.593075Z\\\",\\\"is_anonymous\\\":false}}\\ntoken_length=812\\nHTTP/1.1 200 OK\\r\\nContent-Type: application/json; charset=utf-8\\r\\nContent-Length: 241\\r\\nConnection: keep-alive\\r\\nDate: Sat, 01 Aug 2026 15:56:53 GMT\\r\\nServer: postgrest/14.1\\r\\nContent-Range: 0-1/*\\r\\nContent-Location: /todos?select=%2A\\r\\nContent-Profile: public\\r\\nAccess-Control-Allow-Origin: *\\r\\nX-Kong-Upstream-Latency: 1\\r\\nX-Kong-Proxy-Latency: 0\\r\\nVia: kong/2.8.1\\r\\n\\r\\n[{\\\"id\\\":1,\\\"task\\\":\\\"Review the project brief\\\",\\\"is_complete\\\":false,\\\"created_at\\\":\\\"2026-08-01T15:56:29.205815+00:00\\\"}, \\n {\\\"id\\\":2,\\\"task\\\":\\\"Set up the local development environment\\\",\\\"is_complete\\\":true,\\\"created_at\\\":\\\"2026-08-01T15:56:29.205815+00:00\\\"}]POST\\nHTTP/1.1 403 Forbidden\\r\\nContent-Type: application/json; charset=utf-8\\r\\nContent-Length: 89\\r\\nConnection: keep-alive\\r\\nDate: Sat, 01 Aug 2026 15:56:53 GMT\\r\\nServer: postgrest/14.1\\r\\nProxy-Status: PostgREST; error=42501\\r\\nAccess-Control-Allow-Origin: *\\r\\nX-Kong-Upstream-Latency: 1\\r\\nX-Kong-Proxy-Latency: 0\\r\\nVia: kong/2.8.1\\r\\n\\r\\n{\\\"code\\\":\\\"42501\\\",\\\"details\\\":null,\\\"hint\\\":null,\\\"message\\\":\\\"permission denied for table todos\\\"}\",\n \"stderr\": \"\"\n}" + }, + { + "id": "build-cli-001-bootstrap-app-tool-16", + "title": "Tool: bash", + "startTime": "2026-08-01T15:57:12.494Z", + "endTime": "2026-08-01T15:57:18.389Z", + "duration": 5895, + "type": "tool_execution", + "status": "success", + "raw": "{\n \"exit_code\": 0,\n \"stdout\": \"\\n \\n Local | Remote | Time (UTC) \\n ----------------|----------------|---------------------\\n 20260801155605 | 20260801155605 | 2026-08-01 15:56:05 \\n\\n relname | relrowsecurity \\n---------+----------------\\n todos | t\\n(1 row)\\n\\n policyname | roles | cmd | qual \\n------------------------------------+-----------------+--------+------\\n Authenticated users can read todos | {authenticated} | SELECT | true\\n(1 row)\\n\\n grantee | privilege_type \\n---------------+----------------\\n authenticated | SELECT\\n(1 row)\\n\\n\",\n \"stderr\": \"Resetting local database...\\nRecreating database...\\nInitialising schema...\\nSeeding globals from roles.sql...\\nApplying migration 20260801155605_create_todos.sql...\\nSeeding data from supabase/seed.sql...\\nRestarting containers...\\nFinished supabase db reset on branch main.\\nA new version of Supabase CLI is available: v2.111.0 (currently installed v2.67.1)\\nWe recommend updating regularly for new features and bug fixes: https://supabase.com/docs/guides/cli/getting-started#updating-the-supabase-cli\\nConnecting to local database...\\nA new version of Supabase CLI is available: v2.111.0 (currently installed v2.67.1)\\nWe recommend updating regularly for new features and bug fixes: https://supabase.com/docs/guides/cli/getting-started#updating-the-supabase-cli\\n\"\n}", + "input": "{\n \"command\": \"supabase db reset && supabase migration list --local && PGPASSWORD=postgres psql 'postgresql://postgres@127.0.0.1:54322/postgres' -X -c \\\"select relname, relrowsecurity from pg_class where oid='public.todos'::regclass;\\\" -c \\\"select policyname, roles, cmd, qual from pg_policies where schemaname='public' and tablename='todos';\\\" -c \\\"select grantee, privilege_type from information_schema.role_table_grants where table_schema='public' and table_name='todos' and grantee in ('anon','authenticated') order by 1,2;\\\"\",\n \"timeout_sec\": 600\n}", + "output": "{\n \"exit_code\": 0,\n \"stdout\": \"\\n \\n Local | Remote | Time (UTC) \\n ----------------|----------------|---------------------\\n 20260801155605 | 20260801155605 | 2026-08-01 15:56:05 \\n\\n relname | relrowsecurity \\n---------+----------------\\n todos | t\\n(1 row)\\n\\n policyname | roles | cmd | qual \\n------------------------------------+-----------------+--------+------\\n Authenticated users can read todos | {authenticated} | SELECT | true\\n(1 row)\\n\\n grantee | privilege_type \\n---------------+----------------\\n authenticated | SELECT\\n(1 row)\\n\\n\",\n \"stderr\": \"Resetting local database...\\nRecreating database...\\nInitialising schema...\\nSeeding globals from roles.sql...\\nApplying migration 20260801155605_create_todos.sql...\\nSeeding data from supabase/seed.sql...\\nRestarting containers...\\nFinished supabase db reset on branch main.\\nA new version of Supabase CLI is available: v2.111.0 (currently installed v2.67.1)\\nWe recommend updating regularly for new features and bug fixes: https://supabase.com/docs/guides/cli/getting-started#updating-the-supabase-cli\\nConnecting to local database...\\nA new version of Supabase CLI is available: v2.111.0 (currently installed v2.67.1)\\nWe recommend updating regularly for new features and bug fixes: https://supabase.com/docs/guides/cli/getting-started#updating-the-supabase-cli\\n\"\n}" + }, + { + "id": "build-cli-001-bootstrap-app-tool-17", + "title": "Tool: bash", + "startTime": "2026-08-01T15:57:18.389Z", + "endTime": "2026-08-01T15:57:20.359Z", + "duration": 1970, + "type": "tool_execution", + "status": "success", + "raw": "{\n \"exit_code\": 0,\n \"stdout\": \"signed-out GET status: 401\\nbody: {\\\"code\\\":\\\"42501\\\",\\\"details\\\":null,\\\"hint\\\":null,\\\"message\\\":\\\"permission denied for table todos\\\"}\\nsigned-in GET status: 200\\n[{\\\"task\\\":\\\"Review the project brief\\\",\\\"is_complete\\\":false}, \\n {\\\"task\\\":\\\"Set up the local development environment\\\",\\\"is_complete\\\":true}]\\nsigned-in POST status: 403\\n{\\\"code\\\":\\\"42501\\\",\\\"details\\\":null,\\\"hint\\\":null,\\\"message\\\":\\\"permission denied for table todos\\\"}\\n\",\n \"stderr\": \"\"\n}", + "input": "{\n \"command\": \"API=http://127.0.0.1:54321; KEY='REDACTED_JWT'; echo -n 'signed-out GET status: '; curl -sS -o /tmp/anon-body -w '%{http_code}\\\\n' \\\"$API/rest/v1/todos?select=*\\\" -H \\\"apikey: $KEY\\\"; echo \\\"body: $(cat /tmp/anon-body)\\\"; SIGNUP=$(curl -sS \\\"$API/auth/v1/signup\\\" -H \\\"apikey: $KEY\\\" -H 'Content-Type: application/json' --data '{\\\"email\\\":\\\"verify@example.com\\\",\\\"password\\\":\\\"password123\\\"}'); TOKEN=$(printf '%s' \\\"$SIGNUP\\\" | sed -n 's/.*\\\"access_token\\\":\\\"\\\\([^\\\"]*\\\\)\\\".*/\\\\1/p'); echo -n 'signed-in GET status: '; curl -sS -o /tmp/auth-body -w '%{http_code}\\\\n' \\\"$API/rest/v1/todos?select=task,is_complete\\\" -H \\\"apikey: $KEY\\\" -H \\\"Authorization: Bearer $TOKEN\\\"; cat /tmp/auth-body; echo; echo -n 'signed-in POST status: '; curl -sS -o /tmp/post-body -w '%{http_code}\\\\n' -X POST \\\"$API/rest/v1/todos\\\" -H \\\"apikey: $KEY\\\" -H \\\"Authorization: Bearer $TOKEN\\\" -H 'Content-Type: application/json' --data '{\\\"task\\\":\\\"blocked\\\"}'; cat /tmp/post-body; echo\",\n \"timeout_sec\": 240\n}", + "output": "{\n \"exit_code\": 0,\n \"stdout\": \"signed-out GET status: 401\\nbody: {\\\"code\\\":\\\"42501\\\",\\\"details\\\":null,\\\"hint\\\":null,\\\"message\\\":\\\"permission denied for table todos\\\"}\\nsigned-in GET status: 200\\n[{\\\"task\\\":\\\"Review the project brief\\\",\\\"is_complete\\\":false}, \\n {\\\"task\\\":\\\"Set up the local development environment\\\",\\\"is_complete\\\":true}]\\nsigned-in POST status: 403\\n{\\\"code\\\":\\\"42501\\\",\\\"details\\\":null,\\\"hint\\\":null,\\\"message\\\":\\\"permission denied for table todos\\\"}\\n\",\n \"stderr\": \"\"\n}" + }, + { + "id": "build-cli-001-bootstrap-app-tool-18", + "title": "Tool: bash", + "startTime": "2026-08-01T15:57:20.359Z", + "endTime": "2026-08-01T15:57:23.183Z", + "duration": 2824, + "type": "tool_execution", + "status": "success", + "raw": "{\n \"exit_code\": 0,\n \"stdout\": \"\",\n \"stderr\": \"fatal: not a git repository (or any parent up to mount point /tmp)\\nStopping at filesystem boundary (GIT_DISCOVERY_ACROSS_FILESYSTEM not set).\\n\"\n}", + "input": "{\n \"command\": \"git status --short && git diff -- supabase || true\",\n \"timeout_sec\": 240\n}", + "output": "{\n \"exit_code\": 0,\n \"stdout\": \"\",\n \"stderr\": \"fatal: not a git repository (or any parent up to mount point /tmp)\\nStopping at filesystem boundary (GIT_DISCOVERY_ACROSS_FILESYSTEM not set).\\n\"\n}" + }, + { + "id": "build-cli-001-bootstrap-app-llm-19", + "title": "Assistant", + "startTime": "2026-08-01T15:57:23.183Z", + "endTime": "2026-08-01T15:57:23.183Z", + "duration": 0, + "type": "llm_call", + "status": "success", + "raw": "Implemented the Supabase development setup:\n\n- Added migration for `public.todos`\n- Enabled RLS\n- Allowed `SELECT` only for authenticated users\n- Denied anonymous access and all API writes\n- Added two sample todos in `supabase/seed.sql`\n- Started and reset the local Supabase stack\n- Verified API behavior:\n - Signed out: `401`\n - Signed in read: `200` with todos\n - Signed in write: `403`", + "output": "Implemented the Supabase development setup:\n\n- Added migration for `public.todos`\n- Enabled RLS\n- Allowed `SELECT` only for authenticated users\n- Denied anonymous access and all API writes\n- Added two sample todos in `supabase/seed.sql`\n- Started and reset the local Supabase stack\n- Verified API behavior:\n - Signed out: `401`\n - Signed in read: `200` with todos\n - Signed in write: `403`", + "tokensCount": 40452 + }, + { + "id": "build-cli-001-bootstrap-app-check-20", + "title": "Check passed: supabase project initialised (supabase/config.toml exists)", + "startTime": "2026-08-01T15:57:23.183Z", + "endTime": "2026-08-01T15:57:23.183Z", + "duration": 0, + "type": "event", + "status": "success", + "raw": "", + "output": "" + }, + { + "id": "build-cli-001-bootstrap-app-check-21", + "title": "Check passed: todos table is created by a migration file", + "startTime": "2026-08-01T15:57:23.183Z", + "endTime": "2026-08-01T15:57:23.183Z", + "duration": 0, + "type": "event", + "status": "success", + "raw": "", + "output": "" + }, + { + "id": "build-cli-001-bootstrap-app-check-22", + "title": "Check passed: todos table exists with at least 2 seeded rows", + "startTime": "2026-08-01T15:57:23.183Z", + "endTime": "2026-08-01T15:57:23.183Z", + "duration": 0, + "type": "event", + "status": "success", + "raw": "found 2 rows", + "output": "found 2 rows" + }, + { + "id": "build-cli-001-bootstrap-app-check-23", + "title": "Check passed: row level security is enabled on todos", + "startTime": "2026-08-01T15:57:23.183Z", + "endTime": "2026-08-01T15:57:23.183Z", + "duration": 0, + "type": "event", + "status": "success", + "raw": "", + "output": "" + }, + { + "id": "build-cli-001-bootstrap-app-check-24", + "title": "Check passed: a SELECT policy targets the authenticated role", + "startTime": "2026-08-01T15:57:23.183Z", + "endTime": "2026-08-01T15:57:23.183Z", + "duration": 0, + "type": "event", + "status": "success", + "raw": "", + "output": "" + }, + { + "id": "build-cli-001-bootstrap-app-check-25", + "title": "Check passed: REST API returns no todos to anonymous requests", + "startTime": "2026-08-01T15:57:23.183Z", + "endTime": "2026-08-01T15:57:23.183Z", + "duration": 0, + "type": "event", + "status": "success", + "raw": "error 42501: permission denied for table todos", + "output": "error 42501: permission denied for table todos" + }, + { + "id": "build-cli-001-bootstrap-app-check-26", + "title": "Check passed: REST API returns the todos to authenticated requests", + "startTime": "2026-08-01T15:57:23.183Z", + "endTime": "2026-08-01T15:57:23.183Z", + "duration": 0, + "type": "event", + "status": "success", + "raw": "2 rows", + "output": "2 rows" + } + ], + "tokensCount": 40452 + } + ], + "badges": [ + { + "label": "Passed", + "tone": "success" + }, + { + "label": "0/2 skills", + "tone": "neutral" + }, + { + "label": "gpt-5.6-luna", + "tone": "neutral" + } + ] +} diff --git a/apps/web/src/data/traces/build-cli-002-declarative-schema.json b/apps/web/src/data/traces/build-cli-002-declarative-schema.json new file mode 100644 index 00000000..61a43a79 --- /dev/null +++ b/apps/web/src/data/traces/build-cli-002-declarative-schema.json @@ -0,0 +1,442 @@ +{ + "traceRecord": { + "id": "build-cli-002-declarative-schema", + "name": "build-cli-002-declarative-schema", + "spansCount": 35, + "durationMs": 80468, + "agentDescription": "gpt-5.6-luna", + "totalTokens": 35787 + }, + "spans": [ + { + "id": "eval:build-cli-002-declarative-schema", + "title": "build-cli-002-declarative-schema", + "startTime": "2026-08-01T15:57:58.489Z", + "endTime": "2026-08-01T15:59:18.957Z", + "duration": 80468, + "type": "agent_invocation", + "status": "success", + "raw": "Added nullable `description text` to `public.products`.\n\nUpdated:\n- `supabase/schemas/products.sql`\n- New migration: `20260801155907_add_description_to_products.sql`\n\nApplied and verified in the local Supabase database.", + "output": "Added nullable `description text` to `public.products`.\n\nUpdated:\n- `supabase/schemas/products.sql`\n- New migration: `20260801155907_add_description_to_products.sql`\n\nApplied and verified in the local Supabase database.", + "children": [ + { + "id": "build-cli-002-declarative-schema-msg-1", + "title": "System", + "startTime": "2026-08-01T15:57:58.489Z", + "endTime": "2026-08-01T15:57:58.489Z", + "duration": 0, + "type": "event", + "status": "success", + "raw": "You are an agent solving a Supabase eval task in a Linux workspace. Use the provided tools to inspect and modify the workspace and run commands. When you are done, end your turn with a short summary of what you did.\n\nThe Supabase CLI (`supabase`), docker, psql, git, and curl are installed in the workspace. Use the bash tool to run commands (the working directory is always the workspace root) and the files tools to inspect and modify files. Services started with `supabase start` are reachable on their default 127.0.0.1 ports.\n\n## Available skills\n\nThe following agent skills are installed in this workspace under `.claude/skills/`. Only their names and descriptions are shown — the full instructions are not loaded yet. When a task matches a skill, read `.claude/skills//SKILL.md` with the `files_read` tool for its full instructions, then read any files it references in that directory with `files_read` or `bash`.\n\n- supabase: Use when doing ANY task involving Supabase. Triggers: Supabase products (Database, Auth, Edge Functions, Realtime, Storage, Vectors, Cron, Queues); client libraries and SSR integrations (supabase-js, @supabase/ssr) in Next.js, React, SvelteKit, Astro, Remix; auth issues (login, logout, sessions, JWT, cookies, getSession, getUser, getClaims, RLS); Supabase CLI or MCP server; schema changes, migrations, declarative schemas, security audits, Postgres extensions (pg_graphql, pg_cron, pg_vector).\n- supabase-postgres-best-practices: Postgres best practices maintained by Supabase, for Postgres running anywhere. Load this skill BEFORE writing or changing anything that lives in a Postgres database: creating or altering tables and columns (including choosing column types), schema design, migrations and declarative schema files, RLS policies and the tests that verify them, indexes, triggers, database functions, queues and scheduled jobs (pg_cron, pgmq), vector/semantic search (pgvector), and restoring dumps (pg_restore) or importing data. Also load it when diagnosing slow queries, high CPU, timeouts, EXPLAIN plans, connection exhaustion, locking, bloat, or rows visible to the wrong user or tenant. This is not just a performance guide — schema, migration, security, and SQL authoring tasks need these rules too, even for a one-column change or a single query.", + "input": "You are an agent solving a Supabase eval task in a Linux workspace. Use the provided tools to inspect and modify the workspace and run commands. When you are done, end your turn with a short summary of what you did.\n\nThe Supabase CLI (`supabase`), docker, psql, git, and curl are installed in the workspace. Use the bash tool to run commands (the working directory is always the workspace root) and the files tools to inspect and modify files. Services started with `supabase start` are reachable on their default 127.0.0.1 ports.\n\n## Available skills\n\nThe following agent skills are installed in this workspace under `.claude/skills/`. Only their names and descriptions are shown — the full instructions are not loaded yet. When a task matches a skill, read `.claude/skills//SKILL.md` with the `files_read` tool for its full instructions, then read any files it references in that directory with `files_read` or `bash`.\n\n- supabase: Use when doing ANY task involving Supabase. Triggers: Supabase products (Database, Auth, Edge Functions, Realtime, Storage, Vectors, Cron, Queues); client libraries and SSR integrations (supabase-js, @supabase/ssr) in Next.js, React, SvelteKit, Astro, Remix; auth issues (login, logout, sessions, JWT, cookies, getSession, getUser, getClaims, RLS); Supabase CLI or MCP server; schema changes, migrations, declarative schemas, security audits, Postgres extensions (pg_graphql, pg_cron, pg_vector).\n- supabase-postgres-best-practices: Postgres best practices maintained by Supabase, for Postgres running anywhere. Load this skill BEFORE writing or changing anything that lives in a Postgres database: creating or altering tables and columns (including choosing column types), schema design, migrations and declarative schema files, RLS policies and the tests that verify them, indexes, triggers, database functions, queues and scheduled jobs (pg_cron, pgmq), vector/semantic search (pgvector), and restoring dumps (pg_restore) or importing data. Also load it when diagnosing slow queries, high CPU, timeouts, EXPLAIN plans, connection exhaustion, locking, bloat, or rows visible to the wrong user or tenant. This is not just a performance guide — schema, migration, security, and SQL authoring tasks need these rules too, even for a one-column change or a single query." + }, + { + "id": "build-cli-002-declarative-schema-msg-2", + "title": "User", + "startTime": "2026-08-01T15:57:58.489Z", + "endTime": "2026-08-01T15:57:58.489Z", + "duration": 0, + "type": "event", + "status": "success", + "raw": "Add a description text column to the `products` table in my local Supabase stack", + "input": "Add a description text column to the `products` table in my local Supabase stack" + }, + { + "id": "build-cli-002-declarative-schema-tool-3", + "title": "Tool: files_read", + "startTime": "2026-08-01T15:57:58.489Z", + "endTime": "2026-08-01T15:57:58.561Z", + "duration": 72, + "type": "tool_execution", + "status": "success", + "raw": "{\n \"contents\": \"---\\nname: supabase\\ndescription: \\\"Use when doing ANY task involving Supabase. Triggers: Supabase products (Database, Auth, Edge Functions, Realtime, Storage, Vectors, Cron, Queues); client libraries and SSR integrations (supabase-js, @supabase/ssr) in Next.js, React, SvelteKit, Astro, Remix; auth issues (login, logout, sessions, JWT, cookies, getSession, getUser, getClaims, RLS); Supabase CLI or MCP server; schema changes, migrations, declarative schemas, security audits, Postgres extensions (pg_graphql, pg_cron, pg_vector).\\\"\\nmetadata:\\n author: supabase\\n version: \\\"0.1.2\\\"\\n---\\n\\n# Supabase\\n\\n## Core Principles\\n\\n**1. Supabase changes frequently — verify against changelog and current docs before implementing.**\\nDo not rely on training data for Supabase features. Function signatures, config.toml settings, and API conventions change between versions.\\n\\nFirst, fetch `https://supabase.com/changelog.md` (a lightweight summary index — not a heavy pull), scan for `breaking-change` tags relevant to your task, and follow the linked page for any that apply. Then look up the relevant topic using the documentation access methods below.\\n\\n**2. Verify your work.**\\nAfter implementing any fix, run a test query to confirm the change works. A fix without verification is incomplete.\\n\\n**3. Recover from errors, don't loop.**\\nIf an approach fails after 2-3 attempts, stop and reconsider. Try a different method, check documentation, inspect the error more carefully, and review relevant logs when available. Supabase issues are not always solved by retrying the same command, and the answer is not always in the logs, but logs are often worth checking before proceeding.\\n\\n**4. Exposing tables to the Data API:** Depending on the user's [Data API settings](https://supabase.com/dashboard/project//integrations/data_api/settings), newly created tables may not be automatically exposed via the Data (REST) API. If this is the case, `anon` and `authenticated` roles will need to be explicitly granted access.\\n\\n> Note that this is separate from RLS, which controls which _rows_ are visible once a table is accessible, not whether the table is accessible at all.\\n\\nWhen a user reports a SQL-created table is unexpectedly inaccessible, check their Data API settings and whether the roles have been granted access via explicit `GRANT` SQL. When granting public (`anon`/`authenticated`) access, always enable RLS too. See [Exposing a Table to the Data API](https://supabase.com/docs/guides/api/securing-your-api.md) for the full setup workflow.\\n\\n**5. RLS in exposed schemas.**\\nEnable RLS on every table in any exposed schema, which includes `public` by default. This is critical in Supabase because tables in exposed schemas can be reachable through the Data API when the `anon`/`authenticated` roles have access (see [Exposing a Table to the Data API](https://supabase.com/docs/guides/api/securing-your-api.md)). For private schemas, prefer RLS as defense in depth. After enabling RLS, create policies that match the actual access model rather than defaulting every table to the same `auth.uid()` pattern.\\n\\n**6. Security checklist.**\\nWhen working on any Supabase task that touches auth, RLS, views, storage, or user data, run through this checklist. These are Supabase-specific security traps that silently create vulnerabilities:\\n\\n- **Auth and session security**\\n - **Never use `user_metadata` claims in JWT-based authorization decisions.** In Supabase, `raw_user_meta_data` is user-editable and can appear in `auth.jwt()`, so it is unsafe for RLS policies or any other authorization logic. Store authorization data in `raw_app_meta_data` / `app_metadata` instead.\\n - **Deleting a user does not invalidate existing access tokens.** Sign out or revoke sessions first, keep JWT expiry short for sensitive apps, and for strict guarantees validate `session_id` against `auth.sessions` on sensitive operations.\\n - **If you use `app_metadata` or `auth.jwt()` for authorization, remember JWT claims are not always fresh until the user's token is refreshed.**\\n\\n- **API key and client exposure**\\n - **Never expose the `service_role` or secret key in public clients.** Prefer publishable keys for frontend code. Legacy `anon` keys are only for compatibility. In Next.js, any `NEXT_PUBLIC_` env var is sent to the browser.\\n\\n- **RLS, views, and privileged database code**\\n - **Views bypass RLS by default.** In Postgres 15 and above, use `CREATE VIEW ... WITH (security_invoker = true)`. In older versions of Postgres, protect your views by revoking access from the `anon` and `authenticated` roles, or by putting them in an unexposed schema.\\n - **UPDATE requires a SELECT policy.** In Postgres RLS, an UPDATE needs to first SELECT the row. Without a SELECT policy, updates silently return 0 rows — no error, just no change.\\n - **`auth.role()` is deprecated — use the `TO` clause instead.** Supabase has deprecated `auth.role()` in favour of specifying the target role directly on the policy with `TO authenticated` or `TO anon`. Beyond deprecation, `auth.role() = 'authenticated'` breaks silently when anonymous sign-ins are enabled, because anonymous users carry the `authenticated` Postgres role and pass the check regardless of whether the user is genuinely signed in.\\n ```sql\\n -- Deprecated (do not use)\\n create policy \\\"example\\\" on table_name for select\\n using ( auth.role() = 'authenticated' );\\n ```\\n - **`TO authenticated` alone is authentication without authorization (BOLA / IDOR).** Using `TO authenticated` only checks the role — it does not restrict which rows a user can access. The correct pattern combines `TO authenticated` with an ownership predicate in `USING`:\\n ```sql\\n create policy \\\"example\\\" on table_name for select\\n to authenticated\\n using ( (select auth.uid()) = user_id );\\n ```\\n - **UPDATE policies require both `USING` and `WITH CHECK`.** Without `WITH CHECK`, a user can reassign a row's `user_id` to another user:\\n ```sql\\n create policy \\\"example\\\" on table_name for update\\n to authenticated\\n using ( (select auth.uid()) = user_id )\\n with check ( (select auth.uid()) = user_id );\\n ```\\n - **`SECURITY DEFINER` functions bypass RLS.** A `SECURITY DEFINER` function runs with its creator's privileges — typically a role with `bypassrls` (e.g., `postgres`). Never add `SECURITY DEFINER` to resolve a permission error; it silently removes access control without fixing the underlying cause. Prefer `SECURITY INVOKER`.\\n - **`SECURITY DEFINER` functions in `public` are callable by all roles.** Postgres grants `EXECUTE` to `PUBLIC` by default for every new function, so any `SECURITY DEFINER` function in `public` is a public API endpoint callable by `anon` and `authenticated` (which inherit from `PUBLIC`) without any additional grant. When `SECURITY DEFINER` is genuinely needed (e.g., bypassing RLS on an internal lookup table), keep the function in a non-exposed schema, always include an `auth.uid()` check in the function body, and run `supabase db advisors` after making changes.\\n\\n- **Storage access control**\\n - **Storage upsert requires INSERT + SELECT + UPDATE.** Granting only INSERT allows new uploads but file replacement (upsert) silently fails. You need all three.\\n\\n- **Dependency and supply-chain security**\\n - **Always pin package versions and commit lockfiles** when installing Supabase packages (`supabase-js`, `@supabase/ssr`, `supabase-py`, etc.). See the [npm security guide](https://supabase.com/docs/guides/security/npm-security.md) for the full checklist.\\n\\nFor any security concern not covered above, fetch the Supabase product security index: `https://supabase.com/docs/guides/security/product-security.md`\\n\\n## Supabase CLI\\n\\nAlways discover commands via `--help` — never guess. The CLI structure changes between versions.\\n\\n```bash\\nsupabase --help # All top-level commands\\nsupabase --help # Subcommands (e.g., supabase db --help)\\nsupabase --help # Flags for a specific command\\n```\\n\\n**Supabase CLI Known gotchas:**\\n\\n- `supabase db query` requires **CLI v2.79.0+** → use MCP `execute_sql` or `psql` as fallback\\n- `supabase db advisors` requires **CLI v2.81.3+** → use MCP `get_advisors` as fallback\\n- In imperative migration projects, create new hand-authored migration files with `supabase migration new ` first. Never invent a migration filename or rely on memory for the expected format. Declarative schema projects generate migrations from `supabase/schemas/`; see \\\"Making and Committing Schema Changes\\\" below.\\n\\n**Version check and upgrade:** Run `supabase --version` to check. For CLI changelogs and version-specific features, consult the [CLI documentation](https://supabase.com/docs/reference/cli/introduction) or [GitHub releases](https://github.com/supabase/cli/releases).\\n\\n## Supabase MCP Server\\n\\nFor setup instructions, server URL, and configuration, see the [MCP setup guide](https://supabase.com/docs/guides/getting-started/mcp).\\n\\n**Troubleshooting connection issues** — follow these steps in order:\\n\\n1. **Check if the server is reachable:**\\n `curl -so /dev/null -w \\\"%{http_code}\\\" https://mcp.supabase.com/mcp`\\n A `401` is expected (no token) and means the server is up. Timeout or \\\"connection refused\\\" means it may be down.\\n\\n2. **Check `.mcp.json` configuration:**\\n Verify the project root has a valid `.mcp.json` with the correct server URL. If missing, create one pointing to `https://mcp.supabase.com/mcp`.\\n\\n3. **Authenticate the MCP server:**\\n If the server is reachable and `.mcp.json` is correct but tools aren't visible, the user needs to authenticate. The Supabase MCP server uses OAuth 2.1 — tell the user to trigger the auth flow in their agent, complete it in the browser, and reload the session.\\n\\n## Supabase Documentation\\n\\nBefore implementing any Supabase feature, find the relevant documentation. Use these methods in priority order:\\n\\n1. **MCP `search_docs` tool** (preferred — returns relevant snippets directly)\\n2. **Fetch docs pages as markdown** — any docs page can be fetched by appending `.md` to the URL path.\\n3. **Web search** for Supabase-specific topics when you don't know which page to look at.\\n\\n## Making and Committing Schema Changes\\n\\nFirst decide which schema workflow the project uses.\\n\\n### Option A: Declarative schemas\\n\\nUse this when `supabase/schemas/` exists or `config.toml` sets `schema_paths`. Edit the desired schema state in those files, then generate and review the migration. Do not start by hand-writing a migration. See the [Declarative database schemas guide](https://supabase.com/docs/guides/local-development/declarative-database-schemas).\\n\\n### Option B: Imperative migrations\\n\\nUse this when the project does not use declarative schemas.\\n\\n**To make schema changes, use `execute_sql` (MCP) or `supabase db query` (CLI).** These run SQL directly on the database without creating migration history entries, so you can iterate freely and generate a clean migration when ready.\\n\\nDo NOT use `apply_migration` to change a local database schema — it writes a migration history entry on every call, which means you can't iterate, and `supabase db diff` / `supabase db pull` will produce empty or conflicting diffs. If you use it, you'll be stuck with whatever SQL you passed on the first try.\\n\\n**When ready to commit** your changes to a migration file:\\n\\n1. **Run advisors** → `supabase db advisors` (CLI v2.81.3+) or MCP `get_advisors`. Fix any issues.\\n2. **Review the Security Checklist above** if your changes involve views, functions, triggers, or storage.\\n3. **Generate the migration** → `supabase db pull --local --yes`\\n4. **Verify** → `supabase migration list --local`\\n\\n## Reference Guides\\n\\n- **Skill Feedback** → [references/skill-feedback.md](references/skill-feedback.md)\\n **MUST read when** the user reports that this skill gave incorrect guidance or is missing information.\\n\"\n}", + "input": "{\n \"path\": \".claude/skills/supabase/SKILL.md\"\n}", + "output": "{\n \"contents\": \"---\\nname: supabase\\ndescription: \\\"Use when doing ANY task involving Supabase. Triggers: Supabase products (Database, Auth, Edge Functions, Realtime, Storage, Vectors, Cron, Queues); client libraries and SSR integrations (supabase-js, @supabase/ssr) in Next.js, React, SvelteKit, Astro, Remix; auth issues (login, logout, sessions, JWT, cookies, getSession, getUser, getClaims, RLS); Supabase CLI or MCP server; schema changes, migrations, declarative schemas, security audits, Postgres extensions (pg_graphql, pg_cron, pg_vector).\\\"\\nmetadata:\\n author: supabase\\n version: \\\"0.1.2\\\"\\n---\\n\\n# Supabase\\n\\n## Core Principles\\n\\n**1. Supabase changes frequently — verify against changelog and current docs before implementing.**\\nDo not rely on training data for Supabase features. Function signatures, config.toml settings, and API conventions change between versions.\\n\\nFirst, fetch `https://supabase.com/changelog.md` (a lightweight summary index — not a heavy pull), scan for `breaking-change` tags relevant to your task, and follow the linked page for any that apply. Then look up the relevant topic using the documentation access methods below.\\n\\n**2. Verify your work.**\\nAfter implementing any fix, run a test query to confirm the change works. A fix without verification is incomplete.\\n\\n**3. Recover from errors, don't loop.**\\nIf an approach fails after 2-3 attempts, stop and reconsider. Try a different method, check documentation, inspect the error more carefully, and review relevant logs when available. Supabase issues are not always solved by retrying the same command, and the answer is not always in the logs, but logs are often worth checking before proceeding.\\n\\n**4. Exposing tables to the Data API:** Depending on the user's [Data API settings](https://supabase.com/dashboard/project//integrations/data_api/settings), newly created tables may not be automatically exposed via the Data (REST) API. If this is the case, `anon` and `authenticated` roles will need to be explicitly granted access.\\n\\n> Note that this is separate from RLS, which controls which _rows_ are visible once a table is accessible, not whether the table is accessible at all.\\n\\nWhen a user reports a SQL-created table is unexpectedly inaccessible, check their Data API settings and whether the roles have been granted access via explicit `GRANT` SQL. When granting public (`anon`/`authenticated`) access, always enable RLS too. See [Exposing a Table to the Data API](https://supabase.com/docs/guides/api/securing-your-api.md) for the full setup workflow.\\n\\n**5. RLS in exposed schemas.**\\nEnable RLS on every table in any exposed schema, which includes `public` by default. This is critical in Supabase because tables in exposed schemas can be reachable through the Data API when the `anon`/`authenticated` roles have access (see [Exposing a Table to the Data API](https://supabase.com/docs/guides/api/securing-your-api.md)). For private schemas, prefer RLS as defense in depth. After enabling RLS, create policies that match the actual access model rather than defaulting every table to the same `auth.uid()` pattern.\\n\\n**6. Security checklist.**\\nWhen working on any Supabase task that touches auth, RLS, views, storage, or user data, run through this checklist. These are Supabase-specific security traps that silently create vulnerabilities:\\n\\n- **Auth and session security**\\n - **Never use `user_metadata` claims in JWT-based authorization decisions.** In Supabase, `raw_user_meta_data` is user-editable and can appear in `auth.jwt()`, so it is unsafe for RLS policies or any other authorization logic. Store authorization data in `raw_app_meta_data` / `app_metadata` instead.\\n - **Deleting a user does not invalidate existing access tokens.** Sign out or revoke sessions first, keep JWT expiry short for sensitive apps, and for strict guarantees validate `session_id` against `auth.sessions` on sensitive operations.\\n - **If you use `app_metadata` or `auth.jwt()` for authorization, remember JWT claims are not always fresh until the user's token is refreshed.**\\n\\n- **API key and client exposure**\\n - **Never expose the `service_role` or secret key in public clients.** Prefer publishable keys for frontend code. Legacy `anon` keys are only for compatibility. In Next.js, any `NEXT_PUBLIC_` env var is sent to the browser.\\n\\n- **RLS, views, and privileged database code**\\n - **Views bypass RLS by default.** In Postgres 15 and above, use `CREATE VIEW ... WITH (security_invoker = true)`. In older versions of Postgres, protect your views by revoking access from the `anon` and `authenticated` roles, or by putting them in an unexposed schema.\\n - **UPDATE requires a SELECT policy.** In Postgres RLS, an UPDATE needs to first SELECT the row. Without a SELECT policy, updates silently return 0 rows — no error, just no change.\\n - **`auth.role()` is deprecated — use the `TO` clause instead.** Supabase has deprecated `auth.role()` in favour of specifying the target role directly on the policy with `TO authenticated` or `TO anon`. Beyond deprecation, `auth.role() = 'authenticated'` breaks silently when anonymous sign-ins are enabled, because anonymous users carry the `authenticated` Postgres role and pass the check regardless of whether the user is genuinely signed in.\\n ```sql\\n -- Deprecated (do not use)\\n create policy \\\"example\\\" on table_name for select\\n using ( auth.role() = 'authenticated' );\\n ```\\n - **`TO authenticated` alone is authentication without authorization (BOLA / IDOR).** Using `TO authenticated` only checks the role — it does not restrict which rows a user can access. The correct pattern combines `TO authenticated` with an ownership predicate in `USING`:\\n ```sql\\n create policy \\\"example\\\" on table_name for select\\n to authenticated\\n using ( (select auth.uid()) = user_id );\\n ```\\n - **UPDATE policies require both `USING` and `WITH CHECK`.** Without `WITH CHECK`, a user can reassign a row's `user_id` to another user:\\n ```sql\\n create policy \\\"example\\\" on table_name for update\\n to authenticated\\n using ( (select auth.uid()) = user_id )\\n with check ( (select auth.uid()) = user_id );\\n ```\\n - **`SECURITY DEFINER` functions bypass RLS.** A `SECURITY DEFINER` function runs with its creator's privileges — typically a role with `bypassrls` (e.g., `postgres`). Never add `SECURITY DEFINER` to resolve a permission error; it silently removes access control without fixing the underlying cause. Prefer `SECURITY INVOKER`.\\n - **`SECURITY DEFINER` functions in `public` are callable by all roles.** Postgres grants `EXECUTE` to `PUBLIC` by default for every new function, so any `SECURITY DEFINER` function in `public` is a public API endpoint callable by `anon` and `authenticated` (which inherit from `PUBLIC`) without any additional grant. When `SECURITY DEFINER` is genuinely needed (e.g., bypassing RLS on an internal lookup table), keep the function in a non-exposed schema, always include an `auth.uid()` check in the function body, and run `supabase db advisors` after making changes.\\n\\n- **Storage access control**\\n - **Storage upsert requires INSERT + SELECT + UPDATE.** Granting only INSERT allows new uploads but file replacement (upsert) silently fails. You need all three.\\n\\n- **Dependency and supply-chain security**\\n - **Always pin package versions and commit lockfiles** when installing Supabase packages (`supabase-js`, `@supabase/ssr`, `supabase-py`, etc.). See the [npm security guide](https://supabase.com/docs/guides/security/npm-security.md) for the full checklist.\\n\\nFor any security concern not covered above, fetch the Supabase product security index: `https://supabase.com/docs/guides/security/product-security.md`\\n\\n## Supabase CLI\\n\\nAlways discover commands via `--help` — never guess. The CLI structure changes between versions.\\n\\n```bash\\nsupabase --help # All top-level commands\\nsupabase --help # Subcommands (e.g., supabase db --help)\\nsupabase --help # Flags for a specific command\\n```\\n\\n**Supabase CLI Known gotchas:**\\n\\n- `supabase db query` requires **CLI v2.79.0+** → use MCP `execute_sql` or `psql` as fallback\\n- `supabase db advisors` requires **CLI v2.81.3+** → use MCP `get_advisors` as fallback\\n- In imperative migration projects, create new hand-authored migration files with `supabase migration new ` first. Never invent a migration filename or rely on memory for the expected format. Declarative schema projects generate migrations from `supabase/schemas/`; see \\\"Making and Committing Schema Changes\\\" below.\\n\\n**Version check and upgrade:** Run `supabase --version` to check. For CLI changelogs and version-specific features, consult the [CLI documentation](https://supabase.com/docs/reference/cli/introduction) or [GitHub releases](https://github.com/supabase/cli/releases).\\n\\n## Supabase MCP Server\\n\\nFor setup instructions, server URL, and configuration, see the [MCP setup guide](https://supabase.com/docs/guides/getting-started/mcp).\\n\\n**Troubleshooting connection issues** — follow these steps in order:\\n\\n1. **Check if the server is reachable:**\\n `curl -so /dev/null -w \\\"%{http_code}\\\" https://mcp.supabase.com/mcp`\\n A `401` is expected (no token) and means the server is up. Timeout or \\\"connection refused\\\" means it may be down.\\n\\n2. **Check `.mcp.json` configuration:**\\n Verify the project root has a valid `.mcp.json` with the correct server URL. If missing, create one pointing to `https://mcp.supabase.com/mcp`.\\n\\n3. **Authenticate the MCP server:**\\n If the server is reachable and `.mcp.json` is correct but tools aren't visible, the user needs to authenticate. The Supabase MCP server uses OAuth 2.1 — tell the user to trigger the auth flow in their agent, complete it in the browser, and reload the session.\\n\\n## Supabase Documentation\\n\\nBefore implementing any Supabase feature, find the relevant documentation. Use these methods in priority order:\\n\\n1. **MCP `search_docs` tool** (preferred — returns relevant snippets directly)\\n2. **Fetch docs pages as markdown** — any docs page can be fetched by appending `.md` to the URL path.\\n3. **Web search** for Supabase-specific topics when you don't know which page to look at.\\n\\n## Making and Committing Schema Changes\\n\\nFirst decide which schema workflow the project uses.\\n\\n### Option A: Declarative schemas\\n\\nUse this when `supabase/schemas/` exists or `config.toml` sets `schema_paths`. Edit the desired schema state in those files, then generate and review the migration. Do not start by hand-writing a migration. See the [Declarative database schemas guide](https://supabase.com/docs/guides/local-development/declarative-database-schemas).\\n\\n### Option B: Imperative migrations\\n\\nUse this when the project does not use declarative schemas.\\n\\n**To make schema changes, use `execute_sql` (MCP) or `supabase db query` (CLI).** These run SQL directly on the database without creating migration history entries, so you can iterate freely and generate a clean migration when ready.\\n\\nDo NOT use `apply_migration` to change a local database schema — it writes a migration history entry on every call, which means you can't iterate, and `supabase db diff` / `supabase db pull` will produce empty or conflicting diffs. If you use it, you'll be stuck with whatever SQL you passed on the first try.\\n\\n**When ready to commit** your changes to a migration file:\\n\\n1. **Run advisors** → `supabase db advisors` (CLI v2.81.3+) or MCP `get_advisors`. Fix any issues.\\n2. **Review the Security Checklist above** if your changes involve views, functions, triggers, or storage.\\n3. **Generate the migration** → `supabase db pull --local --yes`\\n4. **Verify** → `supabase migration list --local`\\n\\n## Reference Guides\\n\\n- **Skill Feedback** → [references/skill-feedback.md](references/skill-feedback.md)\\n **MUST read when** the user reports that this skill gave incorrect guidance or is missing information.\\n\"\n}" + }, + { + "id": "build-cli-002-declarative-schema-tool-4", + "title": "Tool: files_read", + "startTime": "2026-08-01T15:57:58.561Z", + "endTime": "2026-08-01T15:57:58.562Z", + "duration": 1, + "type": "tool_execution", + "status": "success", + "raw": "{\n \"contents\": \"---\\nname: supabase-postgres-best-practices\\ndescription: \\\"Postgres best practices maintained by Supabase, for Postgres running anywhere. Load this skill BEFORE writing or changing anything that lives in a Postgres database: creating or altering tables and columns (including choosing column types), schema design, migrations and declarative schema files, RLS policies and the tests that verify them, indexes, triggers, database functions, queues and scheduled jobs (pg_cron, pgmq), vector/semantic search (pgvector), and restoring dumps (pg_restore) or importing data. Also load it when diagnosing slow queries, high CPU, timeouts, EXPLAIN plans, connection exhaustion, locking, bloat, or rows visible to the wrong user or tenant. This is not just a performance guide — schema, migration, security, and SQL authoring tasks need these rules too, even for a one-column change or a single query.\\\"\\nlicense: MIT\\nmetadata:\\n author: supabase\\n version: \\\"1.1.1\\\"\\n organization: Supabase\\n date: January 2026\\n abstract: Comprehensive Postgres performance optimization guide for developers using Supabase and Postgres. Contains performance rules across 8 categories, prioritized by impact from critical (query performance, connection management) to incremental (advanced features). Each rule includes detailed explanations, incorrect vs. correct SQL examples, query plan analysis, and specific performance metrics to guide automated optimization and code generation.\\n---\\n\\n# Supabase Postgres Best Practices\\n\\nComprehensive performance optimization guide for Postgres, maintained by Supabase. Contains rules across 8 categories, prioritized by impact to guide automated query optimization and schema design.\\n\\n## When to Apply\\n\\nReference these guidelines when:\\n- Writing SQL queries or designing schemas\\n- Implementing indexes or query optimization\\n- Reviewing database performance issues\\n- Configuring connection pooling or scaling\\n- Optimizing for Postgres-specific features\\n- Working with Row-Level Security (RLS)\\n\\n## Rule Categories by Priority\\n\\n| Priority | Category | Impact | Prefix |\\n|----------|----------|--------|--------|\\n| 1 | Query Performance | CRITICAL | `query-` |\\n| 2 | Connection Management | CRITICAL | `conn-` |\\n| 3 | Security & RLS | CRITICAL | `security-` |\\n| 4 | Schema Design | HIGH | `schema-` |\\n| 5 | Concurrency & Locking | MEDIUM-HIGH | `lock-` |\\n| 6 | Data Access Patterns | MEDIUM | `data-` |\\n| 7 | Monitoring & Diagnostics | LOW-MEDIUM | `monitor-` |\\n| 8 | Advanced Features | LOW | `advanced-` |\\n\\n## How to Use\\n\\nRead individual rule files for detailed explanations and SQL examples:\\n\\n```\\nreferences/query-missing-indexes.md\\nreferences/query-partial-indexes.md\\nreferences/_sections.md\\n```\\n\\nEach rule file contains:\\n- Brief explanation of why it matters\\n- Incorrect SQL example with explanation\\n- Correct SQL example with explanation\\n- Optional EXPLAIN output or metrics\\n- Additional context and references\\n- Supabase-specific notes (when applicable)\\n\\n## References\\n\\n- https://www.postgresql.org/docs/current/\\n- https://supabase.com/docs\\n- https://wiki.postgresql.org/wiki/Performance_Optimization\\n- https://supabase.com/docs/guides/database/overview\\n- https://supabase.com/docs/guides/auth/row-level-security\\n\"\n}", + "input": "{\n \"path\": \".claude/skills/supabase-postgres-best-practices/SKILL.md\"\n}", + "output": "{\n \"contents\": \"---\\nname: supabase-postgres-best-practices\\ndescription: \\\"Postgres best practices maintained by Supabase, for Postgres running anywhere. Load this skill BEFORE writing or changing anything that lives in a Postgres database: creating or altering tables and columns (including choosing column types), schema design, migrations and declarative schema files, RLS policies and the tests that verify them, indexes, triggers, database functions, queues and scheduled jobs (pg_cron, pgmq), vector/semantic search (pgvector), and restoring dumps (pg_restore) or importing data. Also load it when diagnosing slow queries, high CPU, timeouts, EXPLAIN plans, connection exhaustion, locking, bloat, or rows visible to the wrong user or tenant. This is not just a performance guide — schema, migration, security, and SQL authoring tasks need these rules too, even for a one-column change or a single query.\\\"\\nlicense: MIT\\nmetadata:\\n author: supabase\\n version: \\\"1.1.1\\\"\\n organization: Supabase\\n date: January 2026\\n abstract: Comprehensive Postgres performance optimization guide for developers using Supabase and Postgres. Contains performance rules across 8 categories, prioritized by impact from critical (query performance, connection management) to incremental (advanced features). Each rule includes detailed explanations, incorrect vs. correct SQL examples, query plan analysis, and specific performance metrics to guide automated optimization and code generation.\\n---\\n\\n# Supabase Postgres Best Practices\\n\\nComprehensive performance optimization guide for Postgres, maintained by Supabase. Contains rules across 8 categories, prioritized by impact to guide automated query optimization and schema design.\\n\\n## When to Apply\\n\\nReference these guidelines when:\\n- Writing SQL queries or designing schemas\\n- Implementing indexes or query optimization\\n- Reviewing database performance issues\\n- Configuring connection pooling or scaling\\n- Optimizing for Postgres-specific features\\n- Working with Row-Level Security (RLS)\\n\\n## Rule Categories by Priority\\n\\n| Priority | Category | Impact | Prefix |\\n|----------|----------|--------|--------|\\n| 1 | Query Performance | CRITICAL | `query-` |\\n| 2 | Connection Management | CRITICAL | `conn-` |\\n| 3 | Security & RLS | CRITICAL | `security-` |\\n| 4 | Schema Design | HIGH | `schema-` |\\n| 5 | Concurrency & Locking | MEDIUM-HIGH | `lock-` |\\n| 6 | Data Access Patterns | MEDIUM | `data-` |\\n| 7 | Monitoring & Diagnostics | LOW-MEDIUM | `monitor-` |\\n| 8 | Advanced Features | LOW | `advanced-` |\\n\\n## How to Use\\n\\nRead individual rule files for detailed explanations and SQL examples:\\n\\n```\\nreferences/query-missing-indexes.md\\nreferences/query-partial-indexes.md\\nreferences/_sections.md\\n```\\n\\nEach rule file contains:\\n- Brief explanation of why it matters\\n- Incorrect SQL example with explanation\\n- Correct SQL example with explanation\\n- Optional EXPLAIN output or metrics\\n- Additional context and references\\n- Supabase-specific notes (when applicable)\\n\\n## References\\n\\n- https://www.postgresql.org/docs/current/\\n- https://supabase.com/docs\\n- https://wiki.postgresql.org/wiki/Performance_Optimization\\n- https://supabase.com/docs/guides/database/overview\\n- https://supabase.com/docs/guides/auth/row-level-security\\n\"\n}" + }, + { + "id": "build-cli-002-declarative-schema-tool-5", + "title": "Tool: files_list", + "startTime": "2026-08-01T15:57:58.562Z", + "endTime": "2026-08-01T15:58:00.874Z", + "duration": 2312, + "type": "tool_execution", + "status": "error", + "raw": "path must be relative to the workspace", + "input": "{\n \"path\": \"\"\n}", + "output": "path must be relative to the workspace" + }, + { + "id": "build-cli-002-declarative-schema-tool-6", + "title": "Tool: files_list", + "startTime": "2026-08-01T15:58:00.874Z", + "endTime": "2026-08-01T15:58:00.933Z", + "duration": 59, + "type": "tool_execution", + "status": "success", + "raw": "{\n \"entries\": [\n {\n \"path\": \".adal\",\n \"type\": \"dir\"\n },\n {\n \"path\": \".agents\",\n \"type\": \"dir\"\n },\n {\n \"path\": \".aider-desk\",\n \"type\": \"dir\"\n },\n {\n \"path\": \".augment\",\n \"type\": \"dir\"\n },\n {\n \"path\": \".autohand\",\n \"type\": \"dir\"\n },\n {\n \"path\": \".bob\",\n \"type\": \"dir\"\n },\n {\n \"path\": \".claude\",\n \"type\": \"dir\"\n },\n {\n \"path\": \".codeartsdoer\",\n \"type\": \"dir\"\n },\n {\n \"path\": \".codebuddy\",\n \"type\": \"dir\"\n },\n {\n \"path\": \".codemaker\",\n \"type\": \"dir\"\n },\n {\n \"path\": \".codestudio\",\n \"type\": \"dir\"\n },\n {\n \"path\": \".commandcode\",\n \"type\": \"dir\"\n },\n {\n \"path\": \".continue\",\n \"type\": \"dir\"\n },\n {\n \"path\": \".cortex\",\n \"type\": \"dir\"\n },\n {\n \"path\": \".crush\",\n \"type\": \"dir\"\n },\n {\n \"path\": \".devin\",\n \"type\": \"dir\"\n },\n {\n \"path\": \".factory\",\n \"type\": \"dir\"\n },\n {\n \"path\": \".forge\",\n \"type\": \"dir\"\n },\n {\n \"path\": \".goose\",\n \"type\": \"dir\"\n },\n {\n \"path\": \".hermes\",\n \"type\": \"dir\"\n },\n {\n \"path\": \".iflow\",\n \"type\": \"dir\"\n },\n {\n \"path\": \".inferencesh\",\n \"type\": \"dir\"\n },\n {\n \"path\": \".jazz\",\n \"type\": \"dir\"\n },\n {\n \"path\": \".junie\",\n \"type\": \"dir\"\n },\n {\n \"path\": \".kilocode\",\n \"type\": \"dir\"\n },\n {\n \"path\": \".kiro\",\n \"type\": \"dir\"\n },\n {\n \"path\": \".kode\",\n \"type\": \"dir\"\n },\n {\n \"path\": \".lingma\",\n \"type\": \"dir\"\n },\n {\n \"path\": \".mcpjam\",\n \"type\": \"dir\"\n },\n {\n \"path\": \".moxby\",\n \"type\": \"dir\"\n },\n {\n \"path\": \".mux\",\n \"type\": \"dir\"\n },\n {\n \"path\": \".neovate\",\n \"type\": \"dir\"\n },\n {\n \"path\": \".ona\",\n \"type\": \"dir\"\n },\n {\n \"path\": \".openhands\",\n \"type\": \"dir\"\n },\n {\n \"path\": \".pi\",\n \"type\": \"dir\"\n },\n {\n \"path\": \".pochi\",\n \"type\": \"dir\"\n },\n {\n \"path\": \".qoder\",\n \"type\": \"dir\"\n },\n {\n \"path\": \".qwen\",\n \"type\": \"dir\"\n },\n {\n \"path\": \".reasonix\",\n \"type\": \"dir\"\n },\n {\n \"path\": \".roo\",\n \"type\": \"dir\"\n },\n {\n \"path\": \".rovodev\",\n \"type\": \"dir\"\n },\n {\n \"path\": \".tabnine\",\n \"type\": \"dir\"\n },\n {\n \"path\": \".terramind\",\n \"type\": \"dir\"\n },\n {\n \"path\": \".tinycloud\",\n \"type\": \"dir\"\n },\n {\n \"path\": \".trae\",\n \"type\": \"dir\"\n },\n {\n \"path\": \".vibe\",\n \"type\": \"dir\"\n },\n {\n \"path\": \".windsurf\",\n \"type\": \"dir\"\n },\n {\n \"path\": \".zencoder\",\n \"type\": \"dir\"\n },\n {\n \"path\": \"data\",\n \"type\": \"dir\"\n },\n {\n \"path\": \"skills\",\n \"type\": \"dir\"\n },\n {\n \"path\": \"skills-lock.json\",\n \"type\": \"file\"\n },\n {\n \"path\": \"supabase\",\n \"type\": \"dir\"\n }\n ]\n}", + "input": "{\n \"path\": \".\"\n}", + "output": "{\n \"entries\": [\n {\n \"path\": \".adal\",\n \"type\": \"dir\"\n },\n {\n \"path\": \".agents\",\n \"type\": \"dir\"\n },\n {\n \"path\": \".aider-desk\",\n \"type\": \"dir\"\n },\n {\n \"path\": \".augment\",\n \"type\": \"dir\"\n },\n {\n \"path\": \".autohand\",\n \"type\": \"dir\"\n },\n {\n \"path\": \".bob\",\n \"type\": \"dir\"\n },\n {\n \"path\": \".claude\",\n \"type\": \"dir\"\n },\n {\n \"path\": \".codeartsdoer\",\n \"type\": \"dir\"\n },\n {\n \"path\": \".codebuddy\",\n \"type\": \"dir\"\n },\n {\n \"path\": \".codemaker\",\n \"type\": \"dir\"\n },\n {\n \"path\": \".codestudio\",\n \"type\": \"dir\"\n },\n {\n \"path\": \".commandcode\",\n \"type\": \"dir\"\n },\n {\n \"path\": \".continue\",\n \"type\": \"dir\"\n },\n {\n \"path\": \".cortex\",\n \"type\": \"dir\"\n },\n {\n \"path\": \".crush\",\n \"type\": \"dir\"\n },\n {\n \"path\": \".devin\",\n \"type\": \"dir\"\n },\n {\n \"path\": \".factory\",\n \"type\": \"dir\"\n },\n {\n \"path\": \".forge\",\n \"type\": \"dir\"\n },\n {\n \"path\": \".goose\",\n \"type\": \"dir\"\n },\n {\n \"path\": \".hermes\",\n \"type\": \"dir\"\n },\n {\n \"path\": \".iflow\",\n \"type\": \"dir\"\n },\n {\n \"path\": \".inferencesh\",\n \"type\": \"dir\"\n },\n {\n \"path\": \".jazz\",\n \"type\": \"dir\"\n },\n {\n \"path\": \".junie\",\n \"type\": \"dir\"\n },\n {\n \"path\": \".kilocode\",\n \"type\": \"dir\"\n },\n {\n \"path\": \".kiro\",\n \"type\": \"dir\"\n },\n {\n \"path\": \".kode\",\n \"type\": \"dir\"\n },\n {\n \"path\": \".lingma\",\n \"type\": \"dir\"\n },\n {\n \"path\": \".mcpjam\",\n \"type\": \"dir\"\n },\n {\n \"path\": \".moxby\",\n \"type\": \"dir\"\n },\n {\n \"path\": \".mux\",\n \"type\": \"dir\"\n },\n {\n \"path\": \".neovate\",\n \"type\": \"dir\"\n },\n {\n \"path\": \".ona\",\n \"type\": \"dir\"\n },\n {\n \"path\": \".openhands\",\n \"type\": \"dir\"\n },\n {\n \"path\": \".pi\",\n \"type\": \"dir\"\n },\n {\n \"path\": \".pochi\",\n \"type\": \"dir\"\n },\n {\n \"path\": \".qoder\",\n \"type\": \"dir\"\n },\n {\n \"path\": \".qwen\",\n \"type\": \"dir\"\n },\n {\n \"path\": \".reasonix\",\n \"type\": \"dir\"\n },\n {\n \"path\": \".roo\",\n \"type\": \"dir\"\n },\n {\n \"path\": \".rovodev\",\n \"type\": \"dir\"\n },\n {\n \"path\": \".tabnine\",\n \"type\": \"dir\"\n },\n {\n \"path\": \".terramind\",\n \"type\": \"dir\"\n },\n {\n \"path\": \".tinycloud\",\n \"type\": \"dir\"\n },\n {\n \"path\": \".trae\",\n \"type\": \"dir\"\n },\n {\n \"path\": \".vibe\",\n \"type\": \"dir\"\n },\n {\n \"path\": \".windsurf\",\n \"type\": \"dir\"\n },\n {\n \"path\": \".zencoder\",\n \"type\": \"dir\"\n },\n {\n \"path\": \"data\",\n \"type\": \"dir\"\n },\n {\n \"path\": \"skills\",\n \"type\": \"dir\"\n },\n {\n \"path\": \"skills-lock.json\",\n \"type\": \"file\"\n },\n {\n \"path\": \"supabase\",\n \"type\": \"dir\"\n }\n ]\n}" + }, + { + "id": "build-cli-002-declarative-schema-tool-7", + "title": "Tool: bash", + "startTime": "2026-08-01T15:58:00.933Z", + "endTime": "2026-08-01T15:58:01.259Z", + "duration": 326, + "type": "tool_execution", + "status": "success", + "raw": "{\n \"exit_code\": 0,\n \"stdout\": \"2.67.1\\n\\n\\n╭───────────────────────────────────────────────────────────────╮\\n│ ⛁ Database │\\n├─────┬─────────────────────────────────────────────────────────┤\\n│ URL │ postgresql://postgres:REDACTED@127.0.0.1:54322/postgres │\\n╰─────┴─────────────────────────────────────────────────────────╯\\n\\n\\n\\n\",\n \"stderr\": \"A new version of Supabase CLI is available: v2.111.0 (currently installed v2.67.1)\\nWe recommend updating regularly for new features and bug fixes: https://supabase.com/docs/guides/cli/getting-started#updating-the-supabase-cli\\nStopped services: [supabase_kong_declarative-schema supabase_auth_declarative-schema supabase_inbucket_declarative-schema supabase_realtime_declarative-schema supabase_rest_declarative-schema supabase_storage_declarative-schema supabase_imgproxy_declarative-schema supabase_pg_meta_declarative-schema supabase_studio_declarative-schema supabase_edge_runtime_declarative-schema supabase_analytics_declarative-schema supabase_vector_declarative-schema supabase_pooler_declarative-schema]\\nsupabase local development setup is running.\\n\\nA new version of Supabase CLI is available: v2.111.0 (currently installed v2.67.1)\\nWe recommend updating regularly for new features and bug fixes: https://supabase.com/docs/guides/cli/getting-started#updating-the-supabase-cli\\n\"\n}", + "input": "{\n \"command\": \"supabase --version && supabase status\",\n \"timeout_sec\": 30\n}", + "output": "{\n \"exit_code\": 0,\n \"stdout\": \"2.67.1\\n\\n\\n╭───────────────────────────────────────────────────────────────╮\\n│ ⛁ Database │\\n├─────┬─────────────────────────────────────────────────────────┤\\n│ URL │ postgresql://postgres:REDACTED@127.0.0.1:54322/postgres │\\n╰─────┴─────────────────────────────────────────────────────────╯\\n\\n\\n\\n\",\n \"stderr\": \"A new version of Supabase CLI is available: v2.111.0 (currently installed v2.67.1)\\nWe recommend updating regularly for new features and bug fixes: https://supabase.com/docs/guides/cli/getting-started#updating-the-supabase-cli\\nStopped services: [supabase_kong_declarative-schema supabase_auth_declarative-schema supabase_inbucket_declarative-schema supabase_realtime_declarative-schema supabase_rest_declarative-schema supabase_storage_declarative-schema supabase_imgproxy_declarative-schema supabase_pg_meta_declarative-schema supabase_studio_declarative-schema supabase_edge_runtime_declarative-schema supabase_analytics_declarative-schema supabase_vector_declarative-schema supabase_pooler_declarative-schema]\\nsupabase local development setup is running.\\n\\nA new version of Supabase CLI is available: v2.111.0 (currently installed v2.67.1)\\nWe recommend updating regularly for new features and bug fixes: https://supabase.com/docs/guides/cli/getting-started#updating-the-supabase-cli\\n\"\n}" + }, + { + "id": "build-cli-002-declarative-schema-tool-8", + "title": "Tool: bash", + "startTime": "2026-08-01T15:58:01.259Z", + "endTime": "2026-08-01T15:58:01.338Z", + "duration": 79, + "type": "tool_execution", + "status": "success", + "raw": "{\n \"exit_code\": 0,\n \"stdout\": \"# Supabase Changelog\\n\\n## Fixed a panic in project metrics collection that could drop metrics\\n\\n2026-07-29 · Bug Fix · Observability, Platform · [supabase.com/changelog/admin-api-metrics-panic-fix](https://supabase.com/changelog/admin-api-metrics-panic-fix)\\n\\nA metrics endpoint could panic under concurrent requests, intermittently stopping metrics from being collected and pushed for a project.\\n\\n---\\n\\n## Migration of Supabase Management API `logs.all` analytics endpoint to `logs` endpoint\\n\\n2026-07-23 · Breaking Change · Platform · [supabase.com/changelog/48235-migration-of-supabase-management-api-logs-all-analytics-endpoint-to-logs-endpoint](https://supabase.com/changelog/48235-migration-of-supabase-management-api-logs-all-analytics-endpoint-to-logs-endpoint)\\n\\nThe Supabase Management API `logs.all` endpoint is removed on 2026-09-23. Scripts calling it must migrate to the new ClickHouse-backed `logs` endpoint, which accepts ClickHouse SQL only.\\n\\n---\\n\\n## Extension version pinning is deprecated in favor of default versions\\n\\n2026-07-22 · Breaking Change · Database, Security · [supabase.com/changelog/extension-version-pinning-ignored](https://supabase.com/changelog/extension-version-pinning-ignored)\\n\\nSpecifying an explicit version in CREATE or ALTER EXTENSION is deprecated. From 2026-08-05 the version clause is ignored and the extension's default version is installed, with a warning.\\n\\n---\\n\\n## [Public Alpha] Supabase Pipelines\\n\\n2026-07-21 · New Feature · ETL · [supabase.com/changelog/pipelines](https://supabase.com/changelog/pipelines)\\n\\nStream Postgres changes to BigQuery in near real time with Supabase Pipelines, a managed CDC service configured right in the Dashboard. Now in public alpha on all paid plans.\\n\\n---\\n\\n## Self-hosted Supabase: Envoy becomes the default API gateway (breaking change)\\n\\n2026-07-17 · Breaking Change · Platform · [supabase.com/changelog/48048-self-hosted-supabase-envoy-becomes-the-default-api-gateway-b](https://supabase.com/changelog/48048-self-hosted-supabase-envoy-becomes-the-default-api-gateway-b)\\n\\nSelf-hosted Supabase makes Envoy the default API gateway, replacing Kong, the week of 2026-08-09. Opt back into Kong if you rely on its HTTPS listener, a custom `kong.yml`, or the kong service name.\\n\\n---\\n\\n## Realtime schema is now fully locked down against modifications\\n\\n2026-07-14 · Breaking Change · Realtime · [supabase.com/changelog/realtime-schema-locked-down-against-modification](https://supabase.com/changelog/realtime-schema-locked-down-against-modification)\\n\\nSupabase now blocks all changes to the `realtime` schema: creating, altering, or dropping objects fails with a permission error. RLS policies on `realtime.messages` still work.\\n\\n---\\n\\n## Deprecation notice: `@supabase/supabase-js` will require TypeScript 5.0+\\n\\n2026-07-10 · Deprecation · supabase-js · [supabase.com/changelog/47812-deprecation-notice-supabase-supabase-js-will-require-typescript-5-0](https://supabase.com/changelog/47812-deprecation-notice-supabase-supabase-js-will-require-typescript-5-0)\\n\\n`@supabase/supabase-js` will require TypeScript 5.0 starting January 31, 2027, dropping support for TypeScript 4.7 to 4.9. Upgrade to 5.0 or later before then.\\n\\n---\\n\\n## Developer Update - July 2026\\n\\n2026-07-09 · Improvement · Data APIs, Database, Realtime · [supabase.com/changelog/47796-developer-update-july-2026](https://supabase.com/changelog/47796-developer-update-july-2026)\\n\\nSupabase Developer Update for July 2026: Realtime Broadcast now supports binary payloads, Wrappers v0.6.2 adds a MongoDB foreign data wrapper, and OpenCode integrates with Supabase.\\n\\n---\\n\\n## log_connections is to be turned off by default for new projects and existing Free/Pro projects\\n\\n2026-06-22 · Improvement · Platform · [supabase.com/changelog/47197-log-connections-is-to-be-turned-off-by-default-for-new-projects-and-existing-free-pro-projects](https://supabase.com/changelog/47197-log-connections-is-to-be-turned-off-by-default-for-new-projects-and-existing-free-pro-projects)\\n\\nPostgres `log_connections` defaults to off for new projects and existing Free and Pro projects from 2026-07-09, cutting log noise. Re-enable it via the dashboard or Management API.\\n\\n---\\n\\n## Self-hosted Supabase: API_EXTERNAL_URL to include /auth/v1\\n\\n2026-06-18 · Breaking Change · Auth · [supabase.com/changelog/47093-self-hosted-supabase-api-external-url-to-include-auth-v1](https://supabase.com/changelog/47093-self-hosted-supabase-api-external-url-to-include-auth-v1)\\n\\nSelf-hosted Supabase's default `API_EXTERNAL_URL` now includes `/auth/v1` from the week of 2026-07-06. SAML SSO users must repoint their IdP to the new `/auth/v1/sso/saml/*` endpoints.\\n\\n\",\n \"stderr\": \"curl: (23) Failure writing output to destination\\n\"\n}", + "input": "{\n \"command\": \"curl -fsSL https://supabase.com/changelog.md | head -80\",\n \"timeout_sec\": 30\n}", + "output": "{\n \"exit_code\": 0,\n \"stdout\": \"# Supabase Changelog\\n\\n## Fixed a panic in project metrics collection that could drop metrics\\n\\n2026-07-29 · Bug Fix · Observability, Platform · [supabase.com/changelog/admin-api-metrics-panic-fix](https://supabase.com/changelog/admin-api-metrics-panic-fix)\\n\\nA metrics endpoint could panic under concurrent requests, intermittently stopping metrics from being collected and pushed for a project.\\n\\n---\\n\\n## Migration of Supabase Management API `logs.all` analytics endpoint to `logs` endpoint\\n\\n2026-07-23 · Breaking Change · Platform · [supabase.com/changelog/48235-migration-of-supabase-management-api-logs-all-analytics-endpoint-to-logs-endpoint](https://supabase.com/changelog/48235-migration-of-supabase-management-api-logs-all-analytics-endpoint-to-logs-endpoint)\\n\\nThe Supabase Management API `logs.all` endpoint is removed on 2026-09-23. Scripts calling it must migrate to the new ClickHouse-backed `logs` endpoint, which accepts ClickHouse SQL only.\\n\\n---\\n\\n## Extension version pinning is deprecated in favor of default versions\\n\\n2026-07-22 · Breaking Change · Database, Security · [supabase.com/changelog/extension-version-pinning-ignored](https://supabase.com/changelog/extension-version-pinning-ignored)\\n\\nSpecifying an explicit version in CREATE or ALTER EXTENSION is deprecated. From 2026-08-05 the version clause is ignored and the extension's default version is installed, with a warning.\\n\\n---\\n\\n## [Public Alpha] Supabase Pipelines\\n\\n2026-07-21 · New Feature · ETL · [supabase.com/changelog/pipelines](https://supabase.com/changelog/pipelines)\\n\\nStream Postgres changes to BigQuery in near real time with Supabase Pipelines, a managed CDC service configured right in the Dashboard. Now in public alpha on all paid plans.\\n\\n---\\n\\n## Self-hosted Supabase: Envoy becomes the default API gateway (breaking change)\\n\\n2026-07-17 · Breaking Change · Platform · [supabase.com/changelog/48048-self-hosted-supabase-envoy-becomes-the-default-api-gateway-b](https://supabase.com/changelog/48048-self-hosted-supabase-envoy-becomes-the-default-api-gateway-b)\\n\\nSelf-hosted Supabase makes Envoy the default API gateway, replacing Kong, the week of 2026-08-09. Opt back into Kong if you rely on its HTTPS listener, a custom `kong.yml`, or the kong service name.\\n\\n---\\n\\n## Realtime schema is now fully locked down against modifications\\n\\n2026-07-14 · Breaking Change · Realtime · [supabase.com/changelog/realtime-schema-locked-down-against-modification](https://supabase.com/changelog/realtime-schema-locked-down-against-modification)\\n\\nSupabase now blocks all changes to the `realtime` schema: creating, altering, or dropping objects fails with a permission error. RLS policies on `realtime.messages` still work.\\n\\n---\\n\\n## Deprecation notice: `@supabase/supabase-js` will require TypeScript 5.0+\\n\\n2026-07-10 · Deprecation · supabase-js · [supabase.com/changelog/47812-deprecation-notice-supabase-supabase-js-will-require-typescript-5-0](https://supabase.com/changelog/47812-deprecation-notice-supabase-supabase-js-will-require-typescript-5-0)\\n\\n`@supabase/supabase-js` will require TypeScript 5.0 starting January 31, 2027, dropping support for TypeScript 4.7 to 4.9. Upgrade to 5.0 or later before then.\\n\\n---\\n\\n## Developer Update - July 2026\\n\\n2026-07-09 · Improvement · Data APIs, Database, Realtime · [supabase.com/changelog/47796-developer-update-july-2026](https://supabase.com/changelog/47796-developer-update-july-2026)\\n\\nSupabase Developer Update for July 2026: Realtime Broadcast now supports binary payloads, Wrappers v0.6.2 adds a MongoDB foreign data wrapper, and OpenCode integrates with Supabase.\\n\\n---\\n\\n## log_connections is to be turned off by default for new projects and existing Free/Pro projects\\n\\n2026-06-22 · Improvement · Platform · [supabase.com/changelog/47197-log-connections-is-to-be-turned-off-by-default-for-new-projects-and-existing-free-pro-projects](https://supabase.com/changelog/47197-log-connections-is-to-be-turned-off-by-default-for-new-projects-and-existing-free-pro-projects)\\n\\nPostgres `log_connections` defaults to off for new projects and existing Free and Pro projects from 2026-07-09, cutting log noise. Re-enable it via the dashboard or Management API.\\n\\n---\\n\\n## Self-hosted Supabase: API_EXTERNAL_URL to include /auth/v1\\n\\n2026-06-18 · Breaking Change · Auth · [supabase.com/changelog/47093-self-hosted-supabase-api-external-url-to-include-auth-v1](https://supabase.com/changelog/47093-self-hosted-supabase-api-external-url-to-include-auth-v1)\\n\\nSelf-hosted Supabase's default `API_EXTERNAL_URL` now includes `/auth/v1` from the week of 2026-07-06. SAML SSO users must repoint their IdP to the new `/auth/v1/sso/saml/*` endpoints.\\n\\n\",\n \"stderr\": \"curl: (23) Failure writing output to destination\\n\"\n}" + }, + { + "id": "build-cli-002-declarative-schema-tool-9", + "title": "Tool: bash", + "startTime": "2026-08-01T15:58:01.338Z", + "endTime": "2026-08-01T15:58:03.857Z", + "duration": 2519, + "type": "tool_execution", + "status": "success", + "raw": "{\n \"exit_code\": 0,\n \"stdout\": \"Supabase CLI 2.67.1\\n\\nUsage:\\n supabase [command]\\n\\nQuick Start:\\n bootstrap Bootstrap a Supabase project from a starter template\\n\\nLocal Development:\\n db Manage Postgres databases\\n gen Run code generation tools\\n init Initialize a local project\\n inspect Tools to inspect your Supabase project\\n link Link to a Supabase project\\n login Authenticate using an access token\\n logout Log out and delete access tokens locally\\n migration Manage database migration scripts\\n seed Seed a Supabase project from supabase/config.toml\\n services Show versions of all Supabase services\\n start Start containers for Supabase local development\\n status Show status of local Supabase containers\\n stop Stop all local Supabase containers\\n test Run tests on local Supabase containers\\n unlink Unlink a Supabase project\\n\\nManagement APIs:\\n backups Manage Supabase physical backups\\n branches Manage Supabase preview branches\\n config Manage Supabase project configurations\\n domains Manage custom domain names for Supabase projects\\n encryption Manage encryption keys of Supabase projects\\n functions Manage Supabase Edge functions\\n network-bans Manage network bans\\n network-restrictions Manage network restrictions\\n orgs Manage Supabase organizations\\n postgres-config Manage Postgres database config\\n projects Manage Supabase projects\\n secrets Manage Supabase secrets\\n snippets Manage Supabase SQL snippets\\n ssl-enforcement Manage SSL enforcement configuration\\n sso Manage Single Sign-On (SSO) authentication for projects\\n storage Manage Supabase Storage objects\\n vanity-subdomains Manage vanity subdomains for Supabase projects\\n\\nAdditional Commands:\\n completion Generate the autocompletion script for the specified shell\\n help Help about any command\\n\\nFlags:\\n --create-ticket create a support ticket for any CLI error\\n --debug output debug logs to stderr\\n --dns-resolver [ native | https ] lookup domain names using the specified resolver (default native)\\n --experimental enable experimental features\\n -h, --help help for supabase\\n --network-id string use the specified docker network instead of a generated one\\n -o, --output [ env | pretty | json | toml | yaml ] output format of status variables (default pretty)\\n --profile string use a specific profile for connecting to Supabase API (default \\\"supabase\\\")\\n -v, --version version for supabase\\n --workdir string path to a Supabase project directory\\n --yes answer yes to all prompts\\n\\nUse \\\"supabase [command] --help\\\" for more information about a command.\\n\",\n \"stderr\": \"A new version of Supabase CLI is available: v2.111.0 (currently installed v2.67.1)\\nWe recommend updating regularly for new features and bug fixes: https://supabase.com/docs/guides/cli/getting-started#updating-the-supabase-cli\\n\"\n}", + "input": "{\n \"command\": \"supabase --help | head -80\",\n \"timeout_sec\": 30\n}", + "output": "{\n \"exit_code\": 0,\n \"stdout\": \"Supabase CLI 2.67.1\\n\\nUsage:\\n supabase [command]\\n\\nQuick Start:\\n bootstrap Bootstrap a Supabase project from a starter template\\n\\nLocal Development:\\n db Manage Postgres databases\\n gen Run code generation tools\\n init Initialize a local project\\n inspect Tools to inspect your Supabase project\\n link Link to a Supabase project\\n login Authenticate using an access token\\n logout Log out and delete access tokens locally\\n migration Manage database migration scripts\\n seed Seed a Supabase project from supabase/config.toml\\n services Show versions of all Supabase services\\n start Start containers for Supabase local development\\n status Show status of local Supabase containers\\n stop Stop all local Supabase containers\\n test Run tests on local Supabase containers\\n unlink Unlink a Supabase project\\n\\nManagement APIs:\\n backups Manage Supabase physical backups\\n branches Manage Supabase preview branches\\n config Manage Supabase project configurations\\n domains Manage custom domain names for Supabase projects\\n encryption Manage encryption keys of Supabase projects\\n functions Manage Supabase Edge functions\\n network-bans Manage network bans\\n network-restrictions Manage network restrictions\\n orgs Manage Supabase organizations\\n postgres-config Manage Postgres database config\\n projects Manage Supabase projects\\n secrets Manage Supabase secrets\\n snippets Manage Supabase SQL snippets\\n ssl-enforcement Manage SSL enforcement configuration\\n sso Manage Single Sign-On (SSO) authentication for projects\\n storage Manage Supabase Storage objects\\n vanity-subdomains Manage vanity subdomains for Supabase projects\\n\\nAdditional Commands:\\n completion Generate the autocompletion script for the specified shell\\n help Help about any command\\n\\nFlags:\\n --create-ticket create a support ticket for any CLI error\\n --debug output debug logs to stderr\\n --dns-resolver [ native | https ] lookup domain names using the specified resolver (default native)\\n --experimental enable experimental features\\n -h, --help help for supabase\\n --network-id string use the specified docker network instead of a generated one\\n -o, --output [ env | pretty | json | toml | yaml ] output format of status variables (default pretty)\\n --profile string use a specific profile for connecting to Supabase API (default \\\"supabase\\\")\\n -v, --version version for supabase\\n --workdir string path to a Supabase project directory\\n --yes answer yes to all prompts\\n\\nUse \\\"supabase [command] --help\\\" for more information about a command.\\n\",\n \"stderr\": \"A new version of Supabase CLI is available: v2.111.0 (currently installed v2.67.1)\\nWe recommend updating regularly for new features and bug fixes: https://supabase.com/docs/guides/cli/getting-started#updating-the-supabase-cli\\n\"\n}" + }, + { + "id": "build-cli-002-declarative-schema-tool-10", + "title": "Tool: files_list", + "startTime": "2026-08-01T15:58:03.857Z", + "endTime": "2026-08-01T15:58:03.861Z", + "duration": 4, + "type": "tool_execution", + "status": "success", + "raw": "{\n \"entries\": [\n {\n \"path\": \"supabase/.branches\",\n \"type\": \"dir\"\n },\n {\n \"path\": \"supabase/.temp\",\n \"type\": \"dir\"\n },\n {\n \"path\": \"supabase/config.toml\",\n \"type\": \"file\"\n },\n {\n \"path\": \"supabase/migrations\",\n \"type\": \"dir\"\n },\n {\n \"path\": \"supabase/schemas\",\n \"type\": \"dir\"\n }\n ]\n}", + "input": "{\n \"path\": \"supabase\"\n}", + "output": "{\n \"entries\": [\n {\n \"path\": \"supabase/.branches\",\n \"type\": \"dir\"\n },\n {\n \"path\": \"supabase/.temp\",\n \"type\": \"dir\"\n },\n {\n \"path\": \"supabase/config.toml\",\n \"type\": \"file\"\n },\n {\n \"path\": \"supabase/migrations\",\n \"type\": \"dir\"\n },\n {\n \"path\": \"supabase/schemas\",\n \"type\": \"dir\"\n }\n ]\n}" + }, + { + "id": "build-cli-002-declarative-schema-tool-11", + "title": "Tool: bash", + "startTime": "2026-08-01T15:58:03.861Z", + "endTime": "2026-08-01T15:58:03.903Z", + "duration": 42, + "type": "tool_execution", + "status": "success", + "raw": "{\n \"exit_code\": 0,\n \"stdout\": \"supabase/.branches/_current_branch\\nsupabase/.temp/cli-latest\\nsupabase/config.toml\\nsupabase/migrations/20260101000000_create_products.sql\\nsupabase/schemas/products.sql\\n\",\n \"stderr\": \"\"\n}", + "input": "{\n \"command\": \"find supabase -maxdepth 3 -type f -print | sort\",\n \"timeout_sec\": 30\n}", + "output": "{\n \"exit_code\": 0,\n \"stdout\": \"supabase/.branches/_current_branch\\nsupabase/.temp/cli-latest\\nsupabase/config.toml\\nsupabase/migrations/20260101000000_create_products.sql\\nsupabase/schemas/products.sql\\n\",\n \"stderr\": \"\"\n}" + }, + { + "id": "build-cli-002-declarative-schema-tool-12", + "title": "Tool: bash", + "startTime": "2026-08-01T15:58:03.903Z", + "endTime": "2026-08-01T15:58:03.908Z", + "duration": 5, + "type": "tool_execution", + "status": "success", + "raw": "{\n \"exit_code\": 0,\n \"stdout\": \"_realtime|extensions\\n_realtime|schema_migrations\\n_realtime|tenants\\nauth|audit_log_entries\\nauth|flow_state\\nauth|identities\\nauth|instances\\nauth|mfa_amr_claims\\nauth|mfa_challenges\\nauth|mfa_factors\\nauth|oauth_authorizations\\nauth|oauth_client_states\\nauth|oauth_clients\\nauth|oauth_consents\\nauth|one_time_tokens\\nauth|refresh_tokens\\nauth|saml_providers\\nauth|saml_relay_states\\nauth|schema_migrations\\nauth|sessions\\nauth|sso_domains\\nauth|sso_providers\\nauth|users\\nextensions|pg_stat_statements\\nextensions|pg_stat_statements_info\\nnet|_http_response\\nnet|http_request_queue\\npublic|products\\nrealtime|messages\\nrealtime|messages_2026_07_31\\nrealtime|messages_2026_08_01\\nrealtime|messages_2026_08_02\\nrealtime|messages_2026_08_03\\nrealtime|messages_2026_08_04\\nrealtime|schema_migrations\\nrealtime|subscription\\nstorage|buckets\\nstorage|buckets_analytics\\nstorage|buckets_vectors\\nstorage|iceberg_namespaces\\nstorage|iceberg_tables\\nstorage|migrations\\nstorage|objects\\nstorage|prefixes\\nstorage|s3_multipart_uploads\\nstorage|s3_multipart_uploads_parts\\nstorage|vector_indexes\\nsupabase_functions|hooks\\nsupabase_functions|migrations\\nsupabase_migrations|schema_migrations\\nvault|decrypted_secrets\\nvault|secrets\\n\",\n \"stderr\": \"\"\n}", + "input": "{\n \"command\": \"psql 'postgresql://postgres:REDACTED@127.0.0.1:54322/postgres' -Atc \\\"select table_schema, table_name from information_schema.tables where table_schema not in ('pg_catalog','information_schema') order by 1,2;\\\"\",\n \"timeout_sec\": 30\n}", + "output": "{\n \"exit_code\": 0,\n \"stdout\": \"_realtime|extensions\\n_realtime|schema_migrations\\n_realtime|tenants\\nauth|audit_log_entries\\nauth|flow_state\\nauth|identities\\nauth|instances\\nauth|mfa_amr_claims\\nauth|mfa_challenges\\nauth|mfa_factors\\nauth|oauth_authorizations\\nauth|oauth_client_states\\nauth|oauth_clients\\nauth|oauth_consents\\nauth|one_time_tokens\\nauth|refresh_tokens\\nauth|saml_providers\\nauth|saml_relay_states\\nauth|schema_migrations\\nauth|sessions\\nauth|sso_domains\\nauth|sso_providers\\nauth|users\\nextensions|pg_stat_statements\\nextensions|pg_stat_statements_info\\nnet|_http_response\\nnet|http_request_queue\\npublic|products\\nrealtime|messages\\nrealtime|messages_2026_07_31\\nrealtime|messages_2026_08_01\\nrealtime|messages_2026_08_02\\nrealtime|messages_2026_08_03\\nrealtime|messages_2026_08_04\\nrealtime|schema_migrations\\nrealtime|subscription\\nstorage|buckets\\nstorage|buckets_analytics\\nstorage|buckets_vectors\\nstorage|iceberg_namespaces\\nstorage|iceberg_tables\\nstorage|migrations\\nstorage|objects\\nstorage|prefixes\\nstorage|s3_multipart_uploads\\nstorage|s3_multipart_uploads_parts\\nstorage|vector_indexes\\nsupabase_functions|hooks\\nsupabase_functions|migrations\\nsupabase_migrations|schema_migrations\\nvault|decrypted_secrets\\nvault|secrets\\n\",\n \"stderr\": \"\"\n}" + }, + { + "id": "build-cli-002-declarative-schema-tool-13", + "title": "Tool: bash", + "startTime": "2026-08-01T15:58:03.908Z", + "endTime": "2026-08-01T15:58:06.162Z", + "duration": 2254, + "type": "tool_execution", + "status": "success", + "raw": "{\n \"exit_code\": 0,\n \"stdout\": \"Manage Postgres databases\\n\\nUsage:\\n supabase db [command]\\n\\nAvailable Commands:\\n diff Diffs the local database for schema changes\\n dump Dumps data or schemas from the remote database\\n lint Checks local database for typing error\\n pull Pull schema from the remote database\\n push Push new migrations to the remote database\\n reset Resets the local database to current migrations\\n start Starts local Postgres database\\n\\nFlags:\\n -h, --help help for db\\n\\nGlobal Flags:\\n --create-ticket create a support ticket for any CLI error\\n --debug output debug logs to stderr\\n --dns-resolver [ native | https ] lookup domain names using the specified resolver (default native)\\n --experimental enable experimental features\\n --network-id string use the specified docker network instead of a generated one\\n -o, --output [ env | pretty | json | toml | yaml ] output format of status variables (default pretty)\\n --profile string use a specific profile for connecting to Supabase API (default \\\"supabase\\\")\\n --workdir string path to a Supabase project directory\\n --yes answer yes to all prompts\\n\\nUse \\\"supabase db [command] --help\\\" for more information about a command.\\n\",\n \"stderr\": \"A new version of Supabase CLI is available: v2.111.0 (currently installed v2.67.1)\\nWe recommend updating regularly for new features and bug fixes: https://supabase.com/docs/guides/cli/getting-started#updating-the-supabase-cli\\n\"\n}", + "input": "{\n \"command\": \"supabase db --help\",\n \"timeout_sec\": 30\n}", + "output": "{\n \"exit_code\": 0,\n \"stdout\": \"Manage Postgres databases\\n\\nUsage:\\n supabase db [command]\\n\\nAvailable Commands:\\n diff Diffs the local database for schema changes\\n dump Dumps data or schemas from the remote database\\n lint Checks local database for typing error\\n pull Pull schema from the remote database\\n push Push new migrations to the remote database\\n reset Resets the local database to current migrations\\n start Starts local Postgres database\\n\\nFlags:\\n -h, --help help for db\\n\\nGlobal Flags:\\n --create-ticket create a support ticket for any CLI error\\n --debug output debug logs to stderr\\n --dns-resolver [ native | https ] lookup domain names using the specified resolver (default native)\\n --experimental enable experimental features\\n --network-id string use the specified docker network instead of a generated one\\n -o, --output [ env | pretty | json | toml | yaml ] output format of status variables (default pretty)\\n --profile string use a specific profile for connecting to Supabase API (default \\\"supabase\\\")\\n --workdir string path to a Supabase project directory\\n --yes answer yes to all prompts\\n\\nUse \\\"supabase db [command] --help\\\" for more information about a command.\\n\",\n \"stderr\": \"A new version of Supabase CLI is available: v2.111.0 (currently installed v2.67.1)\\nWe recommend updating regularly for new features and bug fixes: https://supabase.com/docs/guides/cli/getting-started#updating-the-supabase-cli\\n\"\n}" + }, + { + "id": "build-cli-002-declarative-schema-tool-14", + "title": "Tool: files_read", + "startTime": "2026-08-01T15:58:06.162Z", + "endTime": "2026-08-01T15:58:06.169Z", + "duration": 7, + "type": "tool_execution", + "status": "success", + "raw": "{\n \"contents\": \"create table public.products (\\n id serial primary key,\\n name text not null,\\n price numeric not null\\n);\\n\"\n}", + "input": "{\n \"path\": \"supabase/schemas/products.sql\"\n}", + "output": "{\n \"contents\": \"create table public.products (\\n id serial primary key,\\n name text not null,\\n price numeric not null\\n);\\n\"\n}" + }, + { + "id": "build-cli-002-declarative-schema-tool-15", + "title": "Tool: files_read", + "startTime": "2026-08-01T15:58:06.169Z", + "endTime": "2026-08-01T15:58:06.176Z", + "duration": 7, + "type": "tool_execution", + "status": "success", + "raw": "{\n \"contents\": \"create table public.products (\\n id serial primary key,\\n name text not null,\\n price numeric not null\\n);\\n\"\n}", + "input": "{\n \"path\": \"supabase/migrations/20260101000000_create_products.sql\"\n}", + "output": "{\n \"contents\": \"create table public.products (\\n id serial primary key,\\n name text not null,\\n price numeric not null\\n);\\n\"\n}" + }, + { + "id": "build-cli-002-declarative-schema-tool-16", + "title": "Tool: files_read", + "startTime": "2026-08-01T15:58:06.176Z", + "endTime": "2026-08-01T15:58:06.177Z", + "duration": 1, + "type": "tool_execution", + "status": "success", + "raw": "{\n \"contents\": \"project_id = \\\"declarative-schema\\\"\\n\\n[db]\\nport = 54322\\nshadow_port = 54320\\nmajor_version = 17\\n\\n[db.migrations]\\nschema_paths = [\\\"./schemas/*.sql\\\"]\\n\\n[api]\\nenabled = true\\nport = 54321\\nschemas = [\\\"public\\\", \\\"graphql_public\\\"]\\nextra_search_path = [\\\"public\\\", \\\"extensions\\\"]\\nmax_rows = 1000\\n\\n[auth]\\nenabled = true\\nsite_url = \\\"http://127.0.0.1:3000\\\"\\njwt_expiry = 3600\\nenable_signup = true\\n\\n[auth.email]\\nenable_signup = true\\nenable_confirmations = false\\n\"\n}", + "input": "{\n \"path\": \"supabase/config.toml\"\n}", + "output": "{\n \"contents\": \"project_id = \\\"declarative-schema\\\"\\n\\n[db]\\nport = 54322\\nshadow_port = 54320\\nmajor_version = 17\\n\\n[db.migrations]\\nschema_paths = [\\\"./schemas/*.sql\\\"]\\n\\n[api]\\nenabled = true\\nport = 54321\\nschemas = [\\\"public\\\", \\\"graphql_public\\\"]\\nextra_search_path = [\\\"public\\\", \\\"extensions\\\"]\\nmax_rows = 1000\\n\\n[auth]\\nenabled = true\\nsite_url = \\\"http://127.0.0.1:3000\\\"\\njwt_expiry = 3600\\nenable_signup = true\\n\\n[auth.email]\\nenable_signup = true\\nenable_confirmations = false\\n\"\n}" + }, + { + "id": "build-cli-002-declarative-schema-tool-17", + "title": "Tool: bash", + "startTime": "2026-08-01T15:58:06.177Z", + "endTime": "2026-08-01T15:58:06.227Z", + "duration": 50, + "type": "tool_execution", + "status": "success", + "raw": "{\n \"exit_code\": 0,\n \"stdout\": \"Manage database migration scripts\\n\\nUsage:\\n supabase migration [command]\\n\\nAliases:\\n migration, migrations\\n\\nAvailable Commands:\\n down Resets applied migrations up to the last n versions\\n fetch Fetch migration files from history table\\n list List local and remote migrations\\n new Create an empty migration script\\n repair Repair the migration history table\\n squash Squash migrations to a single file\\n up Apply pending migrations to local database\\n\\nFlags:\\n -h, --help help for migration\\n\\nGlobal Flags:\\n --create-ticket create a support ticket for any CLI error\\n --debug output debug logs to stderr\\n --dns-resolver [ native | https ] lookup domain names using the specified resolver (default native)\\n --experimental enable experimental features\\n --network-id string use the specified docker network instead of a generated one\\n -o, --output [ env | pretty | json | toml | yaml ] output format of status variables (default pretty)\\n --profile string use a specific profile for connecting to Supabase API (default \\\"supabase\\\")\\n --workdir string path to a Supabase project directory\\n --yes answer yes to all prompts\\n\\nUse \\\"supabase migration [command] --help\\\" for more information about a command.\\nDiffs the local database for schema changes\\n\\nUsage:\\n supabase db diff [flags]\\n\\nFlags:\\n --db-url string Diffs against the database specified by the connection string (must be percent-encoded).\\n -f, --file string Saves schema diff to a new migration file.\\n -h, --help help for diff\\n --linked Diffs local migration files against the linked project.\\n --local Diffs local migration files against the local database. (default true)\\n -s, --schema strings Comma separated list of schema to include.\\n --use-migra Use migra to generate schema diff. (default true)\\n --use-pg-schema Use pg-schema-diff to generate schema diff.\\n --use-pgadmin Use pgAdmin to generate schema diff.\\n\\nGlobal Flags:\\n --create-ticket create a support ticket for any CLI error\\n --debug output debug logs to stderr\\n --dns-resolver [ native | https ] lookup domain names using the specified resolver (default native)\\n --experimental enable experimental features\\n --network-id string use the specified docker network instead of a generated one\\n -o, --output [ env | pretty | json | toml | yaml ] output format of status variables (default pretty)\\n --profile string use a specific profile for connecting to Supabase API (default \\\"supabase\\\")\\n --workdir string path to a Supabase project directory\\n --yes answer yes to all prompts\\n\",\n \"stderr\": \"A new version of Supabase CLI is available: v2.111.0 (currently installed v2.67.1)\\nWe recommend updating regularly for new features and bug fixes: https://supabase.com/docs/guides/cli/getting-started#updating-the-supabase-cli\\nA new version of Supabase CLI is available: v2.111.0 (currently installed v2.67.1)\\nWe recommend updating regularly for new features and bug fixes: https://supabase.com/docs/guides/cli/getting-started#updating-the-supabase-cli\\n\"\n}", + "input": "{\n \"command\": \"supabase migration --help && supabase db diff --help\",\n \"timeout_sec\": 30\n}", + "output": "{\n \"exit_code\": 0,\n \"stdout\": \"Manage database migration scripts\\n\\nUsage:\\n supabase migration [command]\\n\\nAliases:\\n migration, migrations\\n\\nAvailable Commands:\\n down Resets applied migrations up to the last n versions\\n fetch Fetch migration files from history table\\n list List local and remote migrations\\n new Create an empty migration script\\n repair Repair the migration history table\\n squash Squash migrations to a single file\\n up Apply pending migrations to local database\\n\\nFlags:\\n -h, --help help for migration\\n\\nGlobal Flags:\\n --create-ticket create a support ticket for any CLI error\\n --debug output debug logs to stderr\\n --dns-resolver [ native | https ] lookup domain names using the specified resolver (default native)\\n --experimental enable experimental features\\n --network-id string use the specified docker network instead of a generated one\\n -o, --output [ env | pretty | json | toml | yaml ] output format of status variables (default pretty)\\n --profile string use a specific profile for connecting to Supabase API (default \\\"supabase\\\")\\n --workdir string path to a Supabase project directory\\n --yes answer yes to all prompts\\n\\nUse \\\"supabase migration [command] --help\\\" for more information about a command.\\nDiffs the local database for schema changes\\n\\nUsage:\\n supabase db diff [flags]\\n\\nFlags:\\n --db-url string Diffs against the database specified by the connection string (must be percent-encoded).\\n -f, --file string Saves schema diff to a new migration file.\\n -h, --help help for diff\\n --linked Diffs local migration files against the linked project.\\n --local Diffs local migration files against the local database. (default true)\\n -s, --schema strings Comma separated list of schema to include.\\n --use-migra Use migra to generate schema diff. (default true)\\n --use-pg-schema Use pg-schema-diff to generate schema diff.\\n --use-pgadmin Use pgAdmin to generate schema diff.\\n\\nGlobal Flags:\\n --create-ticket create a support ticket for any CLI error\\n --debug output debug logs to stderr\\n --dns-resolver [ native | https ] lookup domain names using the specified resolver (default native)\\n --experimental enable experimental features\\n --network-id string use the specified docker network instead of a generated one\\n -o, --output [ env | pretty | json | toml | yaml ] output format of status variables (default pretty)\\n --profile string use a specific profile for connecting to Supabase API (default \\\"supabase\\\")\\n --workdir string path to a Supabase project directory\\n --yes answer yes to all prompts\\n\",\n \"stderr\": \"A new version of Supabase CLI is available: v2.111.0 (currently installed v2.67.1)\\nWe recommend updating regularly for new features and bug fixes: https://supabase.com/docs/guides/cli/getting-started#updating-the-supabase-cli\\nA new version of Supabase CLI is available: v2.111.0 (currently installed v2.67.1)\\nWe recommend updating regularly for new features and bug fixes: https://supabase.com/docs/guides/cli/getting-started#updating-the-supabase-cli\\n\"\n}" + }, + { + "id": "build-cli-002-declarative-schema-tool-18", + "title": "Tool: bash", + "startTime": "2026-08-01T15:58:06.227Z", + "endTime": "2026-08-01T15:58:10.013Z", + "duration": 3786, + "type": "tool_execution", + "status": "success", + "raw": "{\n \"exit_code\": 0,\n \"stdout\": \"\",\n \"stderr\": \"\"\n}", + "input": "{\n \"command\": \"true\",\n \"timeout_sec\": 30\n}", + "output": "{\n \"exit_code\": 0,\n \"stdout\": \"\",\n \"stderr\": \"\"\n}" + }, + { + "id": "build-cli-002-declarative-schema-tool-19", + "title": "Tool: search_docs", + "startTime": "2026-08-01T15:58:10.013Z", + "endTime": "2026-08-01T15:58:12.502Z", + "duration": 2489, + "type": "tool_execution", + "status": "success", + "raw": "{\n \"content\": [\n {\n \"type\": \"text\",\n \"text\": \"{\\\"result\\\":{\\\"searchDocs\\\":{\\\"nodes\\\":[{\\\"__typename\\\":\\\"Guide\\\",\\\"title\\\":\\\"Declarative database schemas\\\",\\\"href\\\":\\\"https://supabase.com/docs/guides/local-development/declarative-database-schemas\\\",\\\"content\\\":\\\"# Declarative database schemas\\\\n\\\\nManage your database schemas in one place and generate versioned migrations.\\\\n\\\\n## Overview\\\\n\\\\nDeclarative schemas provide a developer-friendly way to maintain

    Files of SQL statements that track the evolution of your database schema over time.
    They allow you to version control your database schema alongside your application code.

    See the database migrations guide to learn more.

    }>schema migrations
    .\\\\n\\\\n[Migrations](/docs/guides/deployment/database-migrations) are traditionally managed imperatively (you provide the instructions on how exactly to change the database). This can lead to related information being scattered over multiple migration files. With declarative schemas, you instead declare the state you want your database to be in, and the instructions are generated for you.\\\\n\\\\n\\\\n## Schema migrations\\\\n\\\\nSchema migrations are SQL statements written in Data Definition Language. They are versioned in your `supabase/migrations` directory to ensure schema consistency between local and remote environments.\\\\n\\\\n\\\\n### Declaring your schema\\\\n\\\\n\\\\n \\\\n \\\\n Create a SQL file in `supabase/schemas` directory that defines an `employees` table.\\\\n \\\\n\\\\n \\\\n \\\\n \\\\n ```sql name=supabase/schemas/employees.sql\\\\n create table \\\\\\\"employees\\\\\\\" (\\\\n \\\\\\\"id\\\\\\\" integer not null,\\\\n \\\\\\\"name\\\\\\\" text\\\\n );\\\\n ```\\\\n \\\\n \\\\n \\\\n \\\\n\\\\n\\\\n\\\\n \\\\n \\\\n Generate a migration file by diffing against your declared schema.\\\\n \\\\n\\\\n \\\\n \\\\n \\\\n ```bash name=Terminal\\\\n supabase db diff -f create_employees_table\\\\n ```\\\\n \\\\n \\\\n \\\\n \\\\n\\\\n\\\\n\\\\n \\\\n \\\\n Start the local database first. Then, apply the migration manually to see your schema changes in the local Dashboard.\\\\n \\\\n\\\\n \\\\n \\\\n \\\\n ```bash name=Terminal\\\\n supabase start\\\\n supabase migration up\\\\n ```\\\\n \\\\n \\\\n \\\\n \\\\n\\\\n\\\\n\\\\n### Updating your schema\\\\n\\\\n\\\\n \\\\n \\\\n Edit `supabase/schemas/employees.sql` file to add a new column to `employees` table.\\\\n \\\\n\\\\n \\\\n \\\\n \\\\n ```sql name=supabase/schemas/employees.sql\\\\n create table \\\\\\\"employees\\\\\\\" (\\\\n \\\\\\\"id\\\\\\\" integer not null,\\\\n \\\\\\\"name\\\\\\\" text,\\\\n \\\\\\\"age\\\\\\\" smallint not null\\\\n );\\\\n ```\\\\n \\\\n \\\\n \\\\n \\\\n\\\\n\\\\n\\\\n Some entities like views and enums expect columns to be declared in a specific order. To avoid messy diffs, always append new columns to the end of the table.\\\\n\\\\n\\\\n\\\\n \\\\n \\\\n Diff existing migrations against your declared schema.\\\\n \\\\n\\\\n \\\\n \\\\n \\\\n ```bash name=Terminal\\\\n supabase db diff -f add_age\\\\n ```\\\\n \\\\n \\\\n \\\\n \\\\n\\\\n\\\\n\\\\n \\\\n \\\\n Verify that the generated migration contain a single incremental change.\\\\n \\\\n\\\\n \\\\n \\\\n _add_age.sql\\\\\\\" label=\\\\\\\"supabase/migrations/_add_age.sql\\\\\\\">\\\\n ```sql name=supabase/migrations/_add_age.sql\\\\n alter table \\\\\\\"public\\\\\\\".\\\\\\\"employees\\\\\\\" add column \\\\\\\"age\\\\\\\" smallint not null;\\\\n ```\\\\n \\\\n \\\\n \\\\n \\\\n\\\\n\\\\n\\\\n \\\\n \\\\n Start the database locally and apply the pending migration.\\\\n \\\\n\\\\n \\\\n \\\\n \\\\n ```bash name=Terminal\\\\n supabase migration up\\\\n ```\\\\n \\\\n \\\\n \\\\n \\\\n\\\\n\\\\n\\\\n### Deploying your schema changes\\\\n\\\\n\\\\n \\\\n \\\\n [Log in](/docs/reference/cli/supabase-login) via the Supabase CLI.\\\\n \\\\n\\\\n \\\\n \\\\n \\\\n ```bash name=Terminal\\\\n supabase login\\\\n ```\\\\n \\\\n \\\\n \\\\n \\\\n\\\\n \\\\n \\\\n Follow the on-screen prompts to [link](/docs/reference/cli/supabase-link) your remote project.\\\\n \\\\n\\\\n \\\\n \\\\n \\\\n ```bash name=Terminal\\\\n supabase link\\\\n ```\\\\n \\\\n \\\\n \\\\n \\\\n\\\\n \\\\n \\\\n [Push](/docs/reference/cli/supabase-db-push) your changes to the remote database.\\\\n \\\\n\\\\n \\\\n \\\\n \\\\n ```bash name=Terminal\\\\n supabase db push\\\\n ```\\\\n \\\\n \\\\n \\\\n \\\\n\\\\n\\\\n\\\\n### Managing dependencies\\\\n\\\\nAs your database schema evolves, you will probably start using more advanced entities like views and functions. These entities are notoriously verbose to manage using plain migrations because the entire body must be recreated whenever there is a change. Using declarative schema, you can now edit them in-place so it’s much easier to review.\\\\n\\\\n\\\\n \\\\n ```sql name=supabase/schemas/employees.sql\\\\n create table \\\\\\\"employees\\\\\\\" (\\\\n \\\\\\\"id\\\\\\\" integer not null,\\\\n \\\\\\\"name\\\\\\\" text,\\\\n \\\\\\\"age\\\\\\\" smallint not null\\\\n );\\\\n\\\\n create view \\\\\\\"profiles\\\\\\\" as\\\\n select id, name from \\\\\\\"employees\\\\\\\";\\\\n\\\\n create function \\\\\\\"get_age\\\\\\\"(employee_id integer) RETURNS smallint\\\\n LANGUAGE \\\\\\\"sql\\\\\\\"\\\\n AS $$\\\\n select age\\\\n from employees\\\\n where id = employee_id;\\\\n $$;\\\\n ```\\\\n \\\\n\\\\n\\\\nYour schema files are run in lexicographic order by default. The order is important when you have foreign keys between multiple tables as the parent table must be created first. For example, your `supabase` directory may end up with the following structure.\\\\n\\\\n```bash\\\\n.\\\\n└── supabase/\\\\n ├── schemas/\\\\n │ ├── employees.sql\\\\n │ └── managers.sql\\\\n └── migrations/\\\\n ├── 20241004112233_create_employees_table.sql\\\\n ├── 20241005112233_add_employee_age.sql\\\\n └── 20241006112233_add_managers_table.sql\\\\n```\\\\n\\\\nFor small projects with only a few tables, the default schema order may be sufficient. However, as your project grows, you might need more control over the order in which schemas are applied. To specify a custom order for applying the schemas, you can declare them explicitly in `config.toml`. Any glob patterns will evaluated, deduplicated, and sorted in lexicographic order. For example, the following pattern ensures `employees.sql` is always executed first.\\\\n\\\\n\\\\n \\\\n ```toml name=supabase/config.toml\\\\n [db.migrations]\\\\n schema_paths = [\\\\n \\\\\\\"./schemas/employees.sql\\\\\\\",\\\\n \\\\\\\"./schemas/*.sql\\\\\\\",\\\\n ]\\\\n ```\\\\n \\\\n\\\\n\\\\n\\\\n### Pulling in your production schema\\\\n\\\\nTo set up declarative schemas on a existing project, you can pull in your production schema by running:\\\\n\\\\n\\\\n \\\\n ```bash name=Terminal\\\\n supabase db dump > supabase/schemas/prod.sql\\\\n ```\\\\n \\\\n\\\\n\\\\nFrom there, you can start breaking down your schema into smaller files and generate migrations. You can do this all at once, or incrementally as you make changes to your schema.\\\\n\\\\n\\\\n### Rolling back a schema change\\\\n\\\\nDuring development, you may want to rollback a migration to keep your new schema changes in a single migration file. This can be done by resetting your local database to a previous version.\\\\n\\\\n\\\\n \\\\n ```bash name=Terminal\\\\n supabase db reset --version 20241005112233\\\\n ```\\\\n \\\\n\\\\n\\\\nAfter a reset, you can [edit the schema](#updating-your-schema) and regenerate a new migration file. Note that you should not reset a version that's already deployed to production.\\\\n\\\\nIf you need to rollback a migration that's already deployed, you should first revert changes to the schema files. Then you can generate a new migration file containing the down migration. This ensures your production migrations are always rolling forward.\\\\n\\\\n\\\\n SQL statements generated in a down migration are usually destructive. You must review them carefully to avoid unintentional data loss.\\\\n\\\\n\\\\n\\\\n## Known caveats\\\\n\\\\nThe `migra` diff tool used for generating schema diff is capable of tracking most database changes. However, there are edge cases where it can fail.\\\\n\\\\nIf you need to use any of the entities below, remember to add them through [versioned migrations](/docs/guides/deployment/database-migrations) instead.\\\\n\\\\n\\\\n### Data manipulation language\\\\n\\\\n* DML statements such as `insert`, `update`, `delete`, etc., are not captured by schema diff\\\\n\\\\n\\\\n### View ownership\\\\n\\\\n* [view owner and grants](https://github.com/djrobstep/migra/issues/160#issuecomment-1702983833)\\\\n* [security invoker on views](https://github.com/djrobstep/migra/issues/234)\\\\n* [materialized views](https://github.com/djrobstep/migra/issues/194)\\\\n* doesn’t recreate views when altering column type\\\\n\\\\n\\\\n### RLS policies\\\\n\\\\n* [alter policy statements](https://github.com/djrobstep/schemainspect/blob/master/schemainspect/pg/obj.py#L228)\\\\n* [column privileges](https://github.com/djrobstep/schemainspect/pull/67)\\\\n\\\\n\\\\n### Other entities\\\\n\\\\n* schema privileges are not tracked because each schema is diffed separately\\\\n* [comments are not tracked](https://github.com/djrobstep/migra/issues/69)\\\\n* [partitions are not tracked](https://github.com/djrobstep/migra/issues/186)\\\\n* [`alter publication ... add table ...`](https://github.com/supabase/cli/issues/883)\\\\n* [create domain statements are ignored](https://github.com/supabase/cli/issues/2137)\\\\n* [grant statements are duplicated from default privileges](https://github.com/supabase/cli/issues/1864)\\\\n\\\"},{\\\"__typename\\\":\\\"Guide\\\",\\\"title\\\":\\\"Local development workflow\\\",\\\"href\\\":\\\"https://supabase.com/docs/guides/local-development/cli-workflows\\\",\\\"content\\\":\\\"# Local development workflow\\\\n\\\\nSet up and run your day-to-day local development workflow with the Supabase CLI.\\\\n\\\\nThis guide walks through two common starting points for local development with the Supabase CLI, and shows how they converge into the same daily workflow. By the end, you'll have a `./supabase` directory in your repo that anyone can clone to recreate the full project, locally or on a fresh remote instance.\\\\n\\\\nThere are two starting points, both leading to the same place: database schema and migrations tracked in version control, with seed data for local development.\\\\n\\\\n* **[Move an existing project to local development](#move-an-existing-project-to-local-development)**: you have a project on the Supabase platform and want to bring it into a proper local development workflow.\\\\n* **[Start a new project from scratch](#start-a-new-project-from-scratch)**: you're building locally and will eventually push to a remote instance.\\\\n\\\\n\\\\n## Before you begin\\\\n\\\\nYou need the Supabase CLI installed and a Docker-compatible runtime running. If you haven't set these up yet, see [Install and run the CLI](/docs/guides/local-development/cli/getting-started) for installation across macOS, Windows, and Linux, and for the details of what `supabase start` brings up and how to access each service.\\\\n\\\\nKeep in mind that **the local stack is for development only**. It is not hardened for production use and must never be exposed to external traffic. It has no TLS, no rate limiting, and default credentials. Use it to develop and test, then deploy to the [Supabase Platform](https://supabase.com) or a proper self-hosted setup for anything beyond that.\\\\n\\\\n\\\\n How you invoke the CLI depends on how you installed it:\\\\n\\\\n * Installed globally with **Homebrew or Scoop**: run `supabase `.\\\\n * Added as a **project dependency** with npm, pnpm, yarn, or bun: run it through your package runner instead, for example `npx supabase ` (or `pnpm supabase`, `yarn supabase`, `bunx supabase`).\\\\n\\\\n Every example in this guide is written as `supabase `. Translate it to whichever form matches your install. See [Install and run the CLI](/docs/guides/local-development/cli/getting-started) for the full setup.\\\\n\\\\n\\\\n\\\\n If you want a working project to explore rather than an empty one, `supabase bootstrap` scaffolds a starter application (Next.js, Flutter, and more) with schema, migrations, and config already wired up. It's an alternative entry point to `supabase init` when starting a new project from scratch.\\\\n\\\\n\\\\n\\\\n## The `./supabase` directory\\\\n\\\\nAfter `supabase init`, your project contains a `./supabase` directory. Here's what goes in it and what to commit:\\\\n\\\\n| Path | Purpose | Commit? |\\\\n| ---------------------- | ----------------------------------------------------------------- | ------- |\\\\n| `config.toml` | Local stack configuration (ports, auth settings, etc.) | Yes |\\\\n| `migrations/` | Timestamped SQL migration files, applied in order | Yes |\\\\n| `seed.sql` | Dev/test data, applied after migrations on `start` and `db reset` | Yes |\\\\n| `schemas/` | Declarative schema files (if using that approach) | Yes |\\\\n| `.temp/`, `.branches/` | CLI internal state | No |\\\\n\\\\nThe `config.toml` is safe to commit. It contains no secrets by default. If you add sensitive values (OAuth credentials, API keys), use the `env()` function to reference environment variables instead of hardcoding them. See [Managing config and secrets](/docs/guides/local-development/managing-config).\\\\n\\\\n\\\\n Many database commands accept `--local` and `--linked` flags to choose what they act on. The defaults are not the same across commands: `db diff` and `db reset` default to `--local`, while `db pull`, `db push`, and `db dump` default to `--linked`. When in doubt, pass the flag explicitly.\\\\n\\\\n\\\\n\\\\n## Move an existing project to local development\\\\n\\\\nYou've built a project on the Supabase platform, with tables created via the Dashboard, SQL editor, or client libraries. Now you want a local dev setup with everything in version control.\\\\n\\\\n\\\\n### Step 1: Initialize\\\\n\\\\nIn your project root:\\\\n\\\\n```bash\\\\nsupabase init\\\\n```\\\\n\\\\nThis creates `./supabase/config.toml`. If you already have a project directory with application code, run this at the root. The `supabase/` directory will sit alongside your app code.\\\\n\\\\n\\\\n### Step 2: Authenticate\\\\n\\\\n```bash\\\\nsupabase login\\\\n```\\\\n\\\\nOpens a browser to generate an access token. The token is stored locally and used for all subsequent CLI commands that interact with the platform.\\\\n\\\\n\\\\n### Step 3: Link to your remote project\\\\n\\\\n```bash\\\\nsupabase link --project-ref \\\\n```\\\\n\\\\nFind your project ID in the Supabase Dashboard URL: `https://supabase.com/dashboard/project/`.\\\\n\\\\nThis tells the CLI which remote project to connect to for `db pull`, `db push`, and other remote operations. You'll be prompted for the database password, which is the password set when you created the project.\\\\n\\\\n\\\\n### Step 4: Pull the remote schema\\\\n\\\\n```bash\\\\nsupabase db pull\\\\n```\\\\n\\\\nThis connects to your remote database, dumps the entire schema, and saves it as a migration file:\\\\n\\\\n```\\\\nsupabase/migrations/_remote_schema.sql\\\\n```\\\\n\\\\nThis initial migration is your baseline. It represents the current state of your database, and all future changes build on top of it. `db pull` also records this migration as already applied in the remote migration history (the `supabase_migrations.schema_migrations` table), so a later `db push` won't try to reapply it.\\\\n\\\\n\\\\n `db pull` diffs your remote database against the CLI's default local stack, so the generated file can include statements you didn't expect. A common example is `DROP EXTENSION pg_net;`, emitted when your remote project has an extension disabled that the local stack enables by default. These statements apply silently on `db reset` and change your local schema, so read the file before committing it. See [Cleaning up generated migrations](#cleaning-up-generated-migrations) for what to look for.\\\\n\\\\n\\\\n\\\\n If you also use Supabase Auth or Storage and have customized their schemas, pull them separately:\\\\n\\\\n ```bash\\\\n supabase db pull --schema auth -f pull-auth-schema\\\\n supabase db pull --schema storage -f pull-storage-schema\\\\n ```\\\\n\\\\n These schemas are managed by Supabase and typically don't need to be pulled unless you've made custom modifications.\\\\n\\\\n\\\\n\\\\n### Step 5: Create seed data\\\\n\\\\nYou have two options:\\\\n\\\\n**Option A: Dump existing data from remote** (then clean it up):\\\\n\\\\n```bash\\\\nsupabase db dump --data-only --linked > supabase/seed.sql\\\\n```\\\\n\\\\n\\\\n Review and clean up the dump before committing. Remove production user data, secrets, personal information, and anything sensitive. Keep only representative test data that a developer needs to work with the project.\\\\n\\\\n\\\\n**Option B: Write seed data by hand** (recommended for most projects):\\\\n\\\\nCreate `supabase/seed.sql` with INSERT statements that set up a useful local development state: a few test users, sample data, and so on. This is often better than dumping production data because you control exactly what's in it.\\\\n\\\\nFor more on organizing seed files, glob patterns, and generating realistic data, see [Seeding your database](/docs/guides/local-development/seeding-your-database).\\\\n\\\\n\\\\n### Step 6: Verify\\\\n\\\\n```bash\\\\nsupabase start\\\\nsupabase db reset\\\\n```\\\\n\\\\n`db reset` destroys the local database and recreates it from scratch: it applies all migrations in order, then runs `seed.sql`. If this succeeds, your setup is reproducible. Anyone who clones the repo can do the same.\\\\n\\\\n\\\\n### Step 7: Commit\\\\n\\\\n```bash\\\\ngit add supabase/\\\\ngit commit -m \\\\\\\"add supabase local development setup\\\\\\\"\\\\n```\\\\n\\\\nYour project now has a fully reproducible local development environment.\\\\n\\\\n\\\\n For an existing project, the pulled migration already serves as your schema baseline. You don't need to also create a `schemas/` directory, because that would mean maintaining two representations of the same schema. If you want to adopt declarative schemas later, see [Declarative database schemas](/docs/guides/local-development/declarative-database-schemas). For day-to-day changes going forward, see [The daily workflow](#the-daily-workflow) below.\\\\n\\\\n\\\\n\\\\n## Start a new project from scratch\\\\n\\\\nNo remote project yet. You're building from scratch and want to do it right from the start.\\\\n\\\\n\\\\n### Step 1: Initialize\\\\n\\\\n```bash\\\\nsupabase init\\\\n```\\\\n\\\\n\\\\n### Step 2: Start the local stack\\\\n\\\\n```bash\\\\nsupabase start\\\\n```\\\\n\\\\nOn first run, Docker images are pulled, which takes a few minutes. Subsequent starts are fast. Once running, the CLI outputs local service URLs and credentials, including the Studio URL for a local instance of the Dashboard. See [Install and run the CLI](/docs/guides/local-development/cli/getting-started#access-your-projects-services) for the full output and how to reach each service.\\\\n\\\\n\\\\n### Step 3: Create your schema\\\\n\\\\nTwo approaches, pick one:\\\\n\\\\n**Option A: Declarative schema** (recommended for new projects)\\\\n\\\\nDeclare the state you want your database to be in as a file in `supabase/schemas/`, for example:\\\\n\\\\n```sql title=\\\\\\\"supabase/schemas/schema.sql\\\\\\\"\\\\ncreate table public.todos (\\\\n id bigint generated by default as identity primary key,\\\\n created_at timestamptz default now() not null,\\\\n title text not null,\\\\n is_complete boolean default false not null,\\\\n user_id uuid references auth.users (id) default auth.uid() not null\\\\n);\\\\n\\\\nalter table public.todos enable row level security;\\\\n\\\\ncreate policy \\\\\\\"Users can read their own todos\\\\\\\"\\\\n on public.todos for select\\\\n using (auth.uid() = user_id);\\\\n\\\\ncreate policy \\\\\\\"Users can create their own todos\\\\\\\"\\\\n on public.todos for insert\\\\n with check (auth.uid() = user_id);\\\\n```\\\\n\\\\nThen generate a migration from it:\\\\n\\\\n```bash\\\\nsupabase db diff -f initial-schema\\\\n```\\\\n\\\\nThis compares your declared schema against the current (empty) database and generates a migration file in `supabase/migrations/`. For the full declarative workflow, including managing views and functions, ordering schema files, and known caveats, see [Declarative database schemas](/docs/guides/local-development/declarative-database-schemas).\\\\n\\\\n**Option B: Write the migration directly**\\\\n\\\\n```bash\\\\nsupabase migration new initial-schema\\\\n```\\\\n\\\\nThis creates an empty file at `supabase/migrations/_initial-schema.sql`. Write your SQL in it, then apply:\\\\n\\\\n```bash\\\\nsupabase db reset\\\\n```\\\\n\\\\n\\\\n### Step 4: Add seed data\\\\n\\\\nCreate `supabase/seed.sql`:\\\\n\\\\n```sql title=\\\\\\\"supabase/seed.sql\\\\\\\"\\\\n-- Create a test user (Supabase Auth)\\\\n-- Note: this is a placeholder row so seeded data has a user_id to reference.\\\\n-- It has no password, so it can't be used to sign in. To create a\\\\n-- login-capable user, use the Auth admin API or the local Studio.\\\\ninsert into auth.users (id, email, raw_user_meta_data)\\\\nvalues ('d0e3c8f0-1234-5678-9abc-def012345678', 'test@example.com', '{}');\\\\n\\\\n-- Seed application data\\\\ninsert into public.todos (title, user_id)\\\\nvalues\\\\n ('Buy groceries', 'd0e3c8f0-1234-5678-9abc-def012345678'),\\\\n ('Write documentation', 'd0e3c8f0-1234-5678-9abc-def012345678');\\\\n```\\\\n\\\\n\\\\n### Step 5: Verify\\\\n\\\\n```bash\\\\nsupabase db reset\\\\n```\\\\n\\\\nDrops everything, applies migrations, runs seed. If this passes, your project is reproducible.\\\\n\\\\n\\\\n### Step 6: Commit\\\\n\\\\n```bash\\\\ngit add supabase/\\\\ngit commit -m \\\\\\\"add supabase local development setup\\\\\\\"\\\\n```\\\\n\\\\n\\\\n## The daily workflow\\\\n\\\\nBoth starting points converge here. You have a working `./supabase` directory in your repo. Here's how day-to-day development works.\\\\n\\\\n\\\\n### Making schema changes\\\\n\\\\nWhich approach you use is a project-level decision, set when you first created your schema - not a per-change choice. It depends on whether you keep declarative files in `supabase/schemas/`. Pick the tab that matches your project.\\\\n\\\\n\\\\n \\\\n 1. Edit your schema file(s) in `supabase/schemas/` (add a table, a column, a policy, etc.)\\\\n 2. Generate a migration: `supabase db diff -f add-due-date-to-todo`\\\\n 3. Review the generated migration file. See [Cleaning up generated migrations](#cleaning-up-generated-migrations)\\\\n 4. Verify the full chain: `supabase db reset`\\\\n 5. Commit the schema file **and** the migration together\\\\n\\\\n \\\\n `db diff` compares your `supabase/schemas/` files against your existing migrations; it does **not** read the live local database. Changes you make directly in Studio or via SQL are ignored, so `db diff` reports \\\\\\\"No schema changes found\\\\\\\" and silently drops them. Always edit the schema files, then diff.\\\\n \\\\n \\\\n\\\\n \\\\n **If you made changes through the local Studio UI:**\\\\n\\\\n ```bash\\\\n supabase db diff -f add-due-date-to-todo\\\\n ```\\\\n\\\\n This captures your UI changes as a migration file. This works only when your project has **no** declarative files in `supabase/schemas/`: `db diff` then compares the live local database against your migrations. If you use declarative schemas, don't edit through Studio expecting `db diff` to catch it - see the **Declarative schemas** tab.\\\\n\\\\n **If you prefer to write SQL directly:**\\\\n\\\\n ```bash\\\\n supabase migration new add-due-date-to-todo\\\\n ```\\\\n\\\\n Write the SQL in the generated file. Then verify:\\\\n\\\\n ```bash\\\\n supabase db reset\\\\n ```\\\\n\\\\n Commit the migration.\\\\n \\\\n\\\\n\\\\n\\\\n### Generating types\\\\n\\\\nIf your app uses the generated TypeScript types, regenerate them whenever your schema changes:\\\\n\\\\n```bash\\\\nsupabase gen types --lang typescript --local > database.types.ts\\\\n```\\\\n\\\\nUse `--linked` instead of `--local` to generate from your remote project. TypeScript is the default language; pass `--lang go`, `--lang swift`, or `--lang python` for others.\\\\n\\\\nFor working with the generated types (helper types, JSON inference, type-safe queries) and automating regeneration in CI, see [Generating types](/docs/guides/api/rest/generating-types).\\\\n\\\\n\\\\n### Staying in sync with your team\\\\n\\\\nWhen someone else pushes new migrations:\\\\n\\\\n```bash\\\\ngit pull\\\\nsupabase db reset\\\\n```\\\\n\\\\n`db reset` replays all migrations from scratch, so you'll always match the current state of the repo.\\\\n\\\\n\\\\n## Pushing to a remote project\\\\n\\\\nWhen you're ready to deploy your schema to a remote Supabase instance:\\\\n\\\\n```bash\\\\n# Authenticate (if not already)\\\\nsupabase login\\\\n\\\\n# Link to the remote project (if not already)\\\\nsupabase link --project-ref \\\\n\\\\n# Preview what will be applied\\\\nsupabase db push --dry-run\\\\n\\\\n# Apply migrations\\\\nsupabase db push\\\\n```\\\\n\\\\n`db push` applies only migrations that haven't been applied to the remote yet. It tracks this via the `supabase_migrations.schema_migrations` table created automatically on the remote database.\\\\n\\\\nTo also seed a fresh remote instance (dev/staging environments only):\\\\n\\\\n```bash\\\\nsupabase db push --include-seed\\\\n```\\\\n\\\\n\\\\n Never use `--include-seed` on a production database. Seed data is for development and testing.\\\\n\\\\n\\\\n\\\\n### Resetting a remote dev or staging project\\\\n\\\\nIf a dev or staging remote drifts or gets into a messy state, you can wipe it and rebuild it from your local migrations:\\\\n\\\\n```bash\\\\nsupabase db reset --linked\\\\n```\\\\n\\\\nUnlike the default `supabase db reset`, which targets your local database, the `--linked` flag runs against the remote project you connected with `supabase link`: it drops the remote schema, then replays every local migration in order. Add `--include-seed` to reload seed data as well.\\\\n\\\\n\\\\n `db reset --linked` is destructive: it erases all data in the linked remote database. Only run it against throwaway dev or staging projects, and double-check which project you're linked to (`supabase projects list` shows the linked one) before running it. Never use it on production.\\\\n\\\\n\\\\nFor multi-environment setups with CI/CD (feature branches, staging, production), see [Managing Environments](/docs/guides/deployment/managing-environments).\\\\n\\\\n\\\\n## Key commands at a glance\\\\n\\\\n| Command | What it does |\\\\n| -------------------------------------- | ------------------------------------------------------------------------------------------------------- |\\\\n| `supabase init` | Creates `./supabase/config.toml` |\\\\n| `supabase start` | Starts the local stack, applies migrations + seed |\\\\n| `supabase stop` | Stops the local stack (data persists until `db reset`) |\\\\n| `supabase db reset` | Destroys local DB, applies all migrations + seed from scratch |\\\\n| `supabase db reset --linked` | Destroys the **linked remote** DB and rebuilds it from local migrations (destructive; dev/staging only) |\\\\n| `supabase db diff -f ` | Generates a migration by diffing current DB state against a shadow database |\\\\n| `supabase db pull` | Pulls remote schema into a new local migration file |\\\\n| `supabase db push` | Applies pending local migrations to the remote database |\\\\n| `supabase db dump` | Exports remote DB schema (or `--data-only` for data) via `pg_dump` |\\\\n| `supabase migration new ` | Creates an empty migration file |\\\\n| `supabase migration list` | Compares local migrations against remote migration history |\\\\n| `supabase gen types --lang typescript` | Generates TypeScript types from your database schema |\\\\n| `supabase link --project-ref` | Connects local project to a remote Supabase project |\\\\n| `supabase login` | Authenticates with the Supabase platform |\\\\n\\\\nFor the full command reference and every flag, see the [CLI reference](/docs/reference/cli).\\\\n\\\\n\\\\n## Cleaning up generated migrations\\\\n\\\\nWhen `supabase db diff` generates a migration, it may include statements that are technically correct but noisy. Review every generated migration before committing.\\\\n\\\\n\\\\n### Grants\\\\n\\\\nYou may see lines like:\\\\n\\\\n```sql\\\\nGRANT MAINTAIN, REFERENCES, TRIGGER, TRUNCATE ON public.todos TO anon;\\\\nGRANT MAINTAIN, REFERENCES, TRIGGER, TRUNCATE ON public.todos TO authenticated;\\\\nGRANT MAINTAIN, REFERENCES, TRIGGER, TRUNCATE ON public.todos TO service_role;\\\\n```\\\\n\\\\nThese appear because the diff tool treats permissions as part of the schema state. For tables in the `public` schema, these grants are applied by default and the lines are redundant. They're harmless, but if you want clean migrations, you can remove them. Be consistent across your team about whether you keep or remove them.\\\\n\\\\n\\\\n### Revoke/re-grant patterns\\\\n\\\\nSometimes a diff produces:\\\\n\\\\n```sql\\\\nREVOKE ALL ON TABLE public.todos FROM anon;\\\\nGRANT ALL ON TABLE public.todos TO anon;\\\\n```\\\\n\\\\nThis is the diff tool being overly cautious. If you haven't changed permissions, these lines can be safely removed.\\\\n\\\\n\\\\n### Extension statements\\\\n\\\\n`CREATE EXTENSION IF NOT EXISTS ...` may appear. Keep these if the extension is required by your migration. Remove them if the extension is already created by a previous migration or is part of the default Supabase setup.\\\\n\\\\n\\\\n### Known limitations of `db diff`\\\\n\\\\nThe diff is generated by `pg-delta`, the default schema diff engine. (The older [`migra`](https://github.com/djrobstep/migra) engine is still available: set `enabled = false` under `[experimental.pgdelta]` in `config.toml`, or pass `--use-migra`.) No diff engine captures everything. Most notably, DML (INSERT, UPDATE, DELETE) is not tracked, so data changes must be added to the migration manually, and some entities like RLS policy renames and certain view properties don't diff cleanly. See the [full list of caveats](/docs/guides/local-development/declarative-database-schemas#known-caveats) in the declarative schemas guide.\\\\n\\\\nTreat `db diff` output as a draft, not a final migration. When in doubt, review the generated SQL and adjust it manually.\\\\n\\\\n\\\\n## Troubleshooting\\\\n\\\\n**`db reset` fails with a migration error**\\\\n\\\\nThe output will show which migration file failed and the SQL error. Fix the migration file, then run `db reset` again.\\\\n\\\\n**`db push` says migrations are already applied**\\\\n\\\\nThe remote database already has those migrations in its history. Run `supabase migration list` to compare local vs. remote state. If they're out of sync, use `supabase migration repair` to correct the remote history.\\\\n\\\\n**Schema drift: remote was changed outside of migrations**\\\\n\\\\nIf someone modified the remote database directly (via Dashboard, SQL editor, etc.), run `supabase db pull` to capture those changes as a new migration file. Then `supabase db reset` locally to verify everything still works.\\\\n\\\\n**Docker issues on `supabase start`**\\\\n\\\\nEnsure Docker is running and has at least 7 GB of RAM allocated. If containers fail health checks, try:\\\\n\\\\n```bash\\\\nsupabase stop\\\\nsupabase start\\\\n```\\\\n\\\\nIf problems persist, `supabase stop --no-backup` for a clean restart (this removes local database data).\\\\n\\\"},{\\\"__typename\\\":\\\"Guide\\\",\\\"title\\\":\\\"Database Migrations\\\",\\\"href\\\":\\\"https://supabase.com/docs/guides/deployment/database-migrations\\\",\\\"content\\\":\\\"# Database Migrations\\\\n\\\\nHow to manage schema migrations for your Supabase project.\\\\n\\\\nDatabase migrations are SQL statements that create, update, or delete your existing database schemas. They are a common way of tracking changes to your database over time.\\\\n\\\\n\\\\n## Schema migrations\\\\n\\\\nFor this guide, we'll create a table called `employees` and see how we can make changes to it.\\\\n\\\\nYou will need to [install](/docs/guides/local-development#quickstart) the Supabase CLI and start the local development stack.\\\\n\\\\n\\\\n If a lock timeout error occurs, in your migration file, consider increasing your [`lock_timeout`](https://postgresqlco.nf/doc/en/param/lock_timeout/) setting.\\\\n\\\\n\\\\n\\\\n \\\\n \\\\n To get started, generate a [new migration](/docs/reference/cli/supabase-migration-new) to store the SQL needed to create our `employees` table.\\\\n \\\\n\\\\n \\\\n \\\\n ```bash name=Terminal\\\\n supabase migration new create_employees_table\\\\n ```\\\\n \\\\n \\\\n \\\\n\\\\n\\\\n\\\\n \\\\n \\\\n This creates a new migration file in supabase/migrations directory.\\\\n\\\\n To that file, add the SQL to create this `employees` table.\\\\n \\\\n\\\\n \\\\n _create_employees_table.sql\\\\\\\">\\\\n ```sql name=supabase/migrations/_create_employees_table.sql\\\\n create table if not exists employees (\\\\n id bigint primary key generated always as identity,\\\\n name text not null,\\\\n email text,\\\\n created_at timestamptz default now()\\\\n );\\\\n ```\\\\n \\\\n \\\\n \\\\n\\\\n\\\\n\\\\n \\\\n \\\\n Run this migration to create the `employees` table.\\\\n\\\\n Now you can visit your new `employees` table in the local Dashboard.\\\\n \\\\n\\\\n \\\\n \\\\n ```bash name=Terminal\\\\n supabase migration up\\\\n ```\\\\n \\\\n \\\\n \\\\n\\\\n\\\\n\\\\n \\\\n \\\\n Next, modify your `employees` table by adding a column for `department`.\\\\n \\\\n\\\\n \\\\n \\\\n ```bash name=Terminal\\\\n supabase migration new add_department_column\\\\n ```\\\\n \\\\n \\\\n \\\\n\\\\n\\\\n\\\\n \\\\n \\\\n To that new migration file, add the SQL to create a new `department` column.\\\\n \\\\n\\\\n \\\\n _add_department_column.sql\\\\\\\">\\\\n ```sql name=supabase/migrations/_add_department_column.sql\\\\n alter table if exists public.employees\\\\n add department text default 'Hooli';\\\\n ```\\\\n \\\\n \\\\n \\\\n\\\\n\\\\n\\\\n \\\\n \\\\n Run this migration to update your existing `employees` table.\\\\n \\\\n\\\\n \\\\n \\\\n ```bash name=Terminal\\\\n supabase migration up\\\\n ```\\\\n \\\\n \\\\n \\\\n\\\\n\\\\nFinally, you should see the `department` column added to your `employees` table in the local Dashboard.\\\\n\\\\n\\\\n View the [complete code](https://github.com/supabase/supabase/tree/master/examples/database/employees) for this example on GitHub.\\\\n\\\\n\\\\n\\\\n### Seeding data\\\\n\\\\nNow that you are managing your database with migrations, it would be great have some seed data to use every time you reset the database.\\\\n\\\\n\\\\n \\\\n \\\\n Create a seed script in supabase/seed.sql.\\\\n\\\\n To that file, add the SQL to insert data into your `employees` table.\\\\n \\\\n\\\\n \\\\n \\\\n ```sql name=supabase/seed.sql\\\\n insert into public.employees\\\\n (name)\\\\n values\\\\n ('Erlich Bachman'),\\\\n ('Richard Hendricks'),\\\\n ('Monica Hall');\\\\n ```\\\\n \\\\n \\\\n \\\\n\\\\n\\\\n\\\\n \\\\n \\\\n Reset your database to reapply migrations and populate with seed data.\\\\n \\\\n\\\\n \\\\n \\\\n ```bash name=Terminal\\\\n supabase db reset\\\\n ```\\\\n \\\\n \\\\n \\\\n\\\\n\\\\nYou should now see the `employees` table, along with your seed data in the Dashboard! All of your database changes are captured in code, and you can reset to a known state at any time, complete with seed data.\\\\n\\\\n\\\\n### Diffing changes\\\\n\\\\nThis workflow is great if you know SQL and are comfortable creating tables and columns. If not, you can still use the Dashboard to create tables and columns, and then use the CLI to diff your changes and create migrations.\\\\n\\\\n\\\\n Only use the Dashboard to make schema changes on your **local** database, then capture them with `supabase db diff`. Making schema changes directly on your **remote** database (via the SQL editor or Table Editor) bypasses the migration history and will cause `db push` to fail with sync errors. Once you're using migrations, all schema changes to your remote database should go through migration files only.\\\\n\\\\n\\\\n\\\\n \\\\n \\\\n Create a new table called `cities`, with columns `id`, `name` and `population`.\\\\n\\\\n Then generate a [schema diff](/docs/reference/cli/supabase-db-diff).\\\\n \\\\n\\\\n \\\\n \\\\n ```bash name=Terminal\\\\n supabase db diff -f create_cities_table\\\\n ```\\\\n \\\\n \\\\n \\\\n\\\\n\\\\n\\\\n \\\\n \\\\n A new migration file is created for you.\\\\n\\\\n Alternately, you can copy the table definitions directly from the Table Editor.\\\\n \\\\n\\\\n \\\\n _create_cities_table.sql\\\\\\\">\\\\n ```sql name=supabase/migrations/_create_cities_table.sql\\\\n create table \\\\\\\"public\\\\\\\".\\\\\\\"cities\\\\\\\" (\\\\n \\\\\\\"id\\\\\\\" bigint primary key generated always as identity,\\\\n \\\\\\\"name\\\\\\\" text,\\\\n \\\\\\\"population\\\\\\\" bigint\\\\n );\\\\n ```\\\\n \\\\n \\\\n \\\\n\\\\n\\\\n\\\\n \\\\n \\\\n Test your new migration file by resetting your local database.\\\\n \\\\n\\\\n \\\\n \\\\n ```bash name=Terminal\\\\n supabase db reset\\\\n ```\\\\n \\\\n \\\\n \\\\n\\\\n\\\\nThe last step is deploying these changes to a live Supabase project.\\\\n\\\\n\\\\n## Deploy your project\\\\n\\\\nYou've been developing your project locally, making changes to your tables via migrations. It's time to deploy your project to the Supabase Platform and start scaling up to millions of users!\\\\n\\\\nHead over to [Supabase](/dashboard) and create a new project to deploy to.\\\\n\\\\n\\\\n \\\\n \\\\n [Login](/docs/reference/cli/supabase-login) to the Supabase CLI using an auto-generated Personal Access Token.\\\\n \\\\n\\\\n \\\\n \\\\n ```bash name=Terminal\\\\n supabase login\\\\n ```\\\\n \\\\n \\\\n \\\\n\\\\n\\\\n\\\\n \\\\n \\\\n [Link](/docs/reference/cli/supabase-link) to your remote project by selecting from the on-screen prompt.\\\\n \\\\n\\\\n \\\\n \\\\n ```bash name=Terminal\\\\n supabase link\\\\n ```\\\\n \\\\n \\\\n \\\\n\\\\n\\\\n\\\\n \\\\n \\\\n [Push](/docs/reference/cli/supabase-db-push) your migrations to the remote database.\\\\n \\\\n\\\\n \\\\n \\\\n ```bash name=Terminal\\\\n supabase db push\\\\n ```\\\\n \\\\n \\\\n \\\\n\\\\n\\\\n\\\\n \\\\n \\\\n [Push](/docs/reference/cli/supabase-db-push) your migrations and seed the remote database.\\\\n \\\\n\\\\n \\\\n \\\\n ```bash name=Terminal\\\\n supabase db push --include-seed\\\\n ```\\\\n \\\\n \\\\n \\\\n\\\\n\\\\nVisiting your live project on [Supabase](/dashboard/project/_), you'll see a new `employees` table, complete with the `department` column you added in the second migration above.\\\\n\\\\n\\\\n## Working with a team\\\\n\\\\nWhen multiple developers share a Supabase project, a few rules keep migrations from getting out of sync.\\\\n\\\\n**The golden rule: never change the remote database directly.** Once you're using migrations, all schema changes — even small ones — should go through migration files. Using the Dashboard's SQL editor or Table Editor on your remote database bypasses the migration history, and `db push` will start failing with sync errors.\\\\n\\\\n**The team workflow:**\\\\n\\\\n\\\\n \\\\n \\\\n Each developer creates migration files on their own branch, never touching the remote database directly.\\\\n \\\\n\\\\n \\\\n \\\\n ```bash name=Terminal\\\\n supabase migration new your_change_description\\\\n ```\\\\n \\\\n \\\\n \\\\n\\\\n\\\\n\\\\n \\\\n \\\\n Reset your local database to apply the migration, then commit the migration file to git.\\\\n \\\\n\\\\n \\\\n \\\\n ```bash name=Terminal\\\\n supabase db reset\\\\n git add supabase/migrations\\\\n git commit -m \\\\\\\"add migration: your_change_description\\\\\\\"\\\\n ```\\\\n \\\\n \\\\n \\\\n\\\\n\\\\n\\\\n \\\\n \\\\n After pulling new migration files from git, reset your local database to apply them.\\\\n \\\\n\\\\n \\\\n \\\\n ```bash name=Terminal\\\\n git pull\\\\n supabase db reset\\\\n ```\\\\n \\\\n \\\\n \\\\n\\\\n\\\\n\\\\n \\\\n \\\\n Coordinate so only one person runs `db push` at a time. Migration files are applied in timestamp order, so concurrent pushes from different machines can cause conflicts.\\\\n \\\\n\\\\n \\\\n \\\\n ```bash name=Terminal\\\\n supabase db push\\\\n ```\\\\n \\\\n \\\\n \\\\n\\\\n\\\\n\\\\n For a more automated deployment approach, consider using [Supabase Branching](/docs/guides/deployment/branching) or a CI/CD pipeline that runs `supabase db push` on merge to your main branch.\\\\n\\\\n\\\\n\\\\n## Diagnosing and fixing sync errors\\\\n\\\\nIf `db push` fails with errors suggesting you run `supabase migration repair`, your local migration files and the remote database's migration history are out of sync. Here's how to diagnose and fix it.\\\\n\\\\n\\\\n### How migration tracking works\\\\n\\\\nSupabase tracks which migrations have been applied on each database in a table called `supabase_migrations.schema_migrations`. When you run `supabase db push`, it compares your local `supabase/migrations` folder against that table and runs only the ones not yet applied, in order.\\\\n\\\\nGit tracks your migration *files*. Supabase tracks what's been *applied to each database*. These are two separate systems that need to stay in sync.\\\\n\\\\n\\\\n### Step 1: Check what's out of sync\\\\n\\\\nStart by listing the migration status across local and remote:\\\\n\\\\n\\\\n ```bash name=Terminal\\\\n supabase migration list\\\\n ```\\\\n\\\\n\\\\nThis shows which migrations are applied locally, which are applied on the remote, and where they diverge.\\\\n\\\\n\\\\n### Step 2: If you made changes on the remote database directly\\\\n\\\\nPull the current remote state into a migration file to get back in sync:\\\\n\\\\n\\\\n ```bash name=Terminal\\\\n supabase db pull\\\\n ```\\\\n\\\\n\\\\nThis creates a new migration file capturing the current remote schema. Commit it to git, then follow the standard workflow going forward.\\\\n\\\\n\\\\n### Step 3: If the migration history table is wrong\\\\n\\\\nIf a migration shows as missing in the remote history table but the schema change is actually already there (for example, it was applied manually), you can mark it as applied without re-running it:\\\\n\\\\n\\\\n ```bash name=Terminal\\\\n supabase migration repair --status applied \\\\n ```\\\\n\\\\n\\\\nOr if a migration is recorded as applied but was never actually run:\\\\n\\\\n\\\\n ```bash name=Terminal\\\\n supabase migration repair --status reverted \\\\n ```\\\\n\\\\n\\\\n\\\\n `migration repair` updates the tracking table only — it does not apply or revert any SQL. Use it to correct the history record when you know the actual database state is correct.\\\\n\\\\n\\\"},{\\\"__typename\\\":\\\"Guide\\\",\\\"title\\\":\\\"Build a User Management App with RedwoodJS\\\",\\\"href\\\":\\\"https://supabase.com/docs/guides/getting-started/tutorials/with-redwoodjs\\\",\\\"content\\\":\\\"# Build a User Management App with RedwoodJS\\\\n\\\\n\\\\n\\\\nThis tutorial demonstrates how to build a basic user management app. The app authenticates and identifies the user, stores their profile information in the database, and allows the user to log in, update their profile details, and upload a profile photo. The app uses:\\\\n\\\\n* [Supabase Database](/docs/guides/database) - a Postgres database for storing your user data and [Row Level Security](/docs/guides/auth#row-level-security) so data is protected and users can only access their own information.\\\\n* [Supabase Auth](/docs/guides/auth) - allow users to sign up and log in.\\\\n* [Supabase Storage](/docs/guides/storage) - allow users to upload a profile photo.\\\\n\\\\n![Supabase User Management example](/docs/img/user-management-demo.png)\\\\n\\\\n\\\\n If you get stuck while working through this guide, you can find the [full example on GitHub](https://github.com/redwoodjs/redwoodjs-supabase-quickstart).\\\\n\\\\n\\\\n\\\\n## About RedwoodJS\\\\n\\\\nA Redwood application is split into two parts: a frontend and a backend. This is represented as two node projects within a single monorepo.\\\\n\\\\nThe frontend project is called **`web`** and the backend project is called **`api`**. For clarity, we will refer to these in prose as **\\\\\\\"sides,\\\\\\\"** that is, the `web side` and the `api side`.\\\\nThey are separate projects because code on the `web side` will end up running in the user's browser while code on the `api side` will run on a server somewhere.\\\\n\\\\n\\\\n Important: When this guide refers to \\\\\\\"API,\\\\\\\" that means the Supabase API and when it refers to `api side`, that means the RedwoodJS `api side`.\\\\n\\\\n\\\\nThe **`api side`** is an implementation of a GraphQL API. The business logic is organized into \\\\\\\"services\\\\\\\" that represent their own internal API and can be called both from external GraphQL requests and other internal services.\\\\n\\\\nThe **`web side`** is built with React. Redwood's router makes it simple to map URL paths to React \\\\\\\"Page\\\\\\\" components (and automatically code-split your app on each route).\\\\nPages may contain a \\\\\\\"Layout\\\\\\\" component to wrap content. They also contain \\\\\\\"Cells\\\\\\\" and regular React components.\\\\nCells allow you to declaratively manage the lifecycle of a component that fetches and displays data.\\\\n\\\\nFor the sake of consistency with the other framework tutorials, we'll build this app a little differently than normal.\\\\nWe ***won't use*** Prisma to connect to the Supabase Postgres database or [Prisma migrations](https://redwoodjs.com/docs/cli-commands#prisma-migrate) as one typically might in a Redwood app.\\\\nInstead, we'll rely on the Supabase client to do some of the work on the **`web`** side and use the client again on the **`api`** side to do data fetching as well.\\\\n\\\\nThat means you will want to refrain from running any `yarn rw prisma migrate` commands and also double check your build commands on deployment to ensure Prisma won't reset your database. Prisma currently doesn't support cross-schema foreign keys, so introspecting the schema fails due\\\\nto how your Supabase `public` schema references the `auth.users`.\\\\n\\\\n\\\\n## Project setup\\\\n\\\\nBefore you start building you need to set up the Database and API. You can do this by starting a new Project in Supabase and then creating a \\\\\\\"schema\\\\\\\" inside the database.\\\\n\\\\n\\\\n### Create a project\\\\n\\\\n1. [Create a new project](/dashboard) in the Supabase Dashboard.\\\\n2. Enter your project details.\\\\n3. Wait for the new database to launch.\\\\n\\\\n\\\\n### Set up the database schema\\\\n\\\\nNow set up the database schema. You can use the \\\\\\\"User Management Starter\\\\\\\" quickstart in the SQL Editor, or you can copy/paste the SQL from below and run it.\\\\n\\\\n\\\\n \\\\n 1. Go to the [SQL Editor](/dashboard/project/_/sql) page in the Dashboard.\\\\n 2. Click **User Management Starter** under the **Community > Quickstarts** tab.\\\\n 3. Click **Run**.\\\\n\\\\n \\\\n You can pull the database schema down to your local project by running the `db pull` command. Read the [local development docs](/docs/guides/cli/local-development#link-your-project) for detailed instructions.\\\\n\\\\n ```bash\\\\n supabase link --project-ref \\\\n # You can get from your project's dashboard URL: https://supabase.com/dashboard/project/\\\\n supabase db pull\\\\n ```\\\\n \\\\n \\\\n\\\\n \\\\n \\\\n When working locally you can run the following command to create a new migration file:\\\\n \\\\n\\\\n ```bash\\\\n supabase migration new user_management_starter\\\\n ```\\\\n\\\\n ```sql\\\\n -- Create a table for public profiles\\\\n create table profiles (\\\\n id uuid references auth.users not null primary key,\\\\n updated_at timestamp with time zone,\\\\n username text unique,\\\\n full_name text,\\\\n avatar_url text,\\\\n website text,\\\\n\\\\n constraint username_length check (char_length(username) >= 3)\\\\n );\\\\n\\\\n -- Grant the privileges roles need\\\\n GRANT SELECT ON public.profiles TO anon;\\\\n GRANT SELECT, INSERT, UPDATE ON public.profiles TO authenticated;\\\\n\\\\n -- Set up Row Level Security (RLS)\\\\n -- See https://supabase.com/docs/guides/database/postgres/row-level-security for more details.\\\\n alter table profiles\\\\n enable row level security;\\\\n\\\\n create policy \\\\\\\"Public profiles are viewable by everyone.\\\\\\\" on profiles\\\\n for select using (true);\\\\n\\\\n create policy \\\\\\\"Users can insert their own profile.\\\\\\\" on profiles\\\\n for insert with check ((select auth.uid()) = id);\\\\n\\\\n create policy \\\\\\\"Users can update own profile.\\\\\\\" on profiles\\\\n for update using ((select auth.uid()) = id);\\\\n\\\\n -- This trigger automatically creates a profile entry when a new user signs up via Supabase Auth.\\\\n -- See https://supabase.com/docs/guides/auth/managing-user-data#using-triggers for more details.\\\\n create function public.handle_new_user()\\\\n returns trigger\\\\n set search_path = ''\\\\n as $$\\\\n begin\\\\n insert into public.profiles (id, full_name, avatar_url)\\\\n values (new.id, new.raw_user_meta_data->>'full_name', new.raw_user_meta_data->>'avatar_url');\\\\n return new;\\\\n end;\\\\n $$ language plpgsql security definer;\\\\n create trigger on_auth_user_created\\\\n after insert on auth.users\\\\n for each row execute procedure public.handle_new_user();\\\\n\\\\n -- Set up Storage!\\\\n insert into storage.buckets (id, name)\\\\n values ('avatars', 'avatars');\\\\n\\\\n -- Set up access controls for storage. Allows downloading object with public key\\\\n -- See https://supabase.com/docs/guides/storage/security/access-control#policy-examples for more details.\\\\n create policy \\\\\\\"Avatar images are publicly accessible.\\\\\\\" on storage.objects\\\\n for select using (bucket_id = 'avatars' and storage.allow_any_operation(array['object.get_authenticated_info', 'object.get_authenticated']));\\\\n\\\\n create policy \\\\\\\"Anyone can upload an avatar.\\\\\\\" on storage.objects\\\\n for insert with check (bucket_id = 'avatars');\\\\n\\\\n create policy \\\\\\\"Anyone can update their own avatar.\\\\\\\" on storage.objects\\\\n for update using ((select auth.uid()) = owner) with check (bucket_id = 'avatars');\\\\n ```\\\\n \\\\n\\\\n\\\\n\\\\n### Get API details\\\\n\\\\nNow that you've created some database tables, you are ready to insert data using the auto-generated API.\\\\n\\\\nTo do this, you need to get the Project URL and key from [the project **Connect** dialog](/dashboard/project/_?showConnect=true\\\\\\\\&connectTab=frameworks\\\\\\\\&framework=).\\\\n\\\\n[Read the API keys docs](/docs/guides/getting-started/api-keys) for a full explanation of all key types and their uses.\\\\n\\\\n\\\\n Supabase is changing the way keys work to improve project security and developer experience. You can [read the full announcement on GitHub](https://github.com/orgs/supabase/discussions/29260).\\\\n\\\\n The older `anon` and `service_role` keys will work until the end of 2026 but **we strongly encourage switching to and using** the new publishable (`sb_publishable_xxx`) and secret (`sb_secret_xxx`) keys now.\\\\n\\\\n In most cases, you can get keys from [the Project's **Connect** dialog](/dashboard/project/_?showConnect=true\\\\\\\\&connectTab=frameworks\\\\\\\\&framework=), but if you want a specific key, you can find them in the [**Settings > API Keys**](/dashboard/project/_/settings/api-keys/) section of the Dashboard.\\\\n\\\\n * **For legacy keys**, copy the `anon` key for client-side operations and the `service_role` key for server-side operations from the **Legacy API Keys** tab.\\\\n * **For new keys**, open the **API Keys** tab, if you don't have a publishable key already, click **Create new API Keys**, and copy the value from the **Publishable key** section.\\\\n\\\\n\\\\n\\\\n## Building the app\\\\n\\\\nLet's start building the RedwoodJS app from scratch.\\\\n\\\\n\\\\n RedwoodJS requires Node.js `>= 14.x <= 16.x` and Yarn `>= 1.15`.\\\\n\\\\n\\\\nMake sure you have installed yarn since RedwoodJS relies on it to [manage its packages in workspaces](https://classic.yarnpkg.com/lang/en/docs/workspaces/) for its `web` and `api` \\\\\\\"sides.\\\\\\\"\\\\n\\\\n\\\\n### Initialize a RedwoodJS app\\\\n\\\\nWe can use [Create Redwood App](https://redwoodjs.com/docs/quick-start) command to initialize\\\\nan app called `supabase-redwoodjs`:\\\\n\\\\n```bash\\\\nyarn create redwood-app supabase-redwoodjs\\\\ncd supabase-redwoodjs\\\\n```\\\\n\\\\nWhile the app is installing, you should see:\\\\n\\\\n```bash\\\\n✔ Creating Redwood app\\\\n ✔ Checking node and yarn compatibility\\\\n ✔ Creating directory 'supabase-redwoodjs'\\\\n✔ Installing packages\\\\n ✔ Running 'yarn install'... (This could take a while)\\\\n✔ Convert TypeScript files to JavaScript\\\\n✔ Generating types\\\\n\\\\nThanks for trying out Redwood!\\\\n```\\\\n\\\\nThen let's install the only additional dependency [supabase-js](https://github.com/supabase/supabase-js) by running the `setup auth` command:\\\\n\\\\n```bash\\\\nyarn redwood setup auth supabase\\\\n```\\\\n\\\\nWhen prompted:\\\\n\\\\n> Overwrite existing /api/src/lib/auth.\\\\\\\\[jt]s?\\\\n\\\\nSay, **yes** and it will setup the Supabase client in your app and also provide hooks used with Supabase authentication.\\\\n\\\\n```bash\\\\n✔ Generating auth lib...\\\\n ✔ Successfully wrote file `./api/src/lib/auth.js`\\\\n ✔ Adding auth config to web...\\\\n ✔ Adding auth config to GraphQL API...\\\\n ✔ Adding required web packages...\\\\n ✔ Installing packages...\\\\n ✔ One more thing...\\\\n\\\\n You will need to add your Supabase URL (SUPABASE_URL), public API KEY,\\\\n and JWT SECRET (SUPABASE_KEY, and SUPABASE_JWT_SECRET) to your .env file.\\\\n```\\\\n\\\\nNext, we want to save the environment variables in a `.env`.\\\\nWe need the `API URL` as well as the key and `jwt_secret` that you copied [earlier](#get-api-details).\\\\n\\\\n\\\\n \\\\n ```bash name=.env\\\\n SUPABASE_URL=YOUR_SUPABASE_URL\\\\n SUPABASE_KEY=YOUR_SUPABASE_PUBLISHABLE_KEY\\\\n SUPABASE_JWT_SECRET=YOUR_SUPABASE_JWT_SECRET\\\\n ```\\\\n \\\\n\\\\n\\\\nAnd finally, you will also need to save **just** the `web side` environment variables to the `redwood.toml`.\\\\n\\\\n\\\\n \\\\n ```bash name=redwood.toml\\\\n [web]\\\\n title = \\\\\\\"Supabase Redwood Tutorial\\\\\\\"\\\\n port = 8910\\\\n apiProxyPath = \\\\\\\"/.redwood/functions\\\\\\\"\\\\n includeEnvironmentVariables = [\\\\\\\"SUPABASE_URL\\\\\\\", \\\\\\\"SUPABASE_KEY\\\\\\\"]\\\\n [api]\\\\n port = 8911\\\\n [browser]\\\\n open = true\\\\n ```\\\\n \\\\n\\\\n\\\\nThese variables will be exposed on the browser, and that's completely fine.\\\\nThey allow your web app to initialize the Supabase client with your publishable key\\\\nsince we have [Row Level Security](/docs/guides/auth#row-level-security) enabled on our Database.\\\\n\\\\nYou'll see these being used to configure your Supabase client in `web/src/App.js`:\\\\n\\\\n\\\\n \\\\n ```js name=web/src/App.js\\\\n // ... Redwood imports\\\\n import { AuthProvider } from '@redwoodjs/auth'\\\\n import { createClient } from '@supabase/supabase-js'\\\\n\\\\n // ...\\\\n\\\\n const supabase = createClient(process.env.SUPABASE_URL, process.env.SUPABASE_KEY)\\\\n\\\\n const App = () => (\\\\n \\\\n \\\\n \\\\n \\\\n \\\\n \\\\n \\\\n \\\\n \\\\n )\\\\n\\\\n export default App\\\\n ```\\\\n \\\\n\\\\n\\\\n\\\\n### App styling (optional)\\\\n\\\\nAn optional step is to update the CSS file `web/src/index.css` to make the app look better.\\\\nYou can find the full contents of this file [in the example repository](https://raw.githubusercontent.com/supabase/supabase/master/examples/user-management/react-user-management/src/index.css).\\\\n\\\\n\\\\n### Start RedwoodJS and your first page\\\\n\\\\nLet's test our setup at the moment by starting up the app:\\\\n\\\\n```bash\\\\nyarn rw dev\\\\n```\\\\n\\\\n\\\\n `rw` is an alias for `redwood`, as in `yarn rw` to run Redwood CLI commands.\\\\n\\\\n\\\\nYou should see a \\\\\\\"Welcome to RedwoodJS\\\\\\\" page and a message about not having any pages yet.\\\\n\\\\nSo, let's create a \\\\\\\"home\\\\\\\" page:\\\\n\\\\n```bash\\\\nyarn rw generate page home /\\\\n\\\\n✔ Generating page files...\\\\n ✔ Successfully wrote file `./web/src/pages/HomePage/HomePage.stories.js`\\\\n ✔ Successfully wrote file `./web/src/pages/HomePage/HomePage.test.js`\\\\n ✔ Successfully wrote file `./web/src/pages/HomePage/HomePage.js`\\\\n✔ Updating routes file...\\\\n✔ Generating types ...\\\\n```\\\\n\\\\n\\\\n The `/` is important here as it creates a root level route.\\\\n\\\\n\\\\nYou can stop the `dev` server if you want; to see your changes, just be sure to run `yarn rw dev` again.\\\\n\\\\nYou should see the `Home` page route in `web/src/Routes.js`:\\\\n\\\\n\\\\n \\\\n ```bash name=web/src/Routes.js\\\\n import { Router, Route } from '@redwoodjs/router'\\\\n\\\\n const Routes = () => {\\\\n return (\\\\n \\\\n \\\\n \\\\n \\\\n )\\\\n }\\\\n\\\\n export default Routes\\\\n ```\\\\n \\\\n\\\\n\\\\n\\\\n### Set up a login component\\\\n\\\\nLet's set up a Redwood component to manage logins and sign ups. We'll use Magic Links, so users can sign in with their email without using passwords.\\\\n\\\\n```bash\\\\nyarn rw g component auth\\\\n\\\\n ✔ Generating component files...\\\\n ✔ Successfully wrote file `./web/src/components/Auth/Auth.test.js`\\\\n ✔ Successfully wrote file `./web/src/components/Auth/Auth.stories.js`\\\\n ✔ Successfully wrote file `./web/src/components/Auth/Auth.js`\\\\n\\\\n```\\\\n\\\\nNow, update the `Auth.js` component to contain:\\\\n\\\\n\\\\n \\\\n ```jsx name=/web/src/components/Auth/Auth.js\\\\n import { useState } from 'react'\\\\n import { useAuth } from '@redwoodjs/auth'\\\\n\\\\n const Auth = () => {\\\\n const { logIn } = useAuth()\\\\n const [loading, setLoading] = useState(false)\\\\n const [email, setEmail] = useState('')\\\\n\\\\n const handleLogin = async (email) => {\\\\n try {\\\\n setLoading(true)\\\\n const { error } = await logIn({ email })\\\\n if (error) throw error\\\\n alert('Check your email for the login link!')\\\\n } catch (error) {\\\\n alert(error.error_description || error.message)\\\\n } finally {\\\\n setLoading(false)\\\\n }\\\\n }\\\\n\\\\n return (\\\\n
    \\\\n
    \\\\n

    Supabase + RedwoodJS

    \\\\n

    Sign in via magic link with your email below

    \\\\n
    \\\\n setEmail(e.target.value)}\\\\n />\\\\n
    \\\\n
    \\\\n {\\\\n e.preventDefault()\\\\n handleLogin(email)\\\\n }}\\\\n className={'button block'}\\\\n disabled={loading}\\\\n >\\\\n {loading ? Loading : Send magic link}\\\\n \\\\n
    \\\\n
    \\\\n
    \\\\n )\\\\n }\\\\n\\\\n export default Auth\\\\n ```\\\\n
    \\\\n
    \\\\n\\\\n\\\\n### Set up an account component\\\\n\\\\nAfter a user is signed in we can allow them to edit their profile details and manage their account.\\\\n\\\\nLet's create a new component for that called `Account.js`.\\\\n\\\\n```bash\\\\nyarn rw g component account\\\\n\\\\n ✔ Generating component files...\\\\n ✔ Successfully wrote file `./web/src/components/Account/Account.test.js`\\\\n ✔ Successfully wrote file `./web/src/components/Account/Account.stories.js`\\\\n ✔ Successfully wrote file `./web/src/components/Account/Account.js`\\\\n```\\\\n\\\\nAnd then update the file to contain:\\\\n\\\\n\\\\n \\\\n ```jsx name=web/src/components/Account/Account.js\\\\n import { useState, useEffect } from 'react'\\\\n import { useAuth } from '@redwoodjs/auth'\\\\n\\\\n const Account = () => {\\\\n const { client: supabase, currentUser, logOut } = useAuth()\\\\n const [loading, setLoading] = useState(true)\\\\n const [username, setUsername] = useState(null)\\\\n const [website, setWebsite] = useState(null)\\\\n const [avatar_url, setAvatarUrl] = useState(null)\\\\n\\\\n useEffect(() => {\\\\n getProfile()\\\\n }, [supabase.auth.session])\\\\n\\\\n async function getProfile() {\\\\n try {\\\\n setLoading(true)\\\\n const user = supabase.auth.user()\\\\n\\\\n const { data, error, status } = await supabase\\\\n .from('profiles')\\\\n .select(`username, website, avatar_url`)\\\\n .eq('id', user.id)\\\\n .single()\\\\n\\\\n if (error && status !== 406) {\\\\n throw error\\\\n }\\\\n\\\\n if (data) {\\\\n setUsername(data.username)\\\\n setWebsite(data.website)\\\\n setAvatarUrl(data.avatar_url)\\\\n }\\\\n } catch (error) {\\\\n alert(error.message)\\\\n } finally {\\\\n setLoading(false)\\\\n }\\\\n }\\\\n\\\\n async function updateProfile({ username, website, avatar_url }) {\\\\n try {\\\\n setLoading(true)\\\\n const user = supabase.auth.user()\\\\n\\\\n const updates = {\\\\n id: user.id,\\\\n username,\\\\n website,\\\\n avatar_url,\\\\n updated_at: new Date(),\\\\n }\\\\n\\\\n const { error } = await supabase.from('profiles').upsert(updates, {\\\\n returning: 'minimal', // Don't return the value after inserting\\\\n })\\\\n\\\\n if (error) {\\\\n throw error\\\\n }\\\\n\\\\n alert('Updated profile!')\\\\n } catch (error) {\\\\n alert(error.message)\\\\n } finally {\\\\n setLoading(false)\\\\n }\\\\n }\\\\n\\\\n return (\\\\n
    \\\\n
    \\\\n

    Supabase + RedwoodJS

    \\\\n

    Your profile

    \\\\n
    \\\\n
    \\\\n \\\\n \\\\n
    \\\\n
    \\\\n \\\\n setUsername(e.target.value)}\\\\n />\\\\n
    \\\\n
    \\\\n \\\\n setWebsite(e.target.value)}\\\\n />\\\\n
    \\\\n\\\\n
    \\\\n updateProfile({ username, website, avatar_url })}\\\\n disabled={loading}\\\\n >\\\\n {loading ? 'Loading ...' : 'Update'}\\\\n \\\\n
    \\\\n\\\\n
    \\\\n \\\\n
    \\\\n
    \\\\n
    \\\\n
    \\\\n )\\\\n }\\\\n\\\\n export default Account\\\\n ```\\\\n
    \\\\n
    \\\\n\\\\nYou'll see the use of `useAuth()` several times. Redwood's `useAuth` hook provides convenient ways to access\\\\n`logIn`, `logOut`, `currentUser`, and access the `supabase` authenticate client. We'll use it to get an instance\\\\nof the Supabase client to interact with your API.\\\\n\\\\n\\\\n### Update home page\\\\n\\\\nWith all the components in place, update your `HomePage` page to use them:\\\\n\\\\n\\\\n \\\\n ```jsx name=web/src/pages/HomePage/HomePage.js\\\\n import { useAuth } from '@redwoodjs/auth'\\\\n import { MetaTags } from '@redwoodjs/web'\\\\n\\\\n import Account from 'src/components/Account'\\\\n import Auth from 'src/components/Auth'\\\\n\\\\n const HomePage = () => {\\\\n const { isAuthenticated } = useAuth()\\\\n\\\\n return (\\\\n <>\\\\n \\\\n {!isAuthenticated ? : }\\\\n \\\\n )\\\\n }\\\\n\\\\n export default HomePage\\\\n ```\\\\n \\\\n\\\\n\\\\n\\\\n What we're doing here is showing the sign in form if you aren't logged in and your account profile if you are.\\\\n\\\\n\\\\n\\\\n## Profile photos\\\\n\\\\nNext, add a way for users to upload a profile photo. Supabase configures every project with [Storage](/docs/guides/storage) for managing large files like photos and videos.\\\\n\\\\n\\\\n### Create an upload widget\\\\n\\\\nLet's create an avatar for the user so that they can upload a profile photo. We can start by creating a new component:\\\\n\\\\n```bash\\\\nyarn rw g component avatar\\\\n ✔ Generating component files...\\\\n ✔ Successfully wrote file `./web/src/components/Avatar/Avatar.test.js`\\\\n ✔ Successfully wrote file `./web/src/components/Avatar/Avatar.stories.js`\\\\n ✔ Successfully wrote file `./web/src/components/Avatar/Avatar.js`\\\\n```\\\\n\\\\nNow, update your Avatar component to contain the following widget:\\\\n\\\\n\\\\n \\\\n ```jsx name=web/src/components/Avatar/Avatar.js\\\\n import { useEffect, useState } from 'react'\\\\n import { useAuth } from '@redwoodjs/auth'\\\\n\\\\n const Avatar = ({ url, size, onUpload }) => {\\\\n const { client: supabase } = useAuth()\\\\n\\\\n const [avatarUrl, setAvatarUrl] = useState(null)\\\\n const [uploading, setUploading] = useState(false)\\\\n\\\\n useEffect(() => {\\\\n if (url) downloadImage(url)\\\\n }, [url])\\\\n\\\\n async function downloadImage(path) {\\\\n try {\\\\n const { data, error } = await supabase.storage.from('avatars').download(path)\\\\n if (error) {\\\\n throw error\\\\n }\\\\n const url = URL.createObjectURL(data)\\\\n setAvatarUrl(url)\\\\n } catch (error) {\\\\n console.log('Error downloading image: ', error.message)\\\\n }\\\\n }\\\\n\\\\n async function uploadAvatar(event) {\\\\n try {\\\\n setUploading(true)\\\\n\\\\n if (!event.target.files || event.target.files.length === 0) {\\\\n throw new Error('You must select an image to upload.')\\\\n }\\\\n\\\\n const file = event.target.files[0]\\\\n const fileExt = file.name.split('.').pop()\\\\n const fileName = `${Math.random()}.${fileExt}`\\\\n const filePath = `${fileName}`\\\\n\\\\n const { error: uploadError } = await supabase.storage.from('avatars').upload(filePath, file)\\\\n\\\\n if (uploadError) {\\\\n throw uploadError\\\\n }\\\\n\\\\n onUpload(filePath)\\\\n } catch (error) {\\\\n alert(error.message)\\\\n } finally {\\\\n setUploading(false)\\\\n }\\\\n }\\\\n\\\\n return (\\\\n
    \\\\n {avatarUrl ? (\\\\n \\\\n ) : (\\\\n
    \\\\n )}\\\\n
    \\\\n \\\\n \\\\n
    \\\\n
    \\\\n )\\\\n }\\\\n\\\\n export default Avatar\\\\n ```\\\\n \\\\n\\\\n\\\\n\\\\n### Launch!\\\\n\\\\nOnce that's done, run this in a terminal window to launch the `dev` server:\\\\n\\\\n```bash\\\\nyarn rw dev\\\\n```\\\\n\\\\nAnd then open the browser to [localhost:8910](http://localhost:8910) and you should see the completed app.\\\\n\\\\n![Supabase RedwoodJS](/docs/img/supabase-redwoodjs-demo.png)\\\\n\\\\nAt this stage you have a fully functional application!\\\\n\\\\n\\\\n## See also\\\\n\\\\n* Learn more about [RedwoodJS](https://redwoodjs.com)\\\\n* Visit the [RedwoodJS Discourse Community](https://community.redwoodjs.com)\\\\n\\\"},{\\\"__typename\\\":\\\"Guide\\\",\\\"title\\\":\\\"Testing Overview\\\",\\\"href\\\":\\\"https://supabase.com/docs/guides/local-development/testing/overview\\\",\\\"content\\\":\\\"# Testing Overview\\\\n\\\\n\\\\n\\\\nTesting is a critical part of database development, especially when working with features like Row Level Security (RLS) policies. This guide provides a comprehensive approach to testing your Supabase database.\\\\n\\\\n\\\\n## Testing approaches\\\\n\\\\n\\\\n### Database unit testing with pgTAP\\\\n\\\\n[pgTAP](https://pgtap.org) is a unit testing framework for Postgres that allows testing:\\\\n\\\\n* Database structure: tables, columns, constraints\\\\n* Row Level Security (RLS) policies\\\\n* Functions and procedures\\\\n* Data integrity\\\\n\\\\nThis example demonstrates setting up and testing RLS policies for a simple todo application:\\\\n\\\\n1. Create a test table with RLS enabled:\\\\n\\\\n ```sql\\\\n -- Create a simple todos table\\\\n create table todos (\\\\n id uuid primary key default gen_random_uuid(),\\\\n task text not null,\\\\n user_id uuid references auth.users not null,\\\\n completed boolean default false\\\\n );\\\\n\\\\n -- Enable RLS\\\\n alter table todos enable row level security;\\\\n\\\\n -- Create a policy\\\\n create policy \\\\\\\"Users can only access their own todos\\\\\\\"\\\\n on todos for all -- this policy applies to all operations\\\\n to authenticated\\\\n using ((select auth.uid()) = user_id);\\\\n ```\\\\n\\\\n2. Set up your testing environment:\\\\n\\\\n ```bash\\\\n # Create a new test for our policies using supabase cli\\\\n supabase test new todos_rls.test\\\\n ```\\\\n\\\\n3. Write your RLS tests:\\\\n\\\\n ```sql\\\\n begin;\\\\n -- install tests utilities\\\\n -- install pgtap extension for testing\\\\n create extension if not exists pgtap with schema extensions;\\\\n -- Start declare we'll have 4 test cases in our test suite\\\\n select plan(4);\\\\n\\\\n -- Setup our testing data\\\\n -- Set up auth.users entries\\\\n insert into auth.users (id, email) values\\\\n \\\\t('123e4567-e89b-12d3-a456-426614174000', 'user1@test.com'),\\\\n \\\\t('987fcdeb-51a2-43d7-9012-345678901234', 'user2@test.com');\\\\n\\\\n -- Create test todos\\\\n insert into public.todos (task, user_id) values\\\\n \\\\t('User 1 Task 1', '123e4567-e89b-12d3-a456-426614174000'),\\\\n \\\\t('User 1 Task 2', '123e4567-e89b-12d3-a456-426614174000'),\\\\n \\\\t('User 2 Task 1', '987fcdeb-51a2-43d7-9012-345678901234');\\\\n\\\\n -- as User 1\\\\n set local role authenticated;\\\\n set local request.jwt.claim.sub = '123e4567-e89b-12d3-a456-426614174000';\\\\n\\\\n -- Test 1: User 1 should only see their own todos\\\\n select results_eq(\\\\n \\\\t'select count(*) from todos',\\\\n \\\\tARRAY[2::bigint],\\\\n \\\\t'User 1 should only see their 2 todos'\\\\n );\\\\n\\\\n -- Test 2: User 1 can create their own todo\\\\n select lives_ok(\\\\n \\\\t$$insert into todos (task, user_id) values ('New Task', '123e4567-e89b-12d3-a456-426614174000'::uuid)$$,\\\\n \\\\t'User 1 can create their own todo'\\\\n );\\\\n\\\\n -- as User 2\\\\n set local request.jwt.claim.sub = '987fcdeb-51a2-43d7-9012-345678901234';\\\\n\\\\n -- Test 3: User 2 should only see their own todos\\\\n select results_eq(\\\\n \\\\t'select count(*) from todos',\\\\n \\\\tARRAY[1::bigint],\\\\n \\\\t'User 2 should only see their 1 todo'\\\\n );\\\\n\\\\n -- Test 4: User 2 cannot modify User 1's todo\\\\n SELECT results_ne(\\\\n \\\\t$$ update todos set task = 'Hacked!' where user_id = '123e4567-e89b-12d3-a456-426614174000'::uuid returning 1 $$,\\\\n \\\\t$$ values(1) $$,\\\\n \\\\t'User 2 cannot modify User 1 todos'\\\\n );\\\\n\\\\n select * from finish();\\\\n rollback;\\\\n ```\\\\n\\\\n4. Run the tests:\\\\n\\\\n ```bash\\\\n supabase test db\\\\n psql:todos_rls.test.sql:4: NOTICE: extension \\\\\\\"pgtap\\\\\\\" already exists, skipping\\\\n ./todos_rls.test.sql .. ok\\\\n All tests successful.\\\\n Files=1, Tests=6, 0 wallclock secs ( 0.01 usr + 0.00 sys = 0.01 CPU)\\\\n Result: PASS\\\\n ```\\\\n\\\\n\\\\n### Application-Level testing\\\\n\\\\nTesting through application code provides end-to-end verification. Unlike database-level testing with pgTAP, application-level tests cannot use transactions for isolation.\\\\n\\\\n\\\\n Application-level tests should not rely on a clean database state, as resetting the database before each test can be slow and makes tests difficult to parallelize.\\\\n Instead, design your tests to be independent by using unique user IDs for each test case.\\\\n\\\\n\\\\nHere's an example using TypeScript that mirrors the pgTAP tests above:\\\\n\\\\n```typescript\\\\nimport crypto from 'crypto'\\\\nimport { createClient } from '@supabase/supabase-js'\\\\nimport { beforeAll, describe, expect, it } from 'vitest'\\\\n\\\\ndescribe('Todos RLS', () => {\\\\n // Generate unique IDs for this test suite to avoid conflicts with other tests\\\\n const USER_1_ID = crypto.randomUUID()\\\\n const USER_2_ID = crypto.randomUUID()\\\\n\\\\n const supabase = createClient(process.env.SUPABASE_URL!, process.env.SUPABASE_PUBLISHABLE_KEY!)\\\\n\\\\n beforeAll(async () => {\\\\n // Setup test data specific to this test suite\\\\n const adminSupabase = createClient(process.env.SUPABASE_URL!, process.env.SUPABASE_SECRET_KEY!)\\\\n\\\\n // Create test users with unique IDs\\\\n await adminSupabase.auth.admin.createUser({\\\\n id: USER_1_ID,\\\\n email: `user1-${USER_1_ID}@test.com`,\\\\n password: 'password123',\\\\n // We want the user to be usable right away without email confirmation\\\\n email_confirm: true,\\\\n })\\\\n await adminSupabase.auth.admin.createUser({\\\\n id: USER_2_ID,\\\\n email: `user2-${USER_2_ID}@test.com`,\\\\n password: 'password123',\\\\n email_confirm: true,\\\\n })\\\\n\\\\n // Create initial todos\\\\n await adminSupabase.from('todos').insert([\\\\n { task: 'User 1 Task 1', user_id: USER_1_ID },\\\\n { task: 'User 1 Task 2', user_id: USER_1_ID },\\\\n { task: 'User 2 Task 1', user_id: USER_2_ID },\\\\n ])\\\\n })\\\\n\\\\n it('should allow User 1 to only see their own todos', async () => {\\\\n // Sign in as User 1\\\\n await supabase.auth.signInWithPassword({\\\\n email: `user1-${USER_1_ID}@test.com`,\\\\n password: 'password123',\\\\n })\\\\n\\\\n const { data: todos } = await supabase.from('todos').select('*')\\\\n\\\\n expect(todos).toHaveLength(2)\\\\n todos?.forEach((todo) => {\\\\n expect(todo.user_id).toBe(USER_1_ID)\\\\n })\\\\n })\\\\n\\\\n it('should allow User 1 to create their own todo', async () => {\\\\n await supabase.auth.signInWithPassword({\\\\n email: `user1-${USER_1_ID}@test.com`,\\\\n password: 'password123',\\\\n })\\\\n\\\\n const { error } = await supabase.from('todos').insert({ task: 'New Task', user_id: USER_1_ID })\\\\n\\\\n expect(error).toBeNull()\\\\n })\\\\n\\\\n it('should allow User 2 to only see their own todos', async () => {\\\\n // Sign in as User 2\\\\n await supabase.auth.signInWithPassword({\\\\n email: `user2-${USER_2_ID}@test.com`,\\\\n password: 'password123',\\\\n })\\\\n\\\\n const { data: todos } = await supabase.from('todos').select('*')\\\\n expect(todos).toHaveLength(1)\\\\n todos?.forEach((todo) => {\\\\n expect(todo.user_id).toBe(USER_2_ID)\\\\n })\\\\n })\\\\n\\\\n it('should prevent User 2 from modifying User 1 todos', async () => {\\\\n await supabase.auth.signInWithPassword({\\\\n email: `user2-${USER_2_ID}@test.com`,\\\\n password: 'password123',\\\\n })\\\\n\\\\n // Attempt to update the todos we shouldn't have access to\\\\n // result will be a no-op\\\\n await supabase.from('todos').update({ task: 'Hacked!' }).eq('user_id', USER_1_ID)\\\\n\\\\n // Log back in as User 1 to verify their todos weren't changed\\\\n await supabase.auth.signInWithPassword({\\\\n email: `user1-${USER_1_ID}@test.com`,\\\\n password: 'password123',\\\\n })\\\\n\\\\n // Fetch User 1's todos\\\\n const { data: todos } = await supabase.from('todos').select('*')\\\\n\\\\n // Verify that none of the todos were changed to \\\\\\\"Hacked!\\\\\\\"\\\\n expect(todos).toBeDefined()\\\\n todos?.forEach((todo) => {\\\\n expect(todo.task).not.toBe('Hacked!')\\\\n })\\\\n })\\\\n})\\\\n```\\\\n\\\\n\\\\n#### Test isolation strategies\\\\n\\\\nFor application-level testing, consider these approaches for test isolation:\\\\n\\\\n1. **Unique Identifiers**: Generate unique IDs for each test suite to prevent data conflicts\\\\n2. **Cleanup After Tests**: If necessary, clean up created data in an `afterAll` or `afterEach` hook\\\\n3. **Isolated Data Sets**: Use prefixes or namespaces in data to separate test cases\\\\n\\\\n\\\\n### Continuous integration testing\\\\n\\\\nSet up automated database testing in your CI pipeline:\\\\n\\\\n1. Create a GitHub Actions workflow `.github/workflows/db-tests.yml`:\\\\n\\\\n```yaml\\\\nname: Database Tests\\\\n\\\\non:\\\\n push:\\\\n branches: [main]\\\\n pull_request:\\\\n branches: [main]\\\\n\\\\njobs:\\\\n test:\\\\n runs-on: ubuntu-latest\\\\n\\\\n steps:\\\\n - uses: actions/checkout@v4\\\\n\\\\n - name: Setup Supabase CLI\\\\n uses: supabase/setup-cli@v1\\\\n\\\\n - name: Start Supabase\\\\n run: supabase start\\\\n\\\\n - name: Run Tests\\\\n run: supabase test db\\\\n```\\\\n\\\\n\\\\n## Best practices\\\\n\\\\n1. **Test Data Setup**\\\\n * Use begin and rollback to ensure test isolation\\\\n * Create realistic test data that covers edge cases\\\\n * Use different user roles and permissions in tests\\\\n\\\\n2. **RLS Policy Testing**\\\\n * Test Create, Read, Update, Delete operations\\\\n * Test with different user roles: anonymous and authenticated\\\\n * Test edge cases and potential security bypasses\\\\n * Always test negative cases: what users should not be able to do\\\\n\\\\n3. **CI/CD Integration**\\\\n * Run tests automatically on every pull request\\\\n * Include database tests in deployment pipeline\\\\n * Keep test runs fast using transactions\\\\n\\\\n\\\\n## Real-World examples\\\\n\\\\nFor more complex, real-world examples of database testing, check out:\\\\n\\\\n* [Database Tests Example Repository](https://github.com/usebasejump/basejump/tree/main/supabase/tests/database) - A production-grade example of testing RLS policies\\\\n* [RLS Guide and Best Practices](https://github.com/orgs/supabase/discussions/14576)\\\\n\\\\n\\\\n## Troubleshooting\\\\n\\\\nCommon issues and solutions:\\\\n\\\\n1. **Test Failures Due to RLS**\\\\n * Ensure you've set the correct role `set local role authenticated;`\\\\n * Verify JWT claims are set `set local \\\\\\\"request.jwt.claims\\\\\\\"`\\\\n * Check policy definitions match your test assumptions\\\\n\\\\n2. **CI Pipeline Issues**\\\\n * Verify Supabase CLI is properly installed\\\\n * Ensure database migrations are run before tests\\\\n * Check for proper test isolation using transactions\\\\n\\\\n\\\\n## Additional resources\\\\n\\\\n* [pgTAP Documentation](https://pgtap.org)\\\\n* [Supabase CLI Reference](/docs/reference/cli/supabase-test)\\\\n* [pgTAP Supabase reference](/docs/guides/database/extensions/pgtap?queryGroups=database-method\\\\\\\\&database-method=sql#testing-rls-policies)\\\\n* [Database testing reference](/docs/guides/database/testing)\\\\n\\\"}]}}}\"\n }\n ],\n \"isError\": false\n}", + "input": "{\n \"graphql_query\": \"query { searchDocs(query: \\\"declarative database schemas generate migration local Supabase CLI\\\", limit: 5) { nodes { __typename title href content } } }\"\n}", + "output": "{\n \"content\": [\n {\n \"type\": \"text\",\n \"text\": \"{\\\"result\\\":{\\\"searchDocs\\\":{\\\"nodes\\\":[{\\\"__typename\\\":\\\"Guide\\\",\\\"title\\\":\\\"Declarative database schemas\\\",\\\"href\\\":\\\"https://supabase.com/docs/guides/local-development/declarative-database-schemas\\\",\\\"content\\\":\\\"# Declarative database schemas\\\\n\\\\nManage your database schemas in one place and generate versioned migrations.\\\\n\\\\n## Overview\\\\n\\\\nDeclarative schemas provide a developer-friendly way to maintain

    Files of SQL statements that track the evolution of your database schema over time.
    They allow you to version control your database schema alongside your application code.

    See the database migrations guide to learn more.

    }>schema migrations
    .\\\\n\\\\n[Migrations](/docs/guides/deployment/database-migrations) are traditionally managed imperatively (you provide the instructions on how exactly to change the database). This can lead to related information being scattered over multiple migration files. With declarative schemas, you instead declare the state you want your database to be in, and the instructions are generated for you.\\\\n\\\\n\\\\n## Schema migrations\\\\n\\\\nSchema migrations are SQL statements written in Data Definition Language. They are versioned in your `supabase/migrations` directory to ensure schema consistency between local and remote environments.\\\\n\\\\n\\\\n### Declaring your schema\\\\n\\\\n\\\\n \\\\n \\\\n Create a SQL file in `supabase/schemas` directory that defines an `employees` table.\\\\n \\\\n\\\\n \\\\n \\\\n \\\\n ```sql name=supabase/schemas/employees.sql\\\\n create table \\\\\\\"employees\\\\\\\" (\\\\n \\\\\\\"id\\\\\\\" integer not null,\\\\n \\\\\\\"name\\\\\\\" text\\\\n );\\\\n ```\\\\n \\\\n \\\\n \\\\n \\\\n\\\\n\\\\n\\\\n \\\\n \\\\n Generate a migration file by diffing against your declared schema.\\\\n \\\\n\\\\n \\\\n \\\\n \\\\n ```bash name=Terminal\\\\n supabase db diff -f create_employees_table\\\\n ```\\\\n \\\\n \\\\n \\\\n \\\\n\\\\n\\\\n\\\\n \\\\n \\\\n Start the local database first. Then, apply the migration manually to see your schema changes in the local Dashboard.\\\\n \\\\n\\\\n \\\\n \\\\n \\\\n ```bash name=Terminal\\\\n supabase start\\\\n supabase migration up\\\\n ```\\\\n \\\\n \\\\n \\\\n \\\\n\\\\n\\\\n\\\\n### Updating your schema\\\\n\\\\n\\\\n \\\\n \\\\n Edit `supabase/schemas/employees.sql` file to add a new column to `employees` table.\\\\n \\\\n\\\\n \\\\n \\\\n \\\\n ```sql name=supabase/schemas/employees.sql\\\\n create table \\\\\\\"employees\\\\\\\" (\\\\n \\\\\\\"id\\\\\\\" integer not null,\\\\n \\\\\\\"name\\\\\\\" text,\\\\n \\\\\\\"age\\\\\\\" smallint not null\\\\n );\\\\n ```\\\\n \\\\n \\\\n \\\\n \\\\n\\\\n\\\\n\\\\n Some entities like views and enums expect columns to be declared in a specific order. To avoid messy diffs, always append new columns to the end of the table.\\\\n\\\\n\\\\n\\\\n \\\\n \\\\n Diff existing migrations against your declared schema.\\\\n \\\\n\\\\n \\\\n \\\\n \\\\n ```bash name=Terminal\\\\n supabase db diff -f add_age\\\\n ```\\\\n \\\\n \\\\n \\\\n \\\\n\\\\n\\\\n\\\\n \\\\n \\\\n Verify that the generated migration contain a single incremental change.\\\\n \\\\n\\\\n \\\\n \\\\n _add_age.sql\\\\\\\" label=\\\\\\\"supabase/migrations/_add_age.sql\\\\\\\">\\\\n ```sql name=supabase/migrations/_add_age.sql\\\\n alter table \\\\\\\"public\\\\\\\".\\\\\\\"employees\\\\\\\" add column \\\\\\\"age\\\\\\\" smallint not null;\\\\n ```\\\\n \\\\n \\\\n \\\\n \\\\n\\\\n\\\\n\\\\n \\\\n \\\\n Start the database locally and apply the pending migration.\\\\n \\\\n\\\\n \\\\n \\\\n \\\\n ```bash name=Terminal\\\\n supabase migration up\\\\n ```\\\\n \\\\n \\\\n \\\\n \\\\n\\\\n\\\\n\\\\n### Deploying your schema changes\\\\n\\\\n\\\\n \\\\n \\\\n [Log in](/docs/reference/cli/supabase-login) via the Supabase CLI.\\\\n \\\\n\\\\n \\\\n \\\\n \\\\n ```bash name=Terminal\\\\n supabase login\\\\n ```\\\\n \\\\n \\\\n \\\\n \\\\n\\\\n \\\\n \\\\n Follow the on-screen prompts to [link](/docs/reference/cli/supabase-link) your remote project.\\\\n \\\\n\\\\n \\\\n \\\\n \\\\n ```bash name=Terminal\\\\n supabase link\\\\n ```\\\\n \\\\n \\\\n \\\\n \\\\n\\\\n \\\\n \\\\n [Push](/docs/reference/cli/supabase-db-push) your changes to the remote database.\\\\n \\\\n\\\\n \\\\n \\\\n \\\\n ```bash name=Terminal\\\\n supabase db push\\\\n ```\\\\n \\\\n \\\\n \\\\n \\\\n\\\\n\\\\n\\\\n### Managing dependencies\\\\n\\\\nAs your database schema evolves, you will probably start using more advanced entities like views and functions. These entities are notoriously verbose to manage using plain migrations because the entire body must be recreated whenever there is a change. Using declarative schema, you can now edit them in-place so it’s much easier to review.\\\\n\\\\n\\\\n \\\\n ```sql name=supabase/schemas/employees.sql\\\\n create table \\\\\\\"employees\\\\\\\" (\\\\n \\\\\\\"id\\\\\\\" integer not null,\\\\n \\\\\\\"name\\\\\\\" text,\\\\n \\\\\\\"age\\\\\\\" smallint not null\\\\n );\\\\n\\\\n create view \\\\\\\"profiles\\\\\\\" as\\\\n select id, name from \\\\\\\"employees\\\\\\\";\\\\n\\\\n create function \\\\\\\"get_age\\\\\\\"(employee_id integer) RETURNS smallint\\\\n LANGUAGE \\\\\\\"sql\\\\\\\"\\\\n AS $$\\\\n select age\\\\n from employees\\\\n where id = employee_id;\\\\n $$;\\\\n ```\\\\n \\\\n\\\\n\\\\nYour schema files are run in lexicographic order by default. The order is important when you have foreign keys between multiple tables as the parent table must be created first. For example, your `supabase` directory may end up with the following structure.\\\\n\\\\n```bash\\\\n.\\\\n└── supabase/\\\\n ├── schemas/\\\\n │ ├── employees.sql\\\\n │ └── managers.sql\\\\n └── migrations/\\\\n ├── 20241004112233_create_employees_table.sql\\\\n ├── 20241005112233_add_employee_age.sql\\\\n └── 20241006112233_add_managers_table.sql\\\\n```\\\\n\\\\nFor small projects with only a few tables, the default schema order may be sufficient. However, as your project grows, you might need more control over the order in which schemas are applied. To specify a custom order for applying the schemas, you can declare them explicitly in `config.toml`. Any glob patterns will evaluated, deduplicated, and sorted in lexicographic order. For example, the following pattern ensures `employees.sql` is always executed first.\\\\n\\\\n\\\\n \\\\n ```toml name=supabase/config.toml\\\\n [db.migrations]\\\\n schema_paths = [\\\\n \\\\\\\"./schemas/employees.sql\\\\\\\",\\\\n \\\\\\\"./schemas/*.sql\\\\\\\",\\\\n ]\\\\n ```\\\\n \\\\n\\\\n\\\\n\\\\n### Pulling in your production schema\\\\n\\\\nTo set up declarative schemas on a existing project, you can pull in your production schema by running:\\\\n\\\\n\\\\n \\\\n ```bash name=Terminal\\\\n supabase db dump > supabase/schemas/prod.sql\\\\n ```\\\\n \\\\n\\\\n\\\\nFrom there, you can start breaking down your schema into smaller files and generate migrations. You can do this all at once, or incrementally as you make changes to your schema.\\\\n\\\\n\\\\n### Rolling back a schema change\\\\n\\\\nDuring development, you may want to rollback a migration to keep your new schema changes in a single migration file. This can be done by resetting your local database to a previous version.\\\\n\\\\n\\\\n \\\\n ```bash name=Terminal\\\\n supabase db reset --version 20241005112233\\\\n ```\\\\n \\\\n\\\\n\\\\nAfter a reset, you can [edit the schema](#updating-your-schema) and regenerate a new migration file. Note that you should not reset a version that's already deployed to production.\\\\n\\\\nIf you need to rollback a migration that's already deployed, you should first revert changes to the schema files. Then you can generate a new migration file containing the down migration. This ensures your production migrations are always rolling forward.\\\\n\\\\n\\\\n SQL statements generated in a down migration are usually destructive. You must review them carefully to avoid unintentional data loss.\\\\n\\\\n\\\\n\\\\n## Known caveats\\\\n\\\\nThe `migra` diff tool used for generating schema diff is capable of tracking most database changes. However, there are edge cases where it can fail.\\\\n\\\\nIf you need to use any of the entities below, remember to add them through [versioned migrations](/docs/guides/deployment/database-migrations) instead.\\\\n\\\\n\\\\n### Data manipulation language\\\\n\\\\n* DML statements such as `insert`, `update`, `delete`, etc., are not captured by schema diff\\\\n\\\\n\\\\n### View ownership\\\\n\\\\n* [view owner and grants](https://github.com/djrobstep/migra/issues/160#issuecomment-1702983833)\\\\n* [security invoker on views](https://github.com/djrobstep/migra/issues/234)\\\\n* [materialized views](https://github.com/djrobstep/migra/issues/194)\\\\n* doesn’t recreate views when altering column type\\\\n\\\\n\\\\n### RLS policies\\\\n\\\\n* [alter policy statements](https://github.com/djrobstep/schemainspect/blob/master/schemainspect/pg/obj.py#L228)\\\\n* [column privileges](https://github.com/djrobstep/schemainspect/pull/67)\\\\n\\\\n\\\\n### Other entities\\\\n\\\\n* schema privileges are not tracked because each schema is diffed separately\\\\n* [comments are not tracked](https://github.com/djrobstep/migra/issues/69)\\\\n* [partitions are not tracked](https://github.com/djrobstep/migra/issues/186)\\\\n* [`alter publication ... add table ...`](https://github.com/supabase/cli/issues/883)\\\\n* [create domain statements are ignored](https://github.com/supabase/cli/issues/2137)\\\\n* [grant statements are duplicated from default privileges](https://github.com/supabase/cli/issues/1864)\\\\n\\\"},{\\\"__typename\\\":\\\"Guide\\\",\\\"title\\\":\\\"Local development workflow\\\",\\\"href\\\":\\\"https://supabase.com/docs/guides/local-development/cli-workflows\\\",\\\"content\\\":\\\"# Local development workflow\\\\n\\\\nSet up and run your day-to-day local development workflow with the Supabase CLI.\\\\n\\\\nThis guide walks through two common starting points for local development with the Supabase CLI, and shows how they converge into the same daily workflow. By the end, you'll have a `./supabase` directory in your repo that anyone can clone to recreate the full project, locally or on a fresh remote instance.\\\\n\\\\nThere are two starting points, both leading to the same place: database schema and migrations tracked in version control, with seed data for local development.\\\\n\\\\n* **[Move an existing project to local development](#move-an-existing-project-to-local-development)**: you have a project on the Supabase platform and want to bring it into a proper local development workflow.\\\\n* **[Start a new project from scratch](#start-a-new-project-from-scratch)**: you're building locally and will eventually push to a remote instance.\\\\n\\\\n\\\\n## Before you begin\\\\n\\\\nYou need the Supabase CLI installed and a Docker-compatible runtime running. If you haven't set these up yet, see [Install and run the CLI](/docs/guides/local-development/cli/getting-started) for installation across macOS, Windows, and Linux, and for the details of what `supabase start` brings up and how to access each service.\\\\n\\\\nKeep in mind that **the local stack is for development only**. It is not hardened for production use and must never be exposed to external traffic. It has no TLS, no rate limiting, and default credentials. Use it to develop and test, then deploy to the [Supabase Platform](https://supabase.com) or a proper self-hosted setup for anything beyond that.\\\\n\\\\n\\\\n How you invoke the CLI depends on how you installed it:\\\\n\\\\n * Installed globally with **Homebrew or Scoop**: run `supabase `.\\\\n * Added as a **project dependency** with npm, pnpm, yarn, or bun: run it through your package runner instead, for example `npx supabase ` (or `pnpm supabase`, `yarn supabase`, `bunx supabase`).\\\\n\\\\n Every example in this guide is written as `supabase `. Translate it to whichever form matches your install. See [Install and run the CLI](/docs/guides/local-development/cli/getting-started) for the full setup.\\\\n\\\\n\\\\n\\\\n If you want a working project to explore rather than an empty one, `supabase bootstrap` scaffolds a starter application (Next.js, Flutter, and more) with schema, migrations, and config already wired up. It's an alternative entry point to `supabase init` when starting a new project from scratch.\\\\n\\\\n\\\\n\\\\n## The `./supabase` directory\\\\n\\\\nAfter `supabase init`, your project contains a `./supabase` directory. Here's what goes in it and what to commit:\\\\n\\\\n| Path | Purpose | Commit? |\\\\n| ---------------------- | ----------------------------------------------------------------- | ------- |\\\\n| `config.toml` | Local stack configuration (ports, auth settings, etc.) | Yes |\\\\n| `migrations/` | Timestamped SQL migration files, applied in order | Yes |\\\\n| `seed.sql` | Dev/test data, applied after migrations on `start` and `db reset` | Yes |\\\\n| `schemas/` | Declarative schema files (if using that approach) | Yes |\\\\n| `.temp/`, `.branches/` | CLI internal state | No |\\\\n\\\\nThe `config.toml` is safe to commit. It contains no secrets by default. If you add sensitive values (OAuth credentials, API keys), use the `env()` function to reference environment variables instead of hardcoding them. See [Managing config and secrets](/docs/guides/local-development/managing-config).\\\\n\\\\n\\\\n Many database commands accept `--local` and `--linked` flags to choose what they act on. The defaults are not the same across commands: `db diff` and `db reset` default to `--local`, while `db pull`, `db push`, and `db dump` default to `--linked`. When in doubt, pass the flag explicitly.\\\\n\\\\n\\\\n\\\\n## Move an existing project to local development\\\\n\\\\nYou've built a project on the Supabase platform, with tables created via the Dashboard, SQL editor, or client libraries. Now you want a local dev setup with everything in version control.\\\\n\\\\n\\\\n### Step 1: Initialize\\\\n\\\\nIn your project root:\\\\n\\\\n```bash\\\\nsupabase init\\\\n```\\\\n\\\\nThis creates `./supabase/config.toml`. If you already have a project directory with application code, run this at the root. The `supabase/` directory will sit alongside your app code.\\\\n\\\\n\\\\n### Step 2: Authenticate\\\\n\\\\n```bash\\\\nsupabase login\\\\n```\\\\n\\\\nOpens a browser to generate an access token. The token is stored locally and used for all subsequent CLI commands that interact with the platform.\\\\n\\\\n\\\\n### Step 3: Link to your remote project\\\\n\\\\n```bash\\\\nsupabase link --project-ref \\\\n```\\\\n\\\\nFind your project ID in the Supabase Dashboard URL: `https://supabase.com/dashboard/project/`.\\\\n\\\\nThis tells the CLI which remote project to connect to for `db pull`, `db push`, and other remote operations. You'll be prompted for the database password, which is the password set when you created the project.\\\\n\\\\n\\\\n### Step 4: Pull the remote schema\\\\n\\\\n```bash\\\\nsupabase db pull\\\\n```\\\\n\\\\nThis connects to your remote database, dumps the entire schema, and saves it as a migration file:\\\\n\\\\n```\\\\nsupabase/migrations/_remote_schema.sql\\\\n```\\\\n\\\\nThis initial migration is your baseline. It represents the current state of your database, and all future changes build on top of it. `db pull` also records this migration as already applied in the remote migration history (the `supabase_migrations.schema_migrations` table), so a later `db push` won't try to reapply it.\\\\n\\\\n\\\\n `db pull` diffs your remote database against the CLI's default local stack, so the generated file can include statements you didn't expect. A common example is `DROP EXTENSION pg_net;`, emitted when your remote project has an extension disabled that the local stack enables by default. These statements apply silently on `db reset` and change your local schema, so read the file before committing it. See [Cleaning up generated migrations](#cleaning-up-generated-migrations) for what to look for.\\\\n\\\\n\\\\n\\\\n If you also use Supabase Auth or Storage and have customized their schemas, pull them separately:\\\\n\\\\n ```bash\\\\n supabase db pull --schema auth -f pull-auth-schema\\\\n supabase db pull --schema storage -f pull-storage-schema\\\\n ```\\\\n\\\\n These schemas are managed by Supabase and typically don't need to be pulled unless you've made custom modifications.\\\\n\\\\n\\\\n\\\\n### Step 5: Create seed data\\\\n\\\\nYou have two options:\\\\n\\\\n**Option A: Dump existing data from remote** (then clean it up):\\\\n\\\\n```bash\\\\nsupabase db dump --data-only --linked > supabase/seed.sql\\\\n```\\\\n\\\\n\\\\n Review and clean up the dump before committing. Remove production user data, secrets, personal information, and anything sensitive. Keep only representative test data that a developer needs to work with the project.\\\\n\\\\n\\\\n**Option B: Write seed data by hand** (recommended for most projects):\\\\n\\\\nCreate `supabase/seed.sql` with INSERT statements that set up a useful local development state: a few test users, sample data, and so on. This is often better than dumping production data because you control exactly what's in it.\\\\n\\\\nFor more on organizing seed files, glob patterns, and generating realistic data, see [Seeding your database](/docs/guides/local-development/seeding-your-database).\\\\n\\\\n\\\\n### Step 6: Verify\\\\n\\\\n```bash\\\\nsupabase start\\\\nsupabase db reset\\\\n```\\\\n\\\\n`db reset` destroys the local database and recreates it from scratch: it applies all migrations in order, then runs `seed.sql`. If this succeeds, your setup is reproducible. Anyone who clones the repo can do the same.\\\\n\\\\n\\\\n### Step 7: Commit\\\\n\\\\n```bash\\\\ngit add supabase/\\\\ngit commit -m \\\\\\\"add supabase local development setup\\\\\\\"\\\\n```\\\\n\\\\nYour project now has a fully reproducible local development environment.\\\\n\\\\n\\\\n For an existing project, the pulled migration already serves as your schema baseline. You don't need to also create a `schemas/` directory, because that would mean maintaining two representations of the same schema. If you want to adopt declarative schemas later, see [Declarative database schemas](/docs/guides/local-development/declarative-database-schemas). For day-to-day changes going forward, see [The daily workflow](#the-daily-workflow) below.\\\\n\\\\n\\\\n\\\\n## Start a new project from scratch\\\\n\\\\nNo remote project yet. You're building from scratch and want to do it right from the start.\\\\n\\\\n\\\\n### Step 1: Initialize\\\\n\\\\n```bash\\\\nsupabase init\\\\n```\\\\n\\\\n\\\\n### Step 2: Start the local stack\\\\n\\\\n```bash\\\\nsupabase start\\\\n```\\\\n\\\\nOn first run, Docker images are pulled, which takes a few minutes. Subsequent starts are fast. Once running, the CLI outputs local service URLs and credentials, including the Studio URL for a local instance of the Dashboard. See [Install and run the CLI](/docs/guides/local-development/cli/getting-started#access-your-projects-services) for the full output and how to reach each service.\\\\n\\\\n\\\\n### Step 3: Create your schema\\\\n\\\\nTwo approaches, pick one:\\\\n\\\\n**Option A: Declarative schema** (recommended for new projects)\\\\n\\\\nDeclare the state you want your database to be in as a file in `supabase/schemas/`, for example:\\\\n\\\\n```sql title=\\\\\\\"supabase/schemas/schema.sql\\\\\\\"\\\\ncreate table public.todos (\\\\n id bigint generated by default as identity primary key,\\\\n created_at timestamptz default now() not null,\\\\n title text not null,\\\\n is_complete boolean default false not null,\\\\n user_id uuid references auth.users (id) default auth.uid() not null\\\\n);\\\\n\\\\nalter table public.todos enable row level security;\\\\n\\\\ncreate policy \\\\\\\"Users can read their own todos\\\\\\\"\\\\n on public.todos for select\\\\n using (auth.uid() = user_id);\\\\n\\\\ncreate policy \\\\\\\"Users can create their own todos\\\\\\\"\\\\n on public.todos for insert\\\\n with check (auth.uid() = user_id);\\\\n```\\\\n\\\\nThen generate a migration from it:\\\\n\\\\n```bash\\\\nsupabase db diff -f initial-schema\\\\n```\\\\n\\\\nThis compares your declared schema against the current (empty) database and generates a migration file in `supabase/migrations/`. For the full declarative workflow, including managing views and functions, ordering schema files, and known caveats, see [Declarative database schemas](/docs/guides/local-development/declarative-database-schemas).\\\\n\\\\n**Option B: Write the migration directly**\\\\n\\\\n```bash\\\\nsupabase migration new initial-schema\\\\n```\\\\n\\\\nThis creates an empty file at `supabase/migrations/_initial-schema.sql`. Write your SQL in it, then apply:\\\\n\\\\n```bash\\\\nsupabase db reset\\\\n```\\\\n\\\\n\\\\n### Step 4: Add seed data\\\\n\\\\nCreate `supabase/seed.sql`:\\\\n\\\\n```sql title=\\\\\\\"supabase/seed.sql\\\\\\\"\\\\n-- Create a test user (Supabase Auth)\\\\n-- Note: this is a placeholder row so seeded data has a user_id to reference.\\\\n-- It has no password, so it can't be used to sign in. To create a\\\\n-- login-capable user, use the Auth admin API or the local Studio.\\\\ninsert into auth.users (id, email, raw_user_meta_data)\\\\nvalues ('d0e3c8f0-1234-5678-9abc-def012345678', 'test@example.com', '{}');\\\\n\\\\n-- Seed application data\\\\ninsert into public.todos (title, user_id)\\\\nvalues\\\\n ('Buy groceries', 'd0e3c8f0-1234-5678-9abc-def012345678'),\\\\n ('Write documentation', 'd0e3c8f0-1234-5678-9abc-def012345678');\\\\n```\\\\n\\\\n\\\\n### Step 5: Verify\\\\n\\\\n```bash\\\\nsupabase db reset\\\\n```\\\\n\\\\nDrops everything, applies migrations, runs seed. If this passes, your project is reproducible.\\\\n\\\\n\\\\n### Step 6: Commit\\\\n\\\\n```bash\\\\ngit add supabase/\\\\ngit commit -m \\\\\\\"add supabase local development setup\\\\\\\"\\\\n```\\\\n\\\\n\\\\n## The daily workflow\\\\n\\\\nBoth starting points converge here. You have a working `./supabase` directory in your repo. Here's how day-to-day development works.\\\\n\\\\n\\\\n### Making schema changes\\\\n\\\\nWhich approach you use is a project-level decision, set when you first created your schema - not a per-change choice. It depends on whether you keep declarative files in `supabase/schemas/`. Pick the tab that matches your project.\\\\n\\\\n\\\\n \\\\n 1. Edit your schema file(s) in `supabase/schemas/` (add a table, a column, a policy, etc.)\\\\n 2. Generate a migration: `supabase db diff -f add-due-date-to-todo`\\\\n 3. Review the generated migration file. See [Cleaning up generated migrations](#cleaning-up-generated-migrations)\\\\n 4. Verify the full chain: `supabase db reset`\\\\n 5. Commit the schema file **and** the migration together\\\\n\\\\n \\\\n `db diff` compares your `supabase/schemas/` files against your existing migrations; it does **not** read the live local database. Changes you make directly in Studio or via SQL are ignored, so `db diff` reports \\\\\\\"No schema changes found\\\\\\\" and silently drops them. Always edit the schema files, then diff.\\\\n \\\\n \\\\n\\\\n \\\\n **If you made changes through the local Studio UI:**\\\\n\\\\n ```bash\\\\n supabase db diff -f add-due-date-to-todo\\\\n ```\\\\n\\\\n This captures your UI changes as a migration file. This works only when your project has **no** declarative files in `supabase/schemas/`: `db diff` then compares the live local database against your migrations. If you use declarative schemas, don't edit through Studio expecting `db diff` to catch it - see the **Declarative schemas** tab.\\\\n\\\\n **If you prefer to write SQL directly:**\\\\n\\\\n ```bash\\\\n supabase migration new add-due-date-to-todo\\\\n ```\\\\n\\\\n Write the SQL in the generated file. Then verify:\\\\n\\\\n ```bash\\\\n supabase db reset\\\\n ```\\\\n\\\\n Commit the migration.\\\\n \\\\n\\\\n\\\\n\\\\n### Generating types\\\\n\\\\nIf your app uses the generated TypeScript types, regenerate them whenever your schema changes:\\\\n\\\\n```bash\\\\nsupabase gen types --lang typescript --local > database.types.ts\\\\n```\\\\n\\\\nUse `--linked` instead of `--local` to generate from your remote project. TypeScript is the default language; pass `--lang go`, `--lang swift`, or `--lang python` for others.\\\\n\\\\nFor working with the generated types (helper types, JSON inference, type-safe queries) and automating regeneration in CI, see [Generating types](/docs/guides/api/rest/generating-types).\\\\n\\\\n\\\\n### Staying in sync with your team\\\\n\\\\nWhen someone else pushes new migrations:\\\\n\\\\n```bash\\\\ngit pull\\\\nsupabase db reset\\\\n```\\\\n\\\\n`db reset` replays all migrations from scratch, so you'll always match the current state of the repo.\\\\n\\\\n\\\\n## Pushing to a remote project\\\\n\\\\nWhen you're ready to deploy your schema to a remote Supabase instance:\\\\n\\\\n```bash\\\\n# Authenticate (if not already)\\\\nsupabase login\\\\n\\\\n# Link to the remote project (if not already)\\\\nsupabase link --project-ref \\\\n\\\\n# Preview what will be applied\\\\nsupabase db push --dry-run\\\\n\\\\n# Apply migrations\\\\nsupabase db push\\\\n```\\\\n\\\\n`db push` applies only migrations that haven't been applied to the remote yet. It tracks this via the `supabase_migrations.schema_migrations` table created automatically on the remote database.\\\\n\\\\nTo also seed a fresh remote instance (dev/staging environments only):\\\\n\\\\n```bash\\\\nsupabase db push --include-seed\\\\n```\\\\n\\\\n\\\\n Never use `--include-seed` on a production database. Seed data is for development and testing.\\\\n\\\\n\\\\n\\\\n### Resetting a remote dev or staging project\\\\n\\\\nIf a dev or staging remote drifts or gets into a messy state, you can wipe it and rebuild it from your local migrations:\\\\n\\\\n```bash\\\\nsupabase db reset --linked\\\\n```\\\\n\\\\nUnlike the default `supabase db reset`, which targets your local database, the `--linked` flag runs against the remote project you connected with `supabase link`: it drops the remote schema, then replays every local migration in order. Add `--include-seed` to reload seed data as well.\\\\n\\\\n\\\\n `db reset --linked` is destructive: it erases all data in the linked remote database. Only run it against throwaway dev or staging projects, and double-check which project you're linked to (`supabase projects list` shows the linked one) before running it. Never use it on production.\\\\n\\\\n\\\\nFor multi-environment setups with CI/CD (feature branches, staging, production), see [Managing Environments](/docs/guides/deployment/managing-environments).\\\\n\\\\n\\\\n## Key commands at a glance\\\\n\\\\n| Command | What it does |\\\\n| -------------------------------------- | ------------------------------------------------------------------------------------------------------- |\\\\n| `supabase init` | Creates `./supabase/config.toml` |\\\\n| `supabase start` | Starts the local stack, applies migrations + seed |\\\\n| `supabase stop` | Stops the local stack (data persists until `db reset`) |\\\\n| `supabase db reset` | Destroys local DB, applies all migrations + seed from scratch |\\\\n| `supabase db reset --linked` | Destroys the **linked remote** DB and rebuilds it from local migrations (destructive; dev/staging only) |\\\\n| `supabase db diff -f ` | Generates a migration by diffing current DB state against a shadow database |\\\\n| `supabase db pull` | Pulls remote schema into a new local migration file |\\\\n| `supabase db push` | Applies pending local migrations to the remote database |\\\\n| `supabase db dump` | Exports remote DB schema (or `--data-only` for data) via `pg_dump` |\\\\n| `supabase migration new ` | Creates an empty migration file |\\\\n| `supabase migration list` | Compares local migrations against remote migration history |\\\\n| `supabase gen types --lang typescript` | Generates TypeScript types from your database schema |\\\\n| `supabase link --project-ref` | Connects local project to a remote Supabase project |\\\\n| `supabase login` | Authenticates with the Supabase platform |\\\\n\\\\nFor the full command reference and every flag, see the [CLI reference](/docs/reference/cli).\\\\n\\\\n\\\\n## Cleaning up generated migrations\\\\n\\\\nWhen `supabase db diff` generates a migration, it may include statements that are technically correct but noisy. Review every generated migration before committing.\\\\n\\\\n\\\\n### Grants\\\\n\\\\nYou may see lines like:\\\\n\\\\n```sql\\\\nGRANT MAINTAIN, REFERENCES, TRIGGER, TRUNCATE ON public.todos TO anon;\\\\nGRANT MAINTAIN, REFERENCES, TRIGGER, TRUNCATE ON public.todos TO authenticated;\\\\nGRANT MAINTAIN, REFERENCES, TRIGGER, TRUNCATE ON public.todos TO service_role;\\\\n```\\\\n\\\\nThese appear because the diff tool treats permissions as part of the schema state. For tables in the `public` schema, these grants are applied by default and the lines are redundant. They're harmless, but if you want clean migrations, you can remove them. Be consistent across your team about whether you keep or remove them.\\\\n\\\\n\\\\n### Revoke/re-grant patterns\\\\n\\\\nSometimes a diff produces:\\\\n\\\\n```sql\\\\nREVOKE ALL ON TABLE public.todos FROM anon;\\\\nGRANT ALL ON TABLE public.todos TO anon;\\\\n```\\\\n\\\\nThis is the diff tool being overly cautious. If you haven't changed permissions, these lines can be safely removed.\\\\n\\\\n\\\\n### Extension statements\\\\n\\\\n`CREATE EXTENSION IF NOT EXISTS ...` may appear. Keep these if the extension is required by your migration. Remove them if the extension is already created by a previous migration or is part of the default Supabase setup.\\\\n\\\\n\\\\n### Known limitations of `db diff`\\\\n\\\\nThe diff is generated by `pg-delta`, the default schema diff engine. (The older [`migra`](https://github.com/djrobstep/migra) engine is still available: set `enabled = false` under `[experimental.pgdelta]` in `config.toml`, or pass `--use-migra`.) No diff engine captures everything. Most notably, DML (INSERT, UPDATE, DELETE) is not tracked, so data changes must be added to the migration manually, and some entities like RLS policy renames and certain view properties don't diff cleanly. See the [full list of caveats](/docs/guides/local-development/declarative-database-schemas#known-caveats) in the declarative schemas guide.\\\\n\\\\nTreat `db diff` output as a draft, not a final migration. When in doubt, review the generated SQL and adjust it manually.\\\\n\\\\n\\\\n## Troubleshooting\\\\n\\\\n**`db reset` fails with a migration error**\\\\n\\\\nThe output will show which migration file failed and the SQL error. Fix the migration file, then run `db reset` again.\\\\n\\\\n**`db push` says migrations are already applied**\\\\n\\\\nThe remote database already has those migrations in its history. Run `supabase migration list` to compare local vs. remote state. If they're out of sync, use `supabase migration repair` to correct the remote history.\\\\n\\\\n**Schema drift: remote was changed outside of migrations**\\\\n\\\\nIf someone modified the remote database directly (via Dashboard, SQL editor, etc.), run `supabase db pull` to capture those changes as a new migration file. Then `supabase db reset` locally to verify everything still works.\\\\n\\\\n**Docker issues on `supabase start`**\\\\n\\\\nEnsure Docker is running and has at least 7 GB of RAM allocated. If containers fail health checks, try:\\\\n\\\\n```bash\\\\nsupabase stop\\\\nsupabase start\\\\n```\\\\n\\\\nIf problems persist, `supabase stop --no-backup` for a clean restart (this removes local database data).\\\\n\\\"},{\\\"__typename\\\":\\\"Guide\\\",\\\"title\\\":\\\"Database Migrations\\\",\\\"href\\\":\\\"https://supabase.com/docs/guides/deployment/database-migrations\\\",\\\"content\\\":\\\"# Database Migrations\\\\n\\\\nHow to manage schema migrations for your Supabase project.\\\\n\\\\nDatabase migrations are SQL statements that create, update, or delete your existing database schemas. They are a common way of tracking changes to your database over time.\\\\n\\\\n\\\\n## Schema migrations\\\\n\\\\nFor this guide, we'll create a table called `employees` and see how we can make changes to it.\\\\n\\\\nYou will need to [install](/docs/guides/local-development#quickstart) the Supabase CLI and start the local development stack.\\\\n\\\\n\\\\n If a lock timeout error occurs, in your migration file, consider increasing your [`lock_timeout`](https://postgresqlco.nf/doc/en/param/lock_timeout/) setting.\\\\n\\\\n\\\\n\\\\n \\\\n \\\\n To get started, generate a [new migration](/docs/reference/cli/supabase-migration-new) to store the SQL needed to create our `employees` table.\\\\n \\\\n\\\\n \\\\n \\\\n ```bash name=Terminal\\\\n supabase migration new create_employees_table\\\\n ```\\\\n \\\\n \\\\n \\\\n\\\\n\\\\n\\\\n \\\\n \\\\n This creates a new migration file in supabase/migrations directory.\\\\n\\\\n To that file, add the SQL to create this `employees` table.\\\\n \\\\n\\\\n \\\\n _create_employees_table.sql\\\\\\\">\\\\n ```sql name=supabase/migrations/_create_employees_table.sql\\\\n create table if not exists employees (\\\\n id bigint primary key generated always as identity,\\\\n name text not null,\\\\n email text,\\\\n created_at timestamptz default now()\\\\n );\\\\n ```\\\\n \\\\n \\\\n \\\\n\\\\n\\\\n\\\\n \\\\n \\\\n Run this migration to create the `employees` table.\\\\n\\\\n Now you can visit your new `employees` table in the local Dashboard.\\\\n \\\\n\\\\n \\\\n \\\\n ```bash name=Terminal\\\\n supabase migration up\\\\n ```\\\\n \\\\n \\\\n \\\\n\\\\n\\\\n\\\\n \\\\n \\\\n Next, modify your `employees` table by adding a column for `department`.\\\\n \\\\n\\\\n \\\\n \\\\n ```bash name=Terminal\\\\n supabase migration new add_department_column\\\\n ```\\\\n \\\\n \\\\n \\\\n\\\\n\\\\n\\\\n \\\\n \\\\n To that new migration file, add the SQL to create a new `department` column.\\\\n \\\\n\\\\n \\\\n _add_department_column.sql\\\\\\\">\\\\n ```sql name=supabase/migrations/_add_department_column.sql\\\\n alter table if exists public.employees\\\\n add department text default 'Hooli';\\\\n ```\\\\n \\\\n \\\\n \\\\n\\\\n\\\\n\\\\n \\\\n \\\\n Run this migration to update your existing `employees` table.\\\\n \\\\n\\\\n \\\\n \\\\n ```bash name=Terminal\\\\n supabase migration up\\\\n ```\\\\n \\\\n \\\\n \\\\n\\\\n\\\\nFinally, you should see the `department` column added to your `employees` table in the local Dashboard.\\\\n\\\\n\\\\n View the [complete code](https://github.com/supabase/supabase/tree/master/examples/database/employees) for this example on GitHub.\\\\n\\\\n\\\\n\\\\n### Seeding data\\\\n\\\\nNow that you are managing your database with migrations, it would be great have some seed data to use every time you reset the database.\\\\n\\\\n\\\\n \\\\n \\\\n Create a seed script in supabase/seed.sql.\\\\n\\\\n To that file, add the SQL to insert data into your `employees` table.\\\\n \\\\n\\\\n \\\\n \\\\n ```sql name=supabase/seed.sql\\\\n insert into public.employees\\\\n (name)\\\\n values\\\\n ('Erlich Bachman'),\\\\n ('Richard Hendricks'),\\\\n ('Monica Hall');\\\\n ```\\\\n \\\\n \\\\n \\\\n\\\\n\\\\n\\\\n \\\\n \\\\n Reset your database to reapply migrations and populate with seed data.\\\\n \\\\n\\\\n \\\\n \\\\n ```bash name=Terminal\\\\n supabase db reset\\\\n ```\\\\n \\\\n \\\\n \\\\n\\\\n\\\\nYou should now see the `employees` table, along with your seed data in the Dashboard! All of your database changes are captured in code, and you can reset to a known state at any time, complete with seed data.\\\\n\\\\n\\\\n### Diffing changes\\\\n\\\\nThis workflow is great if you know SQL and are comfortable creating tables and columns. If not, you can still use the Dashboard to create tables and columns, and then use the CLI to diff your changes and create migrations.\\\\n\\\\n\\\\n Only use the Dashboard to make schema changes on your **local** database, then capture them with `supabase db diff`. Making schema changes directly on your **remote** database (via the SQL editor or Table Editor) bypasses the migration history and will cause `db push` to fail with sync errors. Once you're using migrations, all schema changes to your remote database should go through migration files only.\\\\n\\\\n\\\\n\\\\n \\\\n \\\\n Create a new table called `cities`, with columns `id`, `name` and `population`.\\\\n\\\\n Then generate a [schema diff](/docs/reference/cli/supabase-db-diff).\\\\n \\\\n\\\\n \\\\n \\\\n ```bash name=Terminal\\\\n supabase db diff -f create_cities_table\\\\n ```\\\\n \\\\n \\\\n \\\\n\\\\n\\\\n\\\\n \\\\n \\\\n A new migration file is created for you.\\\\n\\\\n Alternately, you can copy the table definitions directly from the Table Editor.\\\\n \\\\n\\\\n \\\\n _create_cities_table.sql\\\\\\\">\\\\n ```sql name=supabase/migrations/_create_cities_table.sql\\\\n create table \\\\\\\"public\\\\\\\".\\\\\\\"cities\\\\\\\" (\\\\n \\\\\\\"id\\\\\\\" bigint primary key generated always as identity,\\\\n \\\\\\\"name\\\\\\\" text,\\\\n \\\\\\\"population\\\\\\\" bigint\\\\n );\\\\n ```\\\\n \\\\n \\\\n \\\\n\\\\n\\\\n\\\\n \\\\n \\\\n Test your new migration file by resetting your local database.\\\\n \\\\n\\\\n \\\\n \\\\n ```bash name=Terminal\\\\n supabase db reset\\\\n ```\\\\n \\\\n \\\\n \\\\n\\\\n\\\\nThe last step is deploying these changes to a live Supabase project.\\\\n\\\\n\\\\n## Deploy your project\\\\n\\\\nYou've been developing your project locally, making changes to your tables via migrations. It's time to deploy your project to the Supabase Platform and start scaling up to millions of users!\\\\n\\\\nHead over to [Supabase](/dashboard) and create a new project to deploy to.\\\\n\\\\n\\\\n \\\\n \\\\n [Login](/docs/reference/cli/supabase-login) to the Supabase CLI using an auto-generated Personal Access Token.\\\\n \\\\n\\\\n \\\\n \\\\n ```bash name=Terminal\\\\n supabase login\\\\n ```\\\\n \\\\n \\\\n \\\\n\\\\n\\\\n\\\\n \\\\n \\\\n [Link](/docs/reference/cli/supabase-link) to your remote project by selecting from the on-screen prompt.\\\\n \\\\n\\\\n \\\\n \\\\n ```bash name=Terminal\\\\n supabase link\\\\n ```\\\\n \\\\n \\\\n \\\\n\\\\n\\\\n\\\\n \\\\n \\\\n [Push](/docs/reference/cli/supabase-db-push) your migrations to the remote database.\\\\n \\\\n\\\\n \\\\n \\\\n ```bash name=Terminal\\\\n supabase db push\\\\n ```\\\\n \\\\n \\\\n \\\\n\\\\n\\\\n\\\\n \\\\n \\\\n [Push](/docs/reference/cli/supabase-db-push) your migrations and seed the remote database.\\\\n \\\\n\\\\n \\\\n \\\\n ```bash name=Terminal\\\\n supabase db push --include-seed\\\\n ```\\\\n \\\\n \\\\n \\\\n\\\\n\\\\nVisiting your live project on [Supabase](/dashboard/project/_), you'll see a new `employees` table, complete with the `department` column you added in the second migration above.\\\\n\\\\n\\\\n## Working with a team\\\\n\\\\nWhen multiple developers share a Supabase project, a few rules keep migrations from getting out of sync.\\\\n\\\\n**The golden rule: never change the remote database directly.** Once you're using migrations, all schema changes — even small ones — should go through migration files. Using the Dashboard's SQL editor or Table Editor on your remote database bypasses the migration history, and `db push` will start failing with sync errors.\\\\n\\\\n**The team workflow:**\\\\n\\\\n\\\\n \\\\n \\\\n Each developer creates migration files on their own branch, never touching the remote database directly.\\\\n \\\\n\\\\n \\\\n \\\\n ```bash name=Terminal\\\\n supabase migration new your_change_description\\\\n ```\\\\n \\\\n \\\\n \\\\n\\\\n\\\\n\\\\n \\\\n \\\\n Reset your local database to apply the migration, then commit the migration file to git.\\\\n \\\\n\\\\n \\\\n \\\\n ```bash name=Terminal\\\\n supabase db reset\\\\n git add supabase/migrations\\\\n git commit -m \\\\\\\"add migration: your_change_description\\\\\\\"\\\\n ```\\\\n \\\\n \\\\n \\\\n\\\\n\\\\n\\\\n \\\\n \\\\n After pulling new migration files from git, reset your local database to apply them.\\\\n \\\\n\\\\n \\\\n \\\\n ```bash name=Terminal\\\\n git pull\\\\n supabase db reset\\\\n ```\\\\n \\\\n \\\\n \\\\n\\\\n\\\\n\\\\n \\\\n \\\\n Coordinate so only one person runs `db push` at a time. Migration files are applied in timestamp order, so concurrent pushes from different machines can cause conflicts.\\\\n \\\\n\\\\n \\\\n \\\\n ```bash name=Terminal\\\\n supabase db push\\\\n ```\\\\n \\\\n \\\\n \\\\n\\\\n\\\\n\\\\n For a more automated deployment approach, consider using [Supabase Branching](/docs/guides/deployment/branching) or a CI/CD pipeline that runs `supabase db push` on merge to your main branch.\\\\n\\\\n\\\\n\\\\n## Diagnosing and fixing sync errors\\\\n\\\\nIf `db push` fails with errors suggesting you run `supabase migration repair`, your local migration files and the remote database's migration history are out of sync. Here's how to diagnose and fix it.\\\\n\\\\n\\\\n### How migration tracking works\\\\n\\\\nSupabase tracks which migrations have been applied on each database in a table called `supabase_migrations.schema_migrations`. When you run `supabase db push`, it compares your local `supabase/migrations` folder against that table and runs only the ones not yet applied, in order.\\\\n\\\\nGit tracks your migration *files*. Supabase tracks what's been *applied to each database*. These are two separate systems that need to stay in sync.\\\\n\\\\n\\\\n### Step 1: Check what's out of sync\\\\n\\\\nStart by listing the migration status across local and remote:\\\\n\\\\n\\\\n ```bash name=Terminal\\\\n supabase migration list\\\\n ```\\\\n\\\\n\\\\nThis shows which migrations are applied locally, which are applied on the remote, and where they diverge.\\\\n\\\\n\\\\n### Step 2: If you made changes on the remote database directly\\\\n\\\\nPull the current remote state into a migration file to get back in sync:\\\\n\\\\n\\\\n ```bash name=Terminal\\\\n supabase db pull\\\\n ```\\\\n\\\\n\\\\nThis creates a new migration file capturing the current remote schema. Commit it to git, then follow the standard workflow going forward.\\\\n\\\\n\\\\n### Step 3: If the migration history table is wrong\\\\n\\\\nIf a migration shows as missing in the remote history table but the schema change is actually already there (for example, it was applied manually), you can mark it as applied without re-running it:\\\\n\\\\n\\\\n ```bash name=Terminal\\\\n supabase migration repair --status applied \\\\n ```\\\\n\\\\n\\\\nOr if a migration is recorded as applied but was never actually run:\\\\n\\\\n\\\\n ```bash name=Terminal\\\\n supabase migration repair --status reverted \\\\n ```\\\\n\\\\n\\\\n\\\\n `migration repair` updates the tracking table only — it does not apply or revert any SQL. Use it to correct the history record when you know the actual database state is correct.\\\\n\\\\n\\\"},{\\\"__typename\\\":\\\"Guide\\\",\\\"title\\\":\\\"Build a User Management App with RedwoodJS\\\",\\\"href\\\":\\\"https://supabase.com/docs/guides/getting-started/tutorials/with-redwoodjs\\\",\\\"content\\\":\\\"# Build a User Management App with RedwoodJS\\\\n\\\\n\\\\n\\\\nThis tutorial demonstrates how to build a basic user management app. The app authenticates and identifies the user, stores their profile information in the database, and allows the user to log in, update their profile details, and upload a profile photo. The app uses:\\\\n\\\\n* [Supabase Database](/docs/guides/database) - a Postgres database for storing your user data and [Row Level Security](/docs/guides/auth#row-level-security) so data is protected and users can only access their own information.\\\\n* [Supabase Auth](/docs/guides/auth) - allow users to sign up and log in.\\\\n* [Supabase Storage](/docs/guides/storage) - allow users to upload a profile photo.\\\\n\\\\n![Supabase User Management example](/docs/img/user-management-demo.png)\\\\n\\\\n\\\\n If you get stuck while working through this guide, you can find the [full example on GitHub](https://github.com/redwoodjs/redwoodjs-supabase-quickstart).\\\\n\\\\n\\\\n\\\\n## About RedwoodJS\\\\n\\\\nA Redwood application is split into two parts: a frontend and a backend. This is represented as two node projects within a single monorepo.\\\\n\\\\nThe frontend project is called **`web`** and the backend project is called **`api`**. For clarity, we will refer to these in prose as **\\\\\\\"sides,\\\\\\\"** that is, the `web side` and the `api side`.\\\\nThey are separate projects because code on the `web side` will end up running in the user's browser while code on the `api side` will run on a server somewhere.\\\\n\\\\n\\\\n Important: When this guide refers to \\\\\\\"API,\\\\\\\" that means the Supabase API and when it refers to `api side`, that means the RedwoodJS `api side`.\\\\n\\\\n\\\\nThe **`api side`** is an implementation of a GraphQL API. The business logic is organized into \\\\\\\"services\\\\\\\" that represent their own internal API and can be called both from external GraphQL requests and other internal services.\\\\n\\\\nThe **`web side`** is built with React. Redwood's router makes it simple to map URL paths to React \\\\\\\"Page\\\\\\\" components (and automatically code-split your app on each route).\\\\nPages may contain a \\\\\\\"Layout\\\\\\\" component to wrap content. They also contain \\\\\\\"Cells\\\\\\\" and regular React components.\\\\nCells allow you to declaratively manage the lifecycle of a component that fetches and displays data.\\\\n\\\\nFor the sake of consistency with the other framework tutorials, we'll build this app a little differently than normal.\\\\nWe ***won't use*** Prisma to connect to the Supabase Postgres database or [Prisma migrations](https://redwoodjs.com/docs/cli-commands#prisma-migrate) as one typically might in a Redwood app.\\\\nInstead, we'll rely on the Supabase client to do some of the work on the **`web`** side and use the client again on the **`api`** side to do data fetching as well.\\\\n\\\\nThat means you will want to refrain from running any `yarn rw prisma migrate` commands and also double check your build commands on deployment to ensure Prisma won't reset your database. Prisma currently doesn't support cross-schema foreign keys, so introspecting the schema fails due\\\\nto how your Supabase `public` schema references the `auth.users`.\\\\n\\\\n\\\\n## Project setup\\\\n\\\\nBefore you start building you need to set up the Database and API. You can do this by starting a new Project in Supabase and then creating a \\\\\\\"schema\\\\\\\" inside the database.\\\\n\\\\n\\\\n### Create a project\\\\n\\\\n1. [Create a new project](/dashboard) in the Supabase Dashboard.\\\\n2. Enter your project details.\\\\n3. Wait for the new database to launch.\\\\n\\\\n\\\\n### Set up the database schema\\\\n\\\\nNow set up the database schema. You can use the \\\\\\\"User Management Starter\\\\\\\" quickstart in the SQL Editor, or you can copy/paste the SQL from below and run it.\\\\n\\\\n\\\\n \\\\n 1. Go to the [SQL Editor](/dashboard/project/_/sql) page in the Dashboard.\\\\n 2. Click **User Management Starter** under the **Community > Quickstarts** tab.\\\\n 3. Click **Run**.\\\\n\\\\n \\\\n You can pull the database schema down to your local project by running the `db pull` command. Read the [local development docs](/docs/guides/cli/local-development#link-your-project) for detailed instructions.\\\\n\\\\n ```bash\\\\n supabase link --project-ref \\\\n # You can get from your project's dashboard URL: https://supabase.com/dashboard/project/\\\\n supabase db pull\\\\n ```\\\\n \\\\n \\\\n\\\\n \\\\n \\\\n When working locally you can run the following command to create a new migration file:\\\\n \\\\n\\\\n ```bash\\\\n supabase migration new user_management_starter\\\\n ```\\\\n\\\\n ```sql\\\\n -- Create a table for public profiles\\\\n create table profiles (\\\\n id uuid references auth.users not null primary key,\\\\n updated_at timestamp with time zone,\\\\n username text unique,\\\\n full_name text,\\\\n avatar_url text,\\\\n website text,\\\\n\\\\n constraint username_length check (char_length(username) >= 3)\\\\n );\\\\n\\\\n -- Grant the privileges roles need\\\\n GRANT SELECT ON public.profiles TO anon;\\\\n GRANT SELECT, INSERT, UPDATE ON public.profiles TO authenticated;\\\\n\\\\n -- Set up Row Level Security (RLS)\\\\n -- See https://supabase.com/docs/guides/database/postgres/row-level-security for more details.\\\\n alter table profiles\\\\n enable row level security;\\\\n\\\\n create policy \\\\\\\"Public profiles are viewable by everyone.\\\\\\\" on profiles\\\\n for select using (true);\\\\n\\\\n create policy \\\\\\\"Users can insert their own profile.\\\\\\\" on profiles\\\\n for insert with check ((select auth.uid()) = id);\\\\n\\\\n create policy \\\\\\\"Users can update own profile.\\\\\\\" on profiles\\\\n for update using ((select auth.uid()) = id);\\\\n\\\\n -- This trigger automatically creates a profile entry when a new user signs up via Supabase Auth.\\\\n -- See https://supabase.com/docs/guides/auth/managing-user-data#using-triggers for more details.\\\\n create function public.handle_new_user()\\\\n returns trigger\\\\n set search_path = ''\\\\n as $$\\\\n begin\\\\n insert into public.profiles (id, full_name, avatar_url)\\\\n values (new.id, new.raw_user_meta_data->>'full_name', new.raw_user_meta_data->>'avatar_url');\\\\n return new;\\\\n end;\\\\n $$ language plpgsql security definer;\\\\n create trigger on_auth_user_created\\\\n after insert on auth.users\\\\n for each row execute procedure public.handle_new_user();\\\\n\\\\n -- Set up Storage!\\\\n insert into storage.buckets (id, name)\\\\n values ('avatars', 'avatars');\\\\n\\\\n -- Set up access controls for storage. Allows downloading object with public key\\\\n -- See https://supabase.com/docs/guides/storage/security/access-control#policy-examples for more details.\\\\n create policy \\\\\\\"Avatar images are publicly accessible.\\\\\\\" on storage.objects\\\\n for select using (bucket_id = 'avatars' and storage.allow_any_operation(array['object.get_authenticated_info', 'object.get_authenticated']));\\\\n\\\\n create policy \\\\\\\"Anyone can upload an avatar.\\\\\\\" on storage.objects\\\\n for insert with check (bucket_id = 'avatars');\\\\n\\\\n create policy \\\\\\\"Anyone can update their own avatar.\\\\\\\" on storage.objects\\\\n for update using ((select auth.uid()) = owner) with check (bucket_id = 'avatars');\\\\n ```\\\\n \\\\n\\\\n\\\\n\\\\n### Get API details\\\\n\\\\nNow that you've created some database tables, you are ready to insert data using the auto-generated API.\\\\n\\\\nTo do this, you need to get the Project URL and key from [the project **Connect** dialog](/dashboard/project/_?showConnect=true\\\\\\\\&connectTab=frameworks\\\\\\\\&framework=).\\\\n\\\\n[Read the API keys docs](/docs/guides/getting-started/api-keys) for a full explanation of all key types and their uses.\\\\n\\\\n\\\\n Supabase is changing the way keys work to improve project security and developer experience. You can [read the full announcement on GitHub](https://github.com/orgs/supabase/discussions/29260).\\\\n\\\\n The older `anon` and `service_role` keys will work until the end of 2026 but **we strongly encourage switching to and using** the new publishable (`sb_publishable_xxx`) and secret (`sb_secret_xxx`) keys now.\\\\n\\\\n In most cases, you can get keys from [the Project's **Connect** dialog](/dashboard/project/_?showConnect=true\\\\\\\\&connectTab=frameworks\\\\\\\\&framework=), but if you want a specific key, you can find them in the [**Settings > API Keys**](/dashboard/project/_/settings/api-keys/) section of the Dashboard.\\\\n\\\\n * **For legacy keys**, copy the `anon` key for client-side operations and the `service_role` key for server-side operations from the **Legacy API Keys** tab.\\\\n * **For new keys**, open the **API Keys** tab, if you don't have a publishable key already, click **Create new API Keys**, and copy the value from the **Publishable key** section.\\\\n\\\\n\\\\n\\\\n## Building the app\\\\n\\\\nLet's start building the RedwoodJS app from scratch.\\\\n\\\\n\\\\n RedwoodJS requires Node.js `>= 14.x <= 16.x` and Yarn `>= 1.15`.\\\\n\\\\n\\\\nMake sure you have installed yarn since RedwoodJS relies on it to [manage its packages in workspaces](https://classic.yarnpkg.com/lang/en/docs/workspaces/) for its `web` and `api` \\\\\\\"sides.\\\\\\\"\\\\n\\\\n\\\\n### Initialize a RedwoodJS app\\\\n\\\\nWe can use [Create Redwood App](https://redwoodjs.com/docs/quick-start) command to initialize\\\\nan app called `supabase-redwoodjs`:\\\\n\\\\n```bash\\\\nyarn create redwood-app supabase-redwoodjs\\\\ncd supabase-redwoodjs\\\\n```\\\\n\\\\nWhile the app is installing, you should see:\\\\n\\\\n```bash\\\\n✔ Creating Redwood app\\\\n ✔ Checking node and yarn compatibility\\\\n ✔ Creating directory 'supabase-redwoodjs'\\\\n✔ Installing packages\\\\n ✔ Running 'yarn install'... (This could take a while)\\\\n✔ Convert TypeScript files to JavaScript\\\\n✔ Generating types\\\\n\\\\nThanks for trying out Redwood!\\\\n```\\\\n\\\\nThen let's install the only additional dependency [supabase-js](https://github.com/supabase/supabase-js) by running the `setup auth` command:\\\\n\\\\n```bash\\\\nyarn redwood setup auth supabase\\\\n```\\\\n\\\\nWhen prompted:\\\\n\\\\n> Overwrite existing /api/src/lib/auth.\\\\\\\\[jt]s?\\\\n\\\\nSay, **yes** and it will setup the Supabase client in your app and also provide hooks used with Supabase authentication.\\\\n\\\\n```bash\\\\n✔ Generating auth lib...\\\\n ✔ Successfully wrote file `./api/src/lib/auth.js`\\\\n ✔ Adding auth config to web...\\\\n ✔ Adding auth config to GraphQL API...\\\\n ✔ Adding required web packages...\\\\n ✔ Installing packages...\\\\n ✔ One more thing...\\\\n\\\\n You will need to add your Supabase URL (SUPABASE_URL), public API KEY,\\\\n and JWT SECRET (SUPABASE_KEY, and SUPABASE_JWT_SECRET) to your .env file.\\\\n```\\\\n\\\\nNext, we want to save the environment variables in a `.env`.\\\\nWe need the `API URL` as well as the key and `jwt_secret` that you copied [earlier](#get-api-details).\\\\n\\\\n\\\\n \\\\n ```bash name=.env\\\\n SUPABASE_URL=YOUR_SUPABASE_URL\\\\n SUPABASE_KEY=YOUR_SUPABASE_PUBLISHABLE_KEY\\\\n SUPABASE_JWT_SECRET=YOUR_SUPABASE_JWT_SECRET\\\\n ```\\\\n \\\\n\\\\n\\\\nAnd finally, you will also need to save **just** the `web side` environment variables to the `redwood.toml`.\\\\n\\\\n\\\\n \\\\n ```bash name=redwood.toml\\\\n [web]\\\\n title = \\\\\\\"Supabase Redwood Tutorial\\\\\\\"\\\\n port = 8910\\\\n apiProxyPath = \\\\\\\"/.redwood/functions\\\\\\\"\\\\n includeEnvironmentVariables = [\\\\\\\"SUPABASE_URL\\\\\\\", \\\\\\\"SUPABASE_KEY\\\\\\\"]\\\\n [api]\\\\n port = 8911\\\\n [browser]\\\\n open = true\\\\n ```\\\\n \\\\n\\\\n\\\\nThese variables will be exposed on the browser, and that's completely fine.\\\\nThey allow your web app to initialize the Supabase client with your publishable key\\\\nsince we have [Row Level Security](/docs/guides/auth#row-level-security) enabled on our Database.\\\\n\\\\nYou'll see these being used to configure your Supabase client in `web/src/App.js`:\\\\n\\\\n\\\\n \\\\n ```js name=web/src/App.js\\\\n // ... Redwood imports\\\\n import { AuthProvider } from '@redwoodjs/auth'\\\\n import { createClient } from '@supabase/supabase-js'\\\\n\\\\n // ...\\\\n\\\\n const supabase = createClient(process.env.SUPABASE_URL, process.env.SUPABASE_KEY)\\\\n\\\\n const App = () => (\\\\n \\\\n \\\\n \\\\n \\\\n \\\\n \\\\n \\\\n \\\\n \\\\n )\\\\n\\\\n export default App\\\\n ```\\\\n \\\\n\\\\n\\\\n\\\\n### App styling (optional)\\\\n\\\\nAn optional step is to update the CSS file `web/src/index.css` to make the app look better.\\\\nYou can find the full contents of this file [in the example repository](https://raw.githubusercontent.com/supabase/supabase/master/examples/user-management/react-user-management/src/index.css).\\\\n\\\\n\\\\n### Start RedwoodJS and your first page\\\\n\\\\nLet's test our setup at the moment by starting up the app:\\\\n\\\\n```bash\\\\nyarn rw dev\\\\n```\\\\n\\\\n\\\\n `rw` is an alias for `redwood`, as in `yarn rw` to run Redwood CLI commands.\\\\n\\\\n\\\\nYou should see a \\\\\\\"Welcome to RedwoodJS\\\\\\\" page and a message about not having any pages yet.\\\\n\\\\nSo, let's create a \\\\\\\"home\\\\\\\" page:\\\\n\\\\n```bash\\\\nyarn rw generate page home /\\\\n\\\\n✔ Generating page files...\\\\n ✔ Successfully wrote file `./web/src/pages/HomePage/HomePage.stories.js`\\\\n ✔ Successfully wrote file `./web/src/pages/HomePage/HomePage.test.js`\\\\n ✔ Successfully wrote file `./web/src/pages/HomePage/HomePage.js`\\\\n✔ Updating routes file...\\\\n✔ Generating types ...\\\\n```\\\\n\\\\n\\\\n The `/` is important here as it creates a root level route.\\\\n\\\\n\\\\nYou can stop the `dev` server if you want; to see your changes, just be sure to run `yarn rw dev` again.\\\\n\\\\nYou should see the `Home` page route in `web/src/Routes.js`:\\\\n\\\\n\\\\n \\\\n ```bash name=web/src/Routes.js\\\\n import { Router, Route } from '@redwoodjs/router'\\\\n\\\\n const Routes = () => {\\\\n return (\\\\n \\\\n \\\\n \\\\n \\\\n )\\\\n }\\\\n\\\\n export default Routes\\\\n ```\\\\n \\\\n\\\\n\\\\n\\\\n### Set up a login component\\\\n\\\\nLet's set up a Redwood component to manage logins and sign ups. We'll use Magic Links, so users can sign in with their email without using passwords.\\\\n\\\\n```bash\\\\nyarn rw g component auth\\\\n\\\\n ✔ Generating component files...\\\\n ✔ Successfully wrote file `./web/src/components/Auth/Auth.test.js`\\\\n ✔ Successfully wrote file `./web/src/components/Auth/Auth.stories.js`\\\\n ✔ Successfully wrote file `./web/src/components/Auth/Auth.js`\\\\n\\\\n```\\\\n\\\\nNow, update the `Auth.js` component to contain:\\\\n\\\\n\\\\n \\\\n ```jsx name=/web/src/components/Auth/Auth.js\\\\n import { useState } from 'react'\\\\n import { useAuth } from '@redwoodjs/auth'\\\\n\\\\n const Auth = () => {\\\\n const { logIn } = useAuth()\\\\n const [loading, setLoading] = useState(false)\\\\n const [email, setEmail] = useState('')\\\\n\\\\n const handleLogin = async (email) => {\\\\n try {\\\\n setLoading(true)\\\\n const { error } = await logIn({ email })\\\\n if (error) throw error\\\\n alert('Check your email for the login link!')\\\\n } catch (error) {\\\\n alert(error.error_description || error.message)\\\\n } finally {\\\\n setLoading(false)\\\\n }\\\\n }\\\\n\\\\n return (\\\\n
    \\\\n
    \\\\n

    Supabase + RedwoodJS

    \\\\n

    Sign in via magic link with your email below

    \\\\n
    \\\\n setEmail(e.target.value)}\\\\n />\\\\n
    \\\\n
    \\\\n {\\\\n e.preventDefault()\\\\n handleLogin(email)\\\\n }}\\\\n className={'button block'}\\\\n disabled={loading}\\\\n >\\\\n {loading ? Loading : Send magic link}\\\\n \\\\n
    \\\\n
    \\\\n
    \\\\n )\\\\n }\\\\n\\\\n export default Auth\\\\n ```\\\\n
    \\\\n
    \\\\n\\\\n\\\\n### Set up an account component\\\\n\\\\nAfter a user is signed in we can allow them to edit their profile details and manage their account.\\\\n\\\\nLet's create a new component for that called `Account.js`.\\\\n\\\\n```bash\\\\nyarn rw g component account\\\\n\\\\n ✔ Generating component files...\\\\n ✔ Successfully wrote file `./web/src/components/Account/Account.test.js`\\\\n ✔ Successfully wrote file `./web/src/components/Account/Account.stories.js`\\\\n ✔ Successfully wrote file `./web/src/components/Account/Account.js`\\\\n```\\\\n\\\\nAnd then update the file to contain:\\\\n\\\\n\\\\n \\\\n ```jsx name=web/src/components/Account/Account.js\\\\n import { useState, useEffect } from 'react'\\\\n import { useAuth } from '@redwoodjs/auth'\\\\n\\\\n const Account = () => {\\\\n const { client: supabase, currentUser, logOut } = useAuth()\\\\n const [loading, setLoading] = useState(true)\\\\n const [username, setUsername] = useState(null)\\\\n const [website, setWebsite] = useState(null)\\\\n const [avatar_url, setAvatarUrl] = useState(null)\\\\n\\\\n useEffect(() => {\\\\n getProfile()\\\\n }, [supabase.auth.session])\\\\n\\\\n async function getProfile() {\\\\n try {\\\\n setLoading(true)\\\\n const user = supabase.auth.user()\\\\n\\\\n const { data, error, status } = await supabase\\\\n .from('profiles')\\\\n .select(`username, website, avatar_url`)\\\\n .eq('id', user.id)\\\\n .single()\\\\n\\\\n if (error && status !== 406) {\\\\n throw error\\\\n }\\\\n\\\\n if (data) {\\\\n setUsername(data.username)\\\\n setWebsite(data.website)\\\\n setAvatarUrl(data.avatar_url)\\\\n }\\\\n } catch (error) {\\\\n alert(error.message)\\\\n } finally {\\\\n setLoading(false)\\\\n }\\\\n }\\\\n\\\\n async function updateProfile({ username, website, avatar_url }) {\\\\n try {\\\\n setLoading(true)\\\\n const user = supabase.auth.user()\\\\n\\\\n const updates = {\\\\n id: user.id,\\\\n username,\\\\n website,\\\\n avatar_url,\\\\n updated_at: new Date(),\\\\n }\\\\n\\\\n const { error } = await supabase.from('profiles').upsert(updates, {\\\\n returning: 'minimal', // Don't return the value after inserting\\\\n })\\\\n\\\\n if (error) {\\\\n throw error\\\\n }\\\\n\\\\n alert('Updated profile!')\\\\n } catch (error) {\\\\n alert(error.message)\\\\n } finally {\\\\n setLoading(false)\\\\n }\\\\n }\\\\n\\\\n return (\\\\n
    \\\\n
    \\\\n

    Supabase + RedwoodJS

    \\\\n

    Your profile

    \\\\n
    \\\\n
    \\\\n \\\\n \\\\n
    \\\\n
    \\\\n \\\\n setUsername(e.target.value)}\\\\n />\\\\n
    \\\\n
    \\\\n \\\\n setWebsite(e.target.value)}\\\\n />\\\\n
    \\\\n\\\\n
    \\\\n updateProfile({ username, website, avatar_url })}\\\\n disabled={loading}\\\\n >\\\\n {loading ? 'Loading ...' : 'Update'}\\\\n \\\\n
    \\\\n\\\\n
    \\\\n \\\\n
    \\\\n
    \\\\n
    \\\\n
    \\\\n )\\\\n }\\\\n\\\\n export default Account\\\\n ```\\\\n
    \\\\n
    \\\\n\\\\nYou'll see the use of `useAuth()` several times. Redwood's `useAuth` hook provides convenient ways to access\\\\n`logIn`, `logOut`, `currentUser`, and access the `supabase` authenticate client. We'll use it to get an instance\\\\nof the Supabase client to interact with your API.\\\\n\\\\n\\\\n### Update home page\\\\n\\\\nWith all the components in place, update your `HomePage` page to use them:\\\\n\\\\n\\\\n \\\\n ```jsx name=web/src/pages/HomePage/HomePage.js\\\\n import { useAuth } from '@redwoodjs/auth'\\\\n import { MetaTags } from '@redwoodjs/web'\\\\n\\\\n import Account from 'src/components/Account'\\\\n import Auth from 'src/components/Auth'\\\\n\\\\n const HomePage = () => {\\\\n const { isAuthenticated } = useAuth()\\\\n\\\\n return (\\\\n <>\\\\n \\\\n {!isAuthenticated ? : }\\\\n \\\\n )\\\\n }\\\\n\\\\n export default HomePage\\\\n ```\\\\n \\\\n\\\\n\\\\n\\\\n What we're doing here is showing the sign in form if you aren't logged in and your account profile if you are.\\\\n\\\\n\\\\n\\\\n## Profile photos\\\\n\\\\nNext, add a way for users to upload a profile photo. Supabase configures every project with [Storage](/docs/guides/storage) for managing large files like photos and videos.\\\\n\\\\n\\\\n### Create an upload widget\\\\n\\\\nLet's create an avatar for the user so that they can upload a profile photo. We can start by creating a new component:\\\\n\\\\n```bash\\\\nyarn rw g component avatar\\\\n ✔ Generating component files...\\\\n ✔ Successfully wrote file `./web/src/components/Avatar/Avatar.test.js`\\\\n ✔ Successfully wrote file `./web/src/components/Avatar/Avatar.stories.js`\\\\n ✔ Successfully wrote file `./web/src/components/Avatar/Avatar.js`\\\\n```\\\\n\\\\nNow, update your Avatar component to contain the following widget:\\\\n\\\\n\\\\n \\\\n ```jsx name=web/src/components/Avatar/Avatar.js\\\\n import { useEffect, useState } from 'react'\\\\n import { useAuth } from '@redwoodjs/auth'\\\\n\\\\n const Avatar = ({ url, size, onUpload }) => {\\\\n const { client: supabase } = useAuth()\\\\n\\\\n const [avatarUrl, setAvatarUrl] = useState(null)\\\\n const [uploading, setUploading] = useState(false)\\\\n\\\\n useEffect(() => {\\\\n if (url) downloadImage(url)\\\\n }, [url])\\\\n\\\\n async function downloadImage(path) {\\\\n try {\\\\n const { data, error } = await supabase.storage.from('avatars').download(path)\\\\n if (error) {\\\\n throw error\\\\n }\\\\n const url = URL.createObjectURL(data)\\\\n setAvatarUrl(url)\\\\n } catch (error) {\\\\n console.log('Error downloading image: ', error.message)\\\\n }\\\\n }\\\\n\\\\n async function uploadAvatar(event) {\\\\n try {\\\\n setUploading(true)\\\\n\\\\n if (!event.target.files || event.target.files.length === 0) {\\\\n throw new Error('You must select an image to upload.')\\\\n }\\\\n\\\\n const file = event.target.files[0]\\\\n const fileExt = file.name.split('.').pop()\\\\n const fileName = `${Math.random()}.${fileExt}`\\\\n const filePath = `${fileName}`\\\\n\\\\n const { error: uploadError } = await supabase.storage.from('avatars').upload(filePath, file)\\\\n\\\\n if (uploadError) {\\\\n throw uploadError\\\\n }\\\\n\\\\n onUpload(filePath)\\\\n } catch (error) {\\\\n alert(error.message)\\\\n } finally {\\\\n setUploading(false)\\\\n }\\\\n }\\\\n\\\\n return (\\\\n
    \\\\n {avatarUrl ? (\\\\n \\\\n ) : (\\\\n
    \\\\n )}\\\\n
    \\\\n \\\\n \\\\n
    \\\\n
    \\\\n )\\\\n }\\\\n\\\\n export default Avatar\\\\n ```\\\\n \\\\n\\\\n\\\\n\\\\n### Launch!\\\\n\\\\nOnce that's done, run this in a terminal window to launch the `dev` server:\\\\n\\\\n```bash\\\\nyarn rw dev\\\\n```\\\\n\\\\nAnd then open the browser to [localhost:8910](http://localhost:8910) and you should see the completed app.\\\\n\\\\n![Supabase RedwoodJS](/docs/img/supabase-redwoodjs-demo.png)\\\\n\\\\nAt this stage you have a fully functional application!\\\\n\\\\n\\\\n## See also\\\\n\\\\n* Learn more about [RedwoodJS](https://redwoodjs.com)\\\\n* Visit the [RedwoodJS Discourse Community](https://community.redwoodjs.com)\\\\n\\\"},{\\\"__typename\\\":\\\"Guide\\\",\\\"title\\\":\\\"Testing Overview\\\",\\\"href\\\":\\\"https://supabase.com/docs/guides/local-development/testing/overview\\\",\\\"content\\\":\\\"# Testing Overview\\\\n\\\\n\\\\n\\\\nTesting is a critical part of database development, especially when working with features like Row Level Security (RLS) policies. This guide provides a comprehensive approach to testing your Supabase database.\\\\n\\\\n\\\\n## Testing approaches\\\\n\\\\n\\\\n### Database unit testing with pgTAP\\\\n\\\\n[pgTAP](https://pgtap.org) is a unit testing framework for Postgres that allows testing:\\\\n\\\\n* Database structure: tables, columns, constraints\\\\n* Row Level Security (RLS) policies\\\\n* Functions and procedures\\\\n* Data integrity\\\\n\\\\nThis example demonstrates setting up and testing RLS policies for a simple todo application:\\\\n\\\\n1. Create a test table with RLS enabled:\\\\n\\\\n ```sql\\\\n -- Create a simple todos table\\\\n create table todos (\\\\n id uuid primary key default gen_random_uuid(),\\\\n task text not null,\\\\n user_id uuid references auth.users not null,\\\\n completed boolean default false\\\\n );\\\\n\\\\n -- Enable RLS\\\\n alter table todos enable row level security;\\\\n\\\\n -- Create a policy\\\\n create policy \\\\\\\"Users can only access their own todos\\\\\\\"\\\\n on todos for all -- this policy applies to all operations\\\\n to authenticated\\\\n using ((select auth.uid()) = user_id);\\\\n ```\\\\n\\\\n2. Set up your testing environment:\\\\n\\\\n ```bash\\\\n # Create a new test for our policies using supabase cli\\\\n supabase test new todos_rls.test\\\\n ```\\\\n\\\\n3. Write your RLS tests:\\\\n\\\\n ```sql\\\\n begin;\\\\n -- install tests utilities\\\\n -- install pgtap extension for testing\\\\n create extension if not exists pgtap with schema extensions;\\\\n -- Start declare we'll have 4 test cases in our test suite\\\\n select plan(4);\\\\n\\\\n -- Setup our testing data\\\\n -- Set up auth.users entries\\\\n insert into auth.users (id, email) values\\\\n \\\\t('123e4567-e89b-12d3-a456-426614174000', 'user1@test.com'),\\\\n \\\\t('987fcdeb-51a2-43d7-9012-345678901234', 'user2@test.com');\\\\n\\\\n -- Create test todos\\\\n insert into public.todos (task, user_id) values\\\\n \\\\t('User 1 Task 1', '123e4567-e89b-12d3-a456-426614174000'),\\\\n \\\\t('User 1 Task 2', '123e4567-e89b-12d3-a456-426614174000'),\\\\n \\\\t('User 2 Task 1', '987fcdeb-51a2-43d7-9012-345678901234');\\\\n\\\\n -- as User 1\\\\n set local role authenticated;\\\\n set local request.jwt.claim.sub = '123e4567-e89b-12d3-a456-426614174000';\\\\n\\\\n -- Test 1: User 1 should only see their own todos\\\\n select results_eq(\\\\n \\\\t'select count(*) from todos',\\\\n \\\\tARRAY[2::bigint],\\\\n \\\\t'User 1 should only see their 2 todos'\\\\n );\\\\n\\\\n -- Test 2: User 1 can create their own todo\\\\n select lives_ok(\\\\n \\\\t$$insert into todos (task, user_id) values ('New Task', '123e4567-e89b-12d3-a456-426614174000'::uuid)$$,\\\\n \\\\t'User 1 can create their own todo'\\\\n );\\\\n\\\\n -- as User 2\\\\n set local request.jwt.claim.sub = '987fcdeb-51a2-43d7-9012-345678901234';\\\\n\\\\n -- Test 3: User 2 should only see their own todos\\\\n select results_eq(\\\\n \\\\t'select count(*) from todos',\\\\n \\\\tARRAY[1::bigint],\\\\n \\\\t'User 2 should only see their 1 todo'\\\\n );\\\\n\\\\n -- Test 4: User 2 cannot modify User 1's todo\\\\n SELECT results_ne(\\\\n \\\\t$$ update todos set task = 'Hacked!' where user_id = '123e4567-e89b-12d3-a456-426614174000'::uuid returning 1 $$,\\\\n \\\\t$$ values(1) $$,\\\\n \\\\t'User 2 cannot modify User 1 todos'\\\\n );\\\\n\\\\n select * from finish();\\\\n rollback;\\\\n ```\\\\n\\\\n4. Run the tests:\\\\n\\\\n ```bash\\\\n supabase test db\\\\n psql:todos_rls.test.sql:4: NOTICE: extension \\\\\\\"pgtap\\\\\\\" already exists, skipping\\\\n ./todos_rls.test.sql .. ok\\\\n All tests successful.\\\\n Files=1, Tests=6, 0 wallclock secs ( 0.01 usr + 0.00 sys = 0.01 CPU)\\\\n Result: PASS\\\\n ```\\\\n\\\\n\\\\n### Application-Level testing\\\\n\\\\nTesting through application code provides end-to-end verification. Unlike database-level testing with pgTAP, application-level tests cannot use transactions for isolation.\\\\n\\\\n\\\\n Application-level tests should not rely on a clean database state, as resetting the database before each test can be slow and makes tests difficult to parallelize.\\\\n Instead, design your tests to be independent by using unique user IDs for each test case.\\\\n\\\\n\\\\nHere's an example using TypeScript that mirrors the pgTAP tests above:\\\\n\\\\n```typescript\\\\nimport crypto from 'crypto'\\\\nimport { createClient } from '@supabase/supabase-js'\\\\nimport { beforeAll, describe, expect, it } from 'vitest'\\\\n\\\\ndescribe('Todos RLS', () => {\\\\n // Generate unique IDs for this test suite to avoid conflicts with other tests\\\\n const USER_1_ID = crypto.randomUUID()\\\\n const USER_2_ID = crypto.randomUUID()\\\\n\\\\n const supabase = createClient(process.env.SUPABASE_URL!, process.env.SUPABASE_PUBLISHABLE_KEY!)\\\\n\\\\n beforeAll(async () => {\\\\n // Setup test data specific to this test suite\\\\n const adminSupabase = createClient(process.env.SUPABASE_URL!, process.env.SUPABASE_SECRET_KEY!)\\\\n\\\\n // Create test users with unique IDs\\\\n await adminSupabase.auth.admin.createUser({\\\\n id: USER_1_ID,\\\\n email: `user1-${USER_1_ID}@test.com`,\\\\n password: 'password123',\\\\n // We want the user to be usable right away without email confirmation\\\\n email_confirm: true,\\\\n })\\\\n await adminSupabase.auth.admin.createUser({\\\\n id: USER_2_ID,\\\\n email: `user2-${USER_2_ID}@test.com`,\\\\n password: 'password123',\\\\n email_confirm: true,\\\\n })\\\\n\\\\n // Create initial todos\\\\n await adminSupabase.from('todos').insert([\\\\n { task: 'User 1 Task 1', user_id: USER_1_ID },\\\\n { task: 'User 1 Task 2', user_id: USER_1_ID },\\\\n { task: 'User 2 Task 1', user_id: USER_2_ID },\\\\n ])\\\\n })\\\\n\\\\n it('should allow User 1 to only see their own todos', async () => {\\\\n // Sign in as User 1\\\\n await supabase.auth.signInWithPassword({\\\\n email: `user1-${USER_1_ID}@test.com`,\\\\n password: 'password123',\\\\n })\\\\n\\\\n const { data: todos } = await supabase.from('todos').select('*')\\\\n\\\\n expect(todos).toHaveLength(2)\\\\n todos?.forEach((todo) => {\\\\n expect(todo.user_id).toBe(USER_1_ID)\\\\n })\\\\n })\\\\n\\\\n it('should allow User 1 to create their own todo', async () => {\\\\n await supabase.auth.signInWithPassword({\\\\n email: `user1-${USER_1_ID}@test.com`,\\\\n password: 'password123',\\\\n })\\\\n\\\\n const { error } = await supabase.from('todos').insert({ task: 'New Task', user_id: USER_1_ID })\\\\n\\\\n expect(error).toBeNull()\\\\n })\\\\n\\\\n it('should allow User 2 to only see their own todos', async () => {\\\\n // Sign in as User 2\\\\n await supabase.auth.signInWithPassword({\\\\n email: `user2-${USER_2_ID}@test.com`,\\\\n password: 'password123',\\\\n })\\\\n\\\\n const { data: todos } = await supabase.from('todos').select('*')\\\\n expect(todos).toHaveLength(1)\\\\n todos?.forEach((todo) => {\\\\n expect(todo.user_id).toBe(USER_2_ID)\\\\n })\\\\n })\\\\n\\\\n it('should prevent User 2 from modifying User 1 todos', async () => {\\\\n await supabase.auth.signInWithPassword({\\\\n email: `user2-${USER_2_ID}@test.com`,\\\\n password: 'password123',\\\\n })\\\\n\\\\n // Attempt to update the todos we shouldn't have access to\\\\n // result will be a no-op\\\\n await supabase.from('todos').update({ task: 'Hacked!' }).eq('user_id', USER_1_ID)\\\\n\\\\n // Log back in as User 1 to verify their todos weren't changed\\\\n await supabase.auth.signInWithPassword({\\\\n email: `user1-${USER_1_ID}@test.com`,\\\\n password: 'password123',\\\\n })\\\\n\\\\n // Fetch User 1's todos\\\\n const { data: todos } = await supabase.from('todos').select('*')\\\\n\\\\n // Verify that none of the todos were changed to \\\\\\\"Hacked!\\\\\\\"\\\\n expect(todos).toBeDefined()\\\\n todos?.forEach((todo) => {\\\\n expect(todo.task).not.toBe('Hacked!')\\\\n })\\\\n })\\\\n})\\\\n```\\\\n\\\\n\\\\n#### Test isolation strategies\\\\n\\\\nFor application-level testing, consider these approaches for test isolation:\\\\n\\\\n1. **Unique Identifiers**: Generate unique IDs for each test suite to prevent data conflicts\\\\n2. **Cleanup After Tests**: If necessary, clean up created data in an `afterAll` or `afterEach` hook\\\\n3. **Isolated Data Sets**: Use prefixes or namespaces in data to separate test cases\\\\n\\\\n\\\\n### Continuous integration testing\\\\n\\\\nSet up automated database testing in your CI pipeline:\\\\n\\\\n1. Create a GitHub Actions workflow `.github/workflows/db-tests.yml`:\\\\n\\\\n```yaml\\\\nname: Database Tests\\\\n\\\\non:\\\\n push:\\\\n branches: [main]\\\\n pull_request:\\\\n branches: [main]\\\\n\\\\njobs:\\\\n test:\\\\n runs-on: ubuntu-latest\\\\n\\\\n steps:\\\\n - uses: actions/checkout@v4\\\\n\\\\n - name: Setup Supabase CLI\\\\n uses: supabase/setup-cli@v1\\\\n\\\\n - name: Start Supabase\\\\n run: supabase start\\\\n\\\\n - name: Run Tests\\\\n run: supabase test db\\\\n```\\\\n\\\\n\\\\n## Best practices\\\\n\\\\n1. **Test Data Setup**\\\\n * Use begin and rollback to ensure test isolation\\\\n * Create realistic test data that covers edge cases\\\\n * Use different user roles and permissions in tests\\\\n\\\\n2. **RLS Policy Testing**\\\\n * Test Create, Read, Update, Delete operations\\\\n * Test with different user roles: anonymous and authenticated\\\\n * Test edge cases and potential security bypasses\\\\n * Always test negative cases: what users should not be able to do\\\\n\\\\n3. **CI/CD Integration**\\\\n * Run tests automatically on every pull request\\\\n * Include database tests in deployment pipeline\\\\n * Keep test runs fast using transactions\\\\n\\\\n\\\\n## Real-World examples\\\\n\\\\nFor more complex, real-world examples of database testing, check out:\\\\n\\\\n* [Database Tests Example Repository](https://github.com/usebasejump/basejump/tree/main/supabase/tests/database) - A production-grade example of testing RLS policies\\\\n* [RLS Guide and Best Practices](https://github.com/orgs/supabase/discussions/14576)\\\\n\\\\n\\\\n## Troubleshooting\\\\n\\\\nCommon issues and solutions:\\\\n\\\\n1. **Test Failures Due to RLS**\\\\n * Ensure you've set the correct role `set local role authenticated;`\\\\n * Verify JWT claims are set `set local \\\\\\\"request.jwt.claims\\\\\\\"`\\\\n * Check policy definitions match your test assumptions\\\\n\\\\n2. **CI Pipeline Issues**\\\\n * Verify Supabase CLI is properly installed\\\\n * Ensure database migrations are run before tests\\\\n * Check for proper test isolation using transactions\\\\n\\\\n\\\\n## Additional resources\\\\n\\\\n* [pgTAP Documentation](https://pgtap.org)\\\\n* [Supabase CLI Reference](/docs/reference/cli/supabase-test)\\\\n* [pgTAP Supabase reference](/docs/guides/database/extensions/pgtap?queryGroups=database-method\\\\\\\\&database-method=sql#testing-rls-policies)\\\\n* [Database testing reference](/docs/guides/database/testing)\\\\n\\\"}]}}}\"\n }\n ],\n \"isError\": false\n}" + }, + { + "id": "build-cli-002-declarative-schema-tool-20", + "title": "Tool: files_edit", + "startTime": "2026-08-01T15:58:12.502Z", + "endTime": "2026-08-01T15:59:07.634Z", + "duration": 55132, + "type": "tool_execution", + "status": "success", + "raw": "{\n \"ok\": true\n}", + "input": "{\n \"path\": \"supabase/schemas/products.sql\",\n \"old_string\": \" name text not null,\\n price numeric not null\\n\",\n \"new_string\": \" name text not null,\\n price numeric not null,\\n description text\\n\"\n}", + "output": "{\n \"ok\": true\n}" + }, + { + "id": "build-cli-002-declarative-schema-tool-21", + "title": "Tool: bash", + "startTime": "2026-08-01T15:59:07.634Z", + "endTime": "2026-08-01T15:59:10.385Z", + "duration": 2751, + "type": "tool_execution", + "status": "success", + "raw": "{\n \"exit_code\": 0,\n \"stdout\": \"\",\n \"stderr\": \"Creating shadow database...\\nInitialising schema...\\nSeeding globals from roles.sql...\\nApplying migration 20260101000000_create_products.sql...\\nCreating local database from declarative schemas:\\n • supabase/schemas/products.sql\\nSeeding globals from products.sql...\\nDiffing schemas...\\nfailed to pull docker image: Error response from daemon: toomanyrequests: Rate exceeded\\nRetrying after 4s: public.ecr.aws/supabase/edge-runtime:v1.69.28\\nfailed to pull docker image: Error response from daemon: toomanyrequests: Rate exceeded\\nRetrying after 8s: public.ecr.aws/supabase/edge-runtime:v1.69.28\\nv1.69.28: Pulling from supabase/edge-runtime\\n1aee4545ebb8: Pulling fs layer\\nfd137785c421: Pulling fs layer\\ncf743db6e40b: Pulling fs layer\\n835f99a0977a: Pulling fs layer\\n8c22b93c4c37: Pulling fs layer\\nf8ccf6627173: Pulling fs layer\\n835f99a0977a: Waiting\\n8c22b93c4c37: Waiting\\nf8ccf6627173: Waiting\\ncf743db6e40b: Verifying Checksum\\ncf743db6e40b: Download complete\\nfd137785c421: Verifying Checksum\\nfd137785c421: Download complete\\n1aee4545ebb8: Verifying Checksum\\n1aee4545ebb8: Download complete\\nf8ccf6627173: Verifying Checksum\\nf8ccf6627173: Download complete\\n835f99a0977a: Verifying Checksum\\n835f99a0977a: Download complete\\n1aee4545ebb8: Pull complete\\nfd137785c421: Pull complete\\ncf743db6e40b: Pull complete\\n8c22b93c4c37: Verifying Checksum\\n8c22b93c4c37: Download complete\\n835f99a0977a: Pull complete\\n8c22b93c4c37: Pull complete\\nf8ccf6627173: Pull complete\\nDigest: sha256:29d8fbebb409bd71a68e2efdca20fadcd52af343965a718f4f157b2dff7018ab\\nStatus: Downloaded newer image for public.ecr.aws/supabase/edge-runtime:v1.69.28\\nFinished supabase db diff on branch main.\\n\\nWARNING: The diff tool is not foolproof, so you may need to manually rearrange and modify the generated migration.\\nRun supabase db reset to verify that the new migration does not generate errors.\\nA new version of Supabase CLI is available: v2.111.0 (currently installed v2.67.1)\\nWe recommend updating regularly for new features and bug fixes: https://supabase.com/docs/guides/cli/getting-started#updating-the-supabase-cli\\n\"\n}", + "input": "{\n \"command\": \"supabase db diff -f add_description_to_products\",\n \"timeout_sec\": 120\n}", + "output": "{\n \"exit_code\": 0,\n \"stdout\": \"\",\n \"stderr\": \"Creating shadow database...\\nInitialising schema...\\nSeeding globals from roles.sql...\\nApplying migration 20260101000000_create_products.sql...\\nCreating local database from declarative schemas:\\n • supabase/schemas/products.sql\\nSeeding globals from products.sql...\\nDiffing schemas...\\nfailed to pull docker image: Error response from daemon: toomanyrequests: Rate exceeded\\nRetrying after 4s: public.ecr.aws/supabase/edge-runtime:v1.69.28\\nfailed to pull docker image: Error response from daemon: toomanyrequests: Rate exceeded\\nRetrying after 8s: public.ecr.aws/supabase/edge-runtime:v1.69.28\\nv1.69.28: Pulling from supabase/edge-runtime\\n1aee4545ebb8: Pulling fs layer\\nfd137785c421: Pulling fs layer\\ncf743db6e40b: Pulling fs layer\\n835f99a0977a: Pulling fs layer\\n8c22b93c4c37: Pulling fs layer\\nf8ccf6627173: Pulling fs layer\\n835f99a0977a: Waiting\\n8c22b93c4c37: Waiting\\nf8ccf6627173: Waiting\\ncf743db6e40b: Verifying Checksum\\ncf743db6e40b: Download complete\\nfd137785c421: Verifying Checksum\\nfd137785c421: Download complete\\n1aee4545ebb8: Verifying Checksum\\n1aee4545ebb8: Download complete\\nf8ccf6627173: Verifying Checksum\\nf8ccf6627173: Download complete\\n835f99a0977a: Verifying Checksum\\n835f99a0977a: Download complete\\n1aee4545ebb8: Pull complete\\nfd137785c421: Pull complete\\ncf743db6e40b: Pull complete\\n8c22b93c4c37: Verifying Checksum\\n8c22b93c4c37: Download complete\\n835f99a0977a: Pull complete\\n8c22b93c4c37: Pull complete\\nf8ccf6627173: Pull complete\\nDigest: sha256:29d8fbebb409bd71a68e2efdca20fadcd52af343965a718f4f157b2dff7018ab\\nStatus: Downloaded newer image for public.ecr.aws/supabase/edge-runtime:v1.69.28\\nFinished supabase db diff on branch main.\\n\\nWARNING: The diff tool is not foolproof, so you may need to manually rearrange and modify the generated migration.\\nRun supabase db reset to verify that the new migration does not generate errors.\\nA new version of Supabase CLI is available: v2.111.0 (currently installed v2.67.1)\\nWe recommend updating regularly for new features and bug fixes: https://supabase.com/docs/guides/cli/getting-started#updating-the-supabase-cli\\n\"\n}" + }, + { + "id": "build-cli-002-declarative-schema-tool-22", + "title": "Tool: bash", + "startTime": "2026-08-01T15:59:10.385Z", + "endTime": "2026-08-01T15:59:10.393Z", + "duration": 8, + "type": "tool_execution", + "status": "success", + "raw": "{\n \"exit_code\": 0,\n \"stdout\": \"20260101000000_create_products.sql\\n20260801155907_add_description_to_products.sql\\n\",\n \"stderr\": \"\"\n}", + "input": "{\n \"command\": \"find supabase/migrations -maxdepth 1 -type f -printf '%f\\\\n' | sort\",\n \"timeout_sec\": 30\n}", + "output": "{\n \"exit_code\": 0,\n \"stdout\": \"20260101000000_create_products.sql\\n20260801155907_add_description_to_products.sql\\n\",\n \"stderr\": \"\"\n}" + }, + { + "id": "build-cli-002-declarative-schema-tool-23", + "title": "Tool: bash", + "startTime": "2026-08-01T15:59:10.393Z", + "endTime": "2026-08-01T15:59:10.424Z", + "duration": 31, + "type": "tool_execution", + "status": "success", + "raw": "{\n \"exit_code\": 128,\n \"stdout\": \"\",\n \"stderr\": \"fatal: not a git repository (or any parent up to mount point /tmp)\\nStopping at filesystem boundary (GIT_DISCOVERY_ACROSS_FILESYSTEM not set).\\n\"\n}", + "input": "{\n \"command\": \"git status --short && git diff -- supabase/schemas/products.sql supabase/migrations\",\n \"timeout_sec\": 30\n}", + "output": "{\n \"exit_code\": 128,\n \"stdout\": \"\",\n \"stderr\": \"fatal: not a git repository (or any parent up to mount point /tmp)\\nStopping at filesystem boundary (GIT_DISCOVERY_ACROSS_FILESYSTEM not set).\\n\"\n}" + }, + { + "id": "build-cli-002-declarative-schema-tool-24", + "title": "Tool: bash", + "startTime": "2026-08-01T15:59:10.424Z", + "endTime": "2026-08-01T15:59:12.036Z", + "duration": 1612, + "type": "tool_execution", + "status": "success", + "raw": "{\n \"exit_code\": 0,\n \"stdout\": \"id|integer|NO\\nname|text|NO\\nprice|numeric|NO\\n\",\n \"stderr\": \"\"\n}", + "input": "{\n \"command\": \"psql 'postgresql://postgres:REDACTED@127.0.0.1:54322/postgres' -Atc \\\"select column_name, data_type, is_nullable from information_schema.columns where table_schema='public' and table_name='products' order by ordinal_position;\\\"\",\n \"timeout_sec\": 30\n}", + "output": "{\n \"exit_code\": 0,\n \"stdout\": \"id|integer|NO\\nname|text|NO\\nprice|numeric|NO\\n\",\n \"stderr\": \"\"\n}" + }, + { + "id": "build-cli-002-declarative-schema-tool-25", + "title": "Tool: files_read", + "startTime": "2026-08-01T15:59:12.036Z", + "endTime": "2026-08-01T15:59:13.934Z", + "duration": 1898, + "type": "tool_execution", + "status": "success", + "raw": "{\n \"contents\": \"alter table \\\"public\\\".\\\"products\\\" add column \\\"description\\\" text;\\n\\n\\n\"\n}", + "input": "{\n \"path\": \"supabase/migrations/20260801155907_add_description_to_products.sql\"\n}", + "output": "{\n \"contents\": \"alter table \\\"public\\\".\\\"products\\\" add column \\\"description\\\" text;\\n\\n\\n\"\n}" + }, + { + "id": "build-cli-002-declarative-schema-tool-26", + "title": "Tool: bash", + "startTime": "2026-08-01T15:59:13.934Z", + "endTime": "2026-08-01T15:59:17.409Z", + "duration": 3475, + "type": "tool_execution", + "status": "success", + "raw": "{\n \"exit_code\": 0,\n \"stdout\": \"Apply pending migrations to local database\\n\\nUsage:\\n supabase migration up [flags]\\n\\nFlags:\\n --db-url string Applies migrations to the database specified by the connection string (must be percent-encoded).\\n -h, --help help for up\\n --include-all Include all migrations not found on remote history table.\\n --linked Applies pending migrations to the linked project.\\n --local Applies pending migrations to the local database. (default true)\\n\\nGlobal Flags:\\n --create-ticket create a support ticket for any CLI error\\n --debug output debug logs to stderr\\n --dns-resolver [ native | https ] lookup domain names using the specified resolver (default native)\\n --experimental enable experimental features\\n --network-id string use the specified docker network instead of a generated one\\n -o, --output [ env | pretty | json | toml | yaml ] output format of status variables (default pretty)\\n --profile string use a specific profile for connecting to Supabase API (default \\\"supabase\\\")\\n --workdir string path to a Supabase project directory\\n --yes answer yes to all prompts\\nLocal database is up to date.\\n\",\n \"stderr\": \"A new version of Supabase CLI is available: v2.111.0 (currently installed v2.67.1)\\nWe recommend updating regularly for new features and bug fixes: https://supabase.com/docs/guides/cli/getting-started#updating-the-supabase-cli\\nConnecting to local database...\\nApplying migration 20260801155907_add_description_to_products.sql...\\nA new version of Supabase CLI is available: v2.111.0 (currently installed v2.67.1)\\nWe recommend updating regularly for new features and bug fixes: https://supabase.com/docs/guides/cli/getting-started#updating-the-supabase-cli\\n\"\n}", + "input": "{\n \"command\": \"supabase migration up --help && supabase migration up\",\n \"timeout_sec\": 120\n}", + "output": "{\n \"exit_code\": 0,\n \"stdout\": \"Apply pending migrations to local database\\n\\nUsage:\\n supabase migration up [flags]\\n\\nFlags:\\n --db-url string Applies migrations to the database specified by the connection string (must be percent-encoded).\\n -h, --help help for up\\n --include-all Include all migrations not found on remote history table.\\n --linked Applies pending migrations to the linked project.\\n --local Applies pending migrations to the local database. (default true)\\n\\nGlobal Flags:\\n --create-ticket create a support ticket for any CLI error\\n --debug output debug logs to stderr\\n --dns-resolver [ native | https ] lookup domain names using the specified resolver (default native)\\n --experimental enable experimental features\\n --network-id string use the specified docker network instead of a generated one\\n -o, --output [ env | pretty | json | toml | yaml ] output format of status variables (default pretty)\\n --profile string use a specific profile for connecting to Supabase API (default \\\"supabase\\\")\\n --workdir string path to a Supabase project directory\\n --yes answer yes to all prompts\\nLocal database is up to date.\\n\",\n \"stderr\": \"A new version of Supabase CLI is available: v2.111.0 (currently installed v2.67.1)\\nWe recommend updating regularly for new features and bug fixes: https://supabase.com/docs/guides/cli/getting-started#updating-the-supabase-cli\\nConnecting to local database...\\nApplying migration 20260801155907_add_description_to_products.sql...\\nA new version of Supabase CLI is available: v2.111.0 (currently installed v2.67.1)\\nWe recommend updating regularly for new features and bug fixes: https://supabase.com/docs/guides/cli/getting-started#updating-the-supabase-cli\\n\"\n}" + }, + { + "id": "build-cli-002-declarative-schema-tool-27", + "title": "Tool: bash", + "startTime": "2026-08-01T15:59:17.409Z", + "endTime": "2026-08-01T15:59:17.418Z", + "duration": 9, + "type": "tool_execution", + "status": "success", + "raw": "{\n \"exit_code\": 0,\n \"stdout\": \" column_name | data_type | is_nullable \\n-------------+-----------+-------------\\n id | integer | NO\\n name | text | NO\\n price | numeric | NO\\n description | text | YES\\n(4 rows)\\n\\n\",\n \"stderr\": \"\"\n}", + "input": "{\n \"command\": \"psql 'postgresql://postgres:REDACTED@127.0.0.1:54322/postgres' -P pager=off -c \\\"select column_name, data_type, is_nullable from information_schema.columns where table_schema='public' and table_name='products' order by ordinal_position;\\\"\",\n \"timeout_sec\": 30\n}", + "output": "{\n \"exit_code\": 0,\n \"stdout\": \" column_name | data_type | is_nullable \\n-------------+-----------+-------------\\n id | integer | NO\\n name | text | NO\\n price | numeric | NO\\n description | text | YES\\n(4 rows)\\n\\n\",\n \"stderr\": \"\"\n}" + }, + { + "id": "build-cli-002-declarative-schema-tool-28", + "title": "Tool: bash", + "startTime": "2026-08-01T15:59:17.418Z", + "endTime": "2026-08-01T15:59:17.441Z", + "duration": 23, + "type": "tool_execution", + "status": "success", + "raw": "{\n \"exit_code\": 0,\n \"stdout\": \"\\n \\n Local | Remote | Time (UTC) \\n ----------------|----------------|---------------------\\n 20260101000000 | 20260101000000 | 2026-01-01 00:00:00 \\n 20260801155907 | 20260801155907 | 2026-08-01 15:59:07 \\n\\n\",\n \"stderr\": \"Connecting to local database...\\nA new version of Supabase CLI is available: v2.111.0 (currently installed v2.67.1)\\nWe recommend updating regularly for new features and bug fixes: https://supabase.com/docs/guides/cli/getting-started#updating-the-supabase-cli\\n\"\n}", + "input": "{\n \"command\": \"supabase migration list --local\",\n \"timeout_sec\": 30\n}", + "output": "{\n \"exit_code\": 0,\n \"stdout\": \"\\n \\n Local | Remote | Time (UTC) \\n ----------------|----------------|---------------------\\n 20260101000000 | 20260101000000 | 2026-01-01 00:00:00 \\n 20260801155907 | 20260801155907 | 2026-08-01 15:59:07 \\n\\n\",\n \"stderr\": \"Connecting to local database...\\nA new version of Supabase CLI is available: v2.111.0 (currently installed v2.67.1)\\nWe recommend updating regularly for new features and bug fixes: https://supabase.com/docs/guides/cli/getting-started#updating-the-supabase-cli\\n\"\n}" + }, + { + "id": "build-cli-002-declarative-schema-tool-29", + "title": "Tool: bash", + "startTime": "2026-08-01T15:59:17.441Z", + "endTime": "2026-08-01T15:59:18.957Z", + "duration": 1516, + "type": "tool_execution", + "status": "success", + "raw": "{\n \"exit_code\": 0,\n \"stdout\": \"t\\n\",\n \"stderr\": \"\"\n}", + "input": "{\n \"command\": \"psql 'postgresql://postgres:REDACTED@127.0.0.1:54322/postgres' -Atc \\\"select exists (select 1 from information_schema.columns where table_schema='public' and table_name='products' and column_name='description' and data_type='text') as description_column_exists;\\\"\",\n \"timeout_sec\": 30\n}", + "output": "{\n \"exit_code\": 0,\n \"stdout\": \"t\\n\",\n \"stderr\": \"\"\n}" + }, + { + "id": "build-cli-002-declarative-schema-llm-30", + "title": "Assistant", + "startTime": "2026-08-01T15:59:18.957Z", + "endTime": "2026-08-01T15:59:18.957Z", + "duration": 0, + "type": "llm_call", + "status": "success", + "raw": "Added nullable `description text` to `public.products`.\n\nUpdated:\n- `supabase/schemas/products.sql`\n- New migration: `20260801155907_add_description_to_products.sql`\n\nApplied and verified in the local Supabase database.", + "output": "Added nullable `description text` to `public.products`.\n\nUpdated:\n- `supabase/schemas/products.sql`\n- New migration: `20260801155907_add_description_to_products.sql`\n\nApplied and verified in the local Supabase database.", + "tokensCount": 35787 + }, + { + "id": "build-cli-002-declarative-schema-check-31", + "title": "Check passed: supabase db diff used to generate the migration", + "startTime": "2026-08-01T15:59:18.957Z", + "endTime": "2026-08-01T15:59:18.957Z", + "duration": 0, + "type": "event", + "status": "success", + "raw": "", + "output": "" + }, + { + "id": "build-cli-002-declarative-schema-check-32", + "title": "Check passed: schema file updated to include description column", + "startTime": "2026-08-01T15:59:18.957Z", + "endTime": "2026-08-01T15:59:18.957Z", + "duration": 0, + "type": "event", + "status": "success", + "raw": "", + "output": "" + }, + { + "id": "build-cli-002-declarative-schema-check-33", + "title": "Check passed: a new migration was generated for the change", + "startTime": "2026-08-01T15:59:18.957Z", + "endTime": "2026-08-01T15:59:18.957Z", + "duration": 0, + "type": "event", + "status": "success", + "raw": "", + "output": "" + }, + { + "id": "build-cli-002-declarative-schema-check-34", + "title": "Check passed: description column exists in the live database", + "startTime": "2026-08-01T15:59:18.957Z", + "endTime": "2026-08-01T15:59:18.957Z", + "duration": 0, + "type": "event", + "status": "success", + "raw": "", + "output": "" + } + ], + "tokensCount": 35787 + } + ], + "badges": [ + { + "label": "Passed", + "tone": "success" + }, + { + "label": "0/2 skills", + "tone": "neutral" + }, + { + "label": "gpt-5.6-luna", + "tone": "neutral" + } + ] +} diff --git a/apps/web/src/data/traces/build-cli-003-pg-cron-queue-workflow.json b/apps/web/src/data/traces/build-cli-003-pg-cron-queue-workflow.json new file mode 100644 index 00000000..613dd035 --- /dev/null +++ b/apps/web/src/data/traces/build-cli-003-pg-cron-queue-workflow.json @@ -0,0 +1,335 @@ +{ + "traceRecord": { + "id": "build-cli-003-pg-cron-queue-workflow", + "name": "build-cli-003-pg-cron-queue-workflow", + "spansCount": 26, + "durationMs": 76870, + "agentDescription": "gpt-5.6-luna", + "totalTokens": 31972 + }, + "spans": [ + { + "id": "eval:build-cli-003-pg-cron-queue-workflow", + "title": "build-cli-003-pg-cron-queue-workflow", + "startTime": "2026-08-01T16:00:07.896Z", + "endTime": "2026-08-01T16:01:24.766Z", + "duration": 76870, + "type": "agent_invocation", + "status": "success", + "raw": "Implemented:\n\n- Added `tasks` queue and `enqueue-tasks` cron job running every minute.\n- Added `process-tasks` Edge Function that reads up to 10 messages and deletes successfully processed messages.\n- Applied and verified the migration locally.\n- Tested the function; queued messages were drained successfully.", + "output": "Implemented:\n\n- Added `tasks` queue and `enqueue-tasks` cron job running every minute.\n- Added `process-tasks` Edge Function that reads up to 10 messages and deletes successfully processed messages.\n- Applied and verified the migration locally.\n- Tested the function; queued messages were drained successfully.", + "children": [ + { + "id": "build-cli-003-pg-cron-queue-workflow-msg-1", + "title": "System", + "startTime": "2026-08-01T16:00:07.896Z", + "endTime": "2026-08-01T16:00:07.896Z", + "duration": 0, + "type": "event", + "status": "success", + "raw": "You are an agent solving a Supabase eval task in a Linux workspace. Use the provided tools to inspect and modify the workspace and run commands. When you are done, end your turn with a short summary of what you did.\n\nThe Supabase CLI (`supabase`), docker, psql, git, and curl are installed in the workspace. Use the bash tool to run commands (the working directory is always the workspace root) and the files tools to inspect and modify files. Services started with `supabase start` are reachable on their default 127.0.0.1 ports.\n\n## Available skills\n\nThe following agent skills are installed in this workspace under `.claude/skills/`. Only their names and descriptions are shown — the full instructions are not loaded yet. When a task matches a skill, read `.claude/skills//SKILL.md` with the `files_read` tool for its full instructions, then read any files it references in that directory with `files_read` or `bash`.\n\n- supabase: Use when doing ANY task involving Supabase. Triggers: Supabase products (Database, Auth, Edge Functions, Realtime, Storage, Vectors, Cron, Queues); client libraries and SSR integrations (supabase-js, @supabase/ssr) in Next.js, React, SvelteKit, Astro, Remix; auth issues (login, logout, sessions, JWT, cookies, getSession, getUser, getClaims, RLS); Supabase CLI or MCP server; schema changes, migrations, declarative schemas, security audits, Postgres extensions (pg_graphql, pg_cron, pg_vector).\n- supabase-postgres-best-practices: Postgres best practices maintained by Supabase, for Postgres running anywhere. Load this skill BEFORE writing or changing anything that lives in a Postgres database: creating or altering tables and columns (including choosing column types), schema design, migrations and declarative schema files, RLS policies and the tests that verify them, indexes, triggers, database functions, queues and scheduled jobs (pg_cron, pgmq), vector/semantic search (pgvector), and restoring dumps (pg_restore) or importing data. Also load it when diagnosing slow queries, high CPU, timeouts, EXPLAIN plans, connection exhaustion, locking, bloat, or rows visible to the wrong user or tenant. This is not just a performance guide — schema, migration, security, and SQL authoring tasks need these rules too, even for a one-column change or a single query.", + "input": "You are an agent solving a Supabase eval task in a Linux workspace. Use the provided tools to inspect and modify the workspace and run commands. When you are done, end your turn with a short summary of what you did.\n\nThe Supabase CLI (`supabase`), docker, psql, git, and curl are installed in the workspace. Use the bash tool to run commands (the working directory is always the workspace root) and the files tools to inspect and modify files. Services started with `supabase start` are reachable on their default 127.0.0.1 ports.\n\n## Available skills\n\nThe following agent skills are installed in this workspace under `.claude/skills/`. Only their names and descriptions are shown — the full instructions are not loaded yet. When a task matches a skill, read `.claude/skills//SKILL.md` with the `files_read` tool for its full instructions, then read any files it references in that directory with `files_read` or `bash`.\n\n- supabase: Use when doing ANY task involving Supabase. Triggers: Supabase products (Database, Auth, Edge Functions, Realtime, Storage, Vectors, Cron, Queues); client libraries and SSR integrations (supabase-js, @supabase/ssr) in Next.js, React, SvelteKit, Astro, Remix; auth issues (login, logout, sessions, JWT, cookies, getSession, getUser, getClaims, RLS); Supabase CLI or MCP server; schema changes, migrations, declarative schemas, security audits, Postgres extensions (pg_graphql, pg_cron, pg_vector).\n- supabase-postgres-best-practices: Postgres best practices maintained by Supabase, for Postgres running anywhere. Load this skill BEFORE writing or changing anything that lives in a Postgres database: creating or altering tables and columns (including choosing column types), schema design, migrations and declarative schema files, RLS policies and the tests that verify them, indexes, triggers, database functions, queues and scheduled jobs (pg_cron, pgmq), vector/semantic search (pgvector), and restoring dumps (pg_restore) or importing data. Also load it when diagnosing slow queries, high CPU, timeouts, EXPLAIN plans, connection exhaustion, locking, bloat, or rows visible to the wrong user or tenant. This is not just a performance guide — schema, migration, security, and SQL authoring tasks need these rules too, even for a one-column change or a single query." + }, + { + "id": "build-cli-003-pg-cron-queue-workflow-msg-2", + "title": "User", + "startTime": "2026-08-01T16:00:07.896Z", + "endTime": "2026-08-01T16:00:07.896Z", + "duration": 0, + "type": "event", + "status": "success", + "raw": "I want to set up a recurring background workflow on my local Supabase stack.\n\nCan you set up a cron job called `enqueue-tasks` to run every minute and push a task into a queue called `tasks`? Then add a `process-tasks` edge function that reads messages off the `tasks` queue and removes them, so a scheduled worker can keep the backlog drained.", + "input": "I want to set up a recurring background workflow on my local Supabase stack.\n\nCan you set up a cron job called `enqueue-tasks` to run every minute and push a task into a queue called `tasks`? Then add a `process-tasks` edge function that reads messages off the `tasks` queue and removes them, so a scheduled worker can keep the backlog drained." + }, + { + "id": "build-cli-003-pg-cron-queue-workflow-tool-3", + "title": "Tool: files_read", + "startTime": "2026-08-01T16:00:07.896Z", + "endTime": "2026-08-01T16:00:07.981Z", + "duration": 85, + "type": "tool_execution", + "status": "success", + "raw": "{\n \"contents\": \"---\\nname: supabase\\ndescription: \\\"Use when doing ANY task involving Supabase. Triggers: Supabase products (Database, Auth, Edge Functions, Realtime, Storage, Vectors, Cron, Queues); client libraries and SSR integrations (supabase-js, @supabase/ssr) in Next.js, React, SvelteKit, Astro, Remix; auth issues (login, logout, sessions, JWT, cookies, getSession, getUser, getClaims, RLS); Supabase CLI or MCP server; schema changes, migrations, declarative schemas, security audits, Postgres extensions (pg_graphql, pg_cron, pg_vector).\\\"\\nmetadata:\\n author: supabase\\n version: \\\"0.1.2\\\"\\n---\\n\\n# Supabase\\n\\n## Core Principles\\n\\n**1. Supabase changes frequently — verify against changelog and current docs before implementing.**\\nDo not rely on training data for Supabase features. Function signatures, config.toml settings, and API conventions change between versions.\\n\\nFirst, fetch `https://supabase.com/changelog.md` (a lightweight summary index — not a heavy pull), scan for `breaking-change` tags relevant to your task, and follow the linked page for any that apply. Then look up the relevant topic using the documentation access methods below.\\n\\n**2. Verify your work.**\\nAfter implementing any fix, run a test query to confirm the change works. A fix without verification is incomplete.\\n\\n**3. Recover from errors, don't loop.**\\nIf an approach fails after 2-3 attempts, stop and reconsider. Try a different method, check documentation, inspect the error more carefully, and review relevant logs when available. Supabase issues are not always solved by retrying the same command, and the answer is not always in the logs, but logs are often worth checking before proceeding.\\n\\n**4. Exposing tables to the Data API:** Depending on the user's [Data API settings](https://supabase.com/dashboard/project//integrations/data_api/settings), newly created tables may not be automatically exposed via the Data (REST) API. If this is the case, `anon` and `authenticated` roles will need to be explicitly granted access.\\n\\n> Note that this is separate from RLS, which controls which _rows_ are visible once a table is accessible, not whether the table is accessible at all.\\n\\nWhen a user reports a SQL-created table is unexpectedly inaccessible, check their Data API settings and whether the roles have been granted access via explicit `GRANT` SQL. When granting public (`anon`/`authenticated`) access, always enable RLS too. See [Exposing a Table to the Data API](https://supabase.com/docs/guides/api/securing-your-api.md) for the full setup workflow.\\n\\n**5. RLS in exposed schemas.**\\nEnable RLS on every table in any exposed schema, which includes `public` by default. This is critical in Supabase because tables in exposed schemas can be reachable through the Data API when the `anon`/`authenticated` roles have access (see [Exposing a Table to the Data API](https://supabase.com/docs/guides/api/securing-your-api.md)). For private schemas, prefer RLS as defense in depth. After enabling RLS, create policies that match the actual access model rather than defaulting every table to the same `auth.uid()` pattern.\\n\\n**6. Security checklist.**\\nWhen working on any Supabase task that touches auth, RLS, views, storage, or user data, run through this checklist. These are Supabase-specific security traps that silently create vulnerabilities:\\n\\n- **Auth and session security**\\n - **Never use `user_metadata` claims in JWT-based authorization decisions.** In Supabase, `raw_user_meta_data` is user-editable and can appear in `auth.jwt()`, so it is unsafe for RLS policies or any other authorization logic. Store authorization data in `raw_app_meta_data` / `app_metadata` instead.\\n - **Deleting a user does not invalidate existing access tokens.** Sign out or revoke sessions first, keep JWT expiry short for sensitive apps, and for strict guarantees validate `session_id` against `auth.sessions` on sensitive operations.\\n - **If you use `app_metadata` or `auth.jwt()` for authorization, remember JWT claims are not always fresh until the user's token is refreshed.**\\n\\n- **API key and client exposure**\\n - **Never expose the `service_role` or secret key in public clients.** Prefer publishable keys for frontend code. Legacy `anon` keys are only for compatibility. In Next.js, any `NEXT_PUBLIC_` env var is sent to the browser.\\n\\n- **RLS, views, and privileged database code**\\n - **Views bypass RLS by default.** In Postgres 15 and above, use `CREATE VIEW ... WITH (security_invoker = true)`. In older versions of Postgres, protect your views by revoking access from the `anon` and `authenticated` roles, or by putting them in an unexposed schema.\\n - **UPDATE requires a SELECT policy.** In Postgres RLS, an UPDATE needs to first SELECT the row. Without a SELECT policy, updates silently return 0 rows — no error, just no change.\\n - **`auth.role()` is deprecated — use the `TO` clause instead.** Supabase has deprecated `auth.role()` in favour of specifying the target role directly on the policy with `TO authenticated` or `TO anon`. Beyond deprecation, `auth.role() = 'authenticated'` breaks silently when anonymous sign-ins are enabled, because anonymous users carry the `authenticated` Postgres role and pass the check regardless of whether the user is genuinely signed in.\\n ```sql\\n -- Deprecated (do not use)\\n create policy \\\"example\\\" on table_name for select\\n using ( auth.role() = 'authenticated' );\\n ```\\n - **`TO authenticated` alone is authentication without authorization (BOLA / IDOR).** Using `TO authenticated` only checks the role — it does not restrict which rows a user can access. The correct pattern combines `TO authenticated` with an ownership predicate in `USING`:\\n ```sql\\n create policy \\\"example\\\" on table_name for select\\n to authenticated\\n using ( (select auth.uid()) = user_id );\\n ```\\n - **UPDATE policies require both `USING` and `WITH CHECK`.** Without `WITH CHECK`, a user can reassign a row's `user_id` to another user:\\n ```sql\\n create policy \\\"example\\\" on table_name for update\\n to authenticated\\n using ( (select auth.uid()) = user_id )\\n with check ( (select auth.uid()) = user_id );\\n ```\\n - **`SECURITY DEFINER` functions bypass RLS.** A `SECURITY DEFINER` function runs with its creator's privileges — typically a role with `bypassrls` (e.g., `postgres`). Never add `SECURITY DEFINER` to resolve a permission error; it silently removes access control without fixing the underlying cause. Prefer `SECURITY INVOKER`.\\n - **`SECURITY DEFINER` functions in `public` are callable by all roles.** Postgres grants `EXECUTE` to `PUBLIC` by default for every new function, so any `SECURITY DEFINER` function in `public` is a public API endpoint callable by `anon` and `authenticated` (which inherit from `PUBLIC`) without any additional grant. When `SECURITY DEFINER` is genuinely needed (e.g., bypassing RLS on an internal lookup table), keep the function in a non-exposed schema, always include an `auth.uid()` check in the function body, and run `supabase db advisors` after making changes.\\n\\n- **Storage access control**\\n - **Storage upsert requires INSERT + SELECT + UPDATE.** Granting only INSERT allows new uploads but file replacement (upsert) silently fails. You need all three.\\n\\n- **Dependency and supply-chain security**\\n - **Always pin package versions and commit lockfiles** when installing Supabase packages (`supabase-js`, `@supabase/ssr`, `supabase-py`, etc.). See the [npm security guide](https://supabase.com/docs/guides/security/npm-security.md) for the full checklist.\\n\\nFor any security concern not covered above, fetch the Supabase product security index: `https://supabase.com/docs/guides/security/product-security.md`\\n\\n## Supabase CLI\\n\\nAlways discover commands via `--help` — never guess. The CLI structure changes between versions.\\n\\n```bash\\nsupabase --help # All top-level commands\\nsupabase --help # Subcommands (e.g., supabase db --help)\\nsupabase --help # Flags for a specific command\\n```\\n\\n**Supabase CLI Known gotchas:**\\n\\n- `supabase db query` requires **CLI v2.79.0+** → use MCP `execute_sql` or `psql` as fallback\\n- `supabase db advisors` requires **CLI v2.81.3+** → use MCP `get_advisors` as fallback\\n- In imperative migration projects, create new hand-authored migration files with `supabase migration new ` first. Never invent a migration filename or rely on memory for the expected format. Declarative schema projects generate migrations from `supabase/schemas/`; see \\\"Making and Committing Schema Changes\\\" below.\\n\\n**Version check and upgrade:** Run `supabase --version` to check. For CLI changelogs and version-specific features, consult the [CLI documentation](https://supabase.com/docs/reference/cli/introduction) or [GitHub releases](https://github.com/supabase/cli/releases).\\n\\n## Supabase MCP Server\\n\\nFor setup instructions, server URL, and configuration, see the [MCP setup guide](https://supabase.com/docs/guides/getting-started/mcp).\\n\\n**Troubleshooting connection issues** — follow these steps in order:\\n\\n1. **Check if the server is reachable:**\\n `curl -so /dev/null -w \\\"%{http_code}\\\" https://mcp.supabase.com/mcp`\\n A `401` is expected (no token) and means the server is up. Timeout or \\\"connection refused\\\" means it may be down.\\n\\n2. **Check `.mcp.json` configuration:**\\n Verify the project root has a valid `.mcp.json` with the correct server URL. If missing, create one pointing to `https://mcp.supabase.com/mcp`.\\n\\n3. **Authenticate the MCP server:**\\n If the server is reachable and `.mcp.json` is correct but tools aren't visible, the user needs to authenticate. The Supabase MCP server uses OAuth 2.1 — tell the user to trigger the auth flow in their agent, complete it in the browser, and reload the session.\\n\\n## Supabase Documentation\\n\\nBefore implementing any Supabase feature, find the relevant documentation. Use these methods in priority order:\\n\\n1. **MCP `search_docs` tool** (preferred — returns relevant snippets directly)\\n2. **Fetch docs pages as markdown** — any docs page can be fetched by appending `.md` to the URL path.\\n3. **Web search** for Supabase-specific topics when you don't know which page to look at.\\n\\n## Making and Committing Schema Changes\\n\\nFirst decide which schema workflow the project uses.\\n\\n### Option A: Declarative schemas\\n\\nUse this when `supabase/schemas/` exists or `config.toml` sets `schema_paths`. Edit the desired schema state in those files, then generate and review the migration. Do not start by hand-writing a migration. See the [Declarative database schemas guide](https://supabase.com/docs/guides/local-development/declarative-database-schemas).\\n\\n### Option B: Imperative migrations\\n\\nUse this when the project does not use declarative schemas.\\n\\n**To make schema changes, use `execute_sql` (MCP) or `supabase db query` (CLI).** These run SQL directly on the database without creating migration history entries, so you can iterate freely and generate a clean migration when ready.\\n\\nDo NOT use `apply_migration` to change a local database schema — it writes a migration history entry on every call, which means you can't iterate, and `supabase db diff` / `supabase db pull` will produce empty or conflicting diffs. If you use it, you'll be stuck with whatever SQL you passed on the first try.\\n\\n**When ready to commit** your changes to a migration file:\\n\\n1. **Run advisors** → `supabase db advisors` (CLI v2.81.3+) or MCP `get_advisors`. Fix any issues.\\n2. **Review the Security Checklist above** if your changes involve views, functions, triggers, or storage.\\n3. **Generate the migration** → `supabase db pull --local --yes`\\n4. **Verify** → `supabase migration list --local`\\n\\n## Reference Guides\\n\\n- **Skill Feedback** → [references/skill-feedback.md](references/skill-feedback.md)\\n **MUST read when** the user reports that this skill gave incorrect guidance or is missing information.\\n\"\n}", + "input": "{\n \"path\": \".claude/skills/supabase/SKILL.md\"\n}", + "output": "{\n \"contents\": \"---\\nname: supabase\\ndescription: \\\"Use when doing ANY task involving Supabase. Triggers: Supabase products (Database, Auth, Edge Functions, Realtime, Storage, Vectors, Cron, Queues); client libraries and SSR integrations (supabase-js, @supabase/ssr) in Next.js, React, SvelteKit, Astro, Remix; auth issues (login, logout, sessions, JWT, cookies, getSession, getUser, getClaims, RLS); Supabase CLI or MCP server; schema changes, migrations, declarative schemas, security audits, Postgres extensions (pg_graphql, pg_cron, pg_vector).\\\"\\nmetadata:\\n author: supabase\\n version: \\\"0.1.2\\\"\\n---\\n\\n# Supabase\\n\\n## Core Principles\\n\\n**1. Supabase changes frequently — verify against changelog and current docs before implementing.**\\nDo not rely on training data for Supabase features. Function signatures, config.toml settings, and API conventions change between versions.\\n\\nFirst, fetch `https://supabase.com/changelog.md` (a lightweight summary index — not a heavy pull), scan for `breaking-change` tags relevant to your task, and follow the linked page for any that apply. Then look up the relevant topic using the documentation access methods below.\\n\\n**2. Verify your work.**\\nAfter implementing any fix, run a test query to confirm the change works. A fix without verification is incomplete.\\n\\n**3. Recover from errors, don't loop.**\\nIf an approach fails after 2-3 attempts, stop and reconsider. Try a different method, check documentation, inspect the error more carefully, and review relevant logs when available. Supabase issues are not always solved by retrying the same command, and the answer is not always in the logs, but logs are often worth checking before proceeding.\\n\\n**4. Exposing tables to the Data API:** Depending on the user's [Data API settings](https://supabase.com/dashboard/project//integrations/data_api/settings), newly created tables may not be automatically exposed via the Data (REST) API. If this is the case, `anon` and `authenticated` roles will need to be explicitly granted access.\\n\\n> Note that this is separate from RLS, which controls which _rows_ are visible once a table is accessible, not whether the table is accessible at all.\\n\\nWhen a user reports a SQL-created table is unexpectedly inaccessible, check their Data API settings and whether the roles have been granted access via explicit `GRANT` SQL. When granting public (`anon`/`authenticated`) access, always enable RLS too. See [Exposing a Table to the Data API](https://supabase.com/docs/guides/api/securing-your-api.md) for the full setup workflow.\\n\\n**5. RLS in exposed schemas.**\\nEnable RLS on every table in any exposed schema, which includes `public` by default. This is critical in Supabase because tables in exposed schemas can be reachable through the Data API when the `anon`/`authenticated` roles have access (see [Exposing a Table to the Data API](https://supabase.com/docs/guides/api/securing-your-api.md)). For private schemas, prefer RLS as defense in depth. After enabling RLS, create policies that match the actual access model rather than defaulting every table to the same `auth.uid()` pattern.\\n\\n**6. Security checklist.**\\nWhen working on any Supabase task that touches auth, RLS, views, storage, or user data, run through this checklist. These are Supabase-specific security traps that silently create vulnerabilities:\\n\\n- **Auth and session security**\\n - **Never use `user_metadata` claims in JWT-based authorization decisions.** In Supabase, `raw_user_meta_data` is user-editable and can appear in `auth.jwt()`, so it is unsafe for RLS policies or any other authorization logic. Store authorization data in `raw_app_meta_data` / `app_metadata` instead.\\n - **Deleting a user does not invalidate existing access tokens.** Sign out or revoke sessions first, keep JWT expiry short for sensitive apps, and for strict guarantees validate `session_id` against `auth.sessions` on sensitive operations.\\n - **If you use `app_metadata` or `auth.jwt()` for authorization, remember JWT claims are not always fresh until the user's token is refreshed.**\\n\\n- **API key and client exposure**\\n - **Never expose the `service_role` or secret key in public clients.** Prefer publishable keys for frontend code. Legacy `anon` keys are only for compatibility. In Next.js, any `NEXT_PUBLIC_` env var is sent to the browser.\\n\\n- **RLS, views, and privileged database code**\\n - **Views bypass RLS by default.** In Postgres 15 and above, use `CREATE VIEW ... WITH (security_invoker = true)`. In older versions of Postgres, protect your views by revoking access from the `anon` and `authenticated` roles, or by putting them in an unexposed schema.\\n - **UPDATE requires a SELECT policy.** In Postgres RLS, an UPDATE needs to first SELECT the row. Without a SELECT policy, updates silently return 0 rows — no error, just no change.\\n - **`auth.role()` is deprecated — use the `TO` clause instead.** Supabase has deprecated `auth.role()` in favour of specifying the target role directly on the policy with `TO authenticated` or `TO anon`. Beyond deprecation, `auth.role() = 'authenticated'` breaks silently when anonymous sign-ins are enabled, because anonymous users carry the `authenticated` Postgres role and pass the check regardless of whether the user is genuinely signed in.\\n ```sql\\n -- Deprecated (do not use)\\n create policy \\\"example\\\" on table_name for select\\n using ( auth.role() = 'authenticated' );\\n ```\\n - **`TO authenticated` alone is authentication without authorization (BOLA / IDOR).** Using `TO authenticated` only checks the role — it does not restrict which rows a user can access. The correct pattern combines `TO authenticated` with an ownership predicate in `USING`:\\n ```sql\\n create policy \\\"example\\\" on table_name for select\\n to authenticated\\n using ( (select auth.uid()) = user_id );\\n ```\\n - **UPDATE policies require both `USING` and `WITH CHECK`.** Without `WITH CHECK`, a user can reassign a row's `user_id` to another user:\\n ```sql\\n create policy \\\"example\\\" on table_name for update\\n to authenticated\\n using ( (select auth.uid()) = user_id )\\n with check ( (select auth.uid()) = user_id );\\n ```\\n - **`SECURITY DEFINER` functions bypass RLS.** A `SECURITY DEFINER` function runs with its creator's privileges — typically a role with `bypassrls` (e.g., `postgres`). Never add `SECURITY DEFINER` to resolve a permission error; it silently removes access control without fixing the underlying cause. Prefer `SECURITY INVOKER`.\\n - **`SECURITY DEFINER` functions in `public` are callable by all roles.** Postgres grants `EXECUTE` to `PUBLIC` by default for every new function, so any `SECURITY DEFINER` function in `public` is a public API endpoint callable by `anon` and `authenticated` (which inherit from `PUBLIC`) without any additional grant. When `SECURITY DEFINER` is genuinely needed (e.g., bypassing RLS on an internal lookup table), keep the function in a non-exposed schema, always include an `auth.uid()` check in the function body, and run `supabase db advisors` after making changes.\\n\\n- **Storage access control**\\n - **Storage upsert requires INSERT + SELECT + UPDATE.** Granting only INSERT allows new uploads but file replacement (upsert) silently fails. You need all three.\\n\\n- **Dependency and supply-chain security**\\n - **Always pin package versions and commit lockfiles** when installing Supabase packages (`supabase-js`, `@supabase/ssr`, `supabase-py`, etc.). See the [npm security guide](https://supabase.com/docs/guides/security/npm-security.md) for the full checklist.\\n\\nFor any security concern not covered above, fetch the Supabase product security index: `https://supabase.com/docs/guides/security/product-security.md`\\n\\n## Supabase CLI\\n\\nAlways discover commands via `--help` — never guess. The CLI structure changes between versions.\\n\\n```bash\\nsupabase --help # All top-level commands\\nsupabase --help # Subcommands (e.g., supabase db --help)\\nsupabase --help # Flags for a specific command\\n```\\n\\n**Supabase CLI Known gotchas:**\\n\\n- `supabase db query` requires **CLI v2.79.0+** → use MCP `execute_sql` or `psql` as fallback\\n- `supabase db advisors` requires **CLI v2.81.3+** → use MCP `get_advisors` as fallback\\n- In imperative migration projects, create new hand-authored migration files with `supabase migration new ` first. Never invent a migration filename or rely on memory for the expected format. Declarative schema projects generate migrations from `supabase/schemas/`; see \\\"Making and Committing Schema Changes\\\" below.\\n\\n**Version check and upgrade:** Run `supabase --version` to check. For CLI changelogs and version-specific features, consult the [CLI documentation](https://supabase.com/docs/reference/cli/introduction) or [GitHub releases](https://github.com/supabase/cli/releases).\\n\\n## Supabase MCP Server\\n\\nFor setup instructions, server URL, and configuration, see the [MCP setup guide](https://supabase.com/docs/guides/getting-started/mcp).\\n\\n**Troubleshooting connection issues** — follow these steps in order:\\n\\n1. **Check if the server is reachable:**\\n `curl -so /dev/null -w \\\"%{http_code}\\\" https://mcp.supabase.com/mcp`\\n A `401` is expected (no token) and means the server is up. Timeout or \\\"connection refused\\\" means it may be down.\\n\\n2. **Check `.mcp.json` configuration:**\\n Verify the project root has a valid `.mcp.json` with the correct server URL. If missing, create one pointing to `https://mcp.supabase.com/mcp`.\\n\\n3. **Authenticate the MCP server:**\\n If the server is reachable and `.mcp.json` is correct but tools aren't visible, the user needs to authenticate. The Supabase MCP server uses OAuth 2.1 — tell the user to trigger the auth flow in their agent, complete it in the browser, and reload the session.\\n\\n## Supabase Documentation\\n\\nBefore implementing any Supabase feature, find the relevant documentation. Use these methods in priority order:\\n\\n1. **MCP `search_docs` tool** (preferred — returns relevant snippets directly)\\n2. **Fetch docs pages as markdown** — any docs page can be fetched by appending `.md` to the URL path.\\n3. **Web search** for Supabase-specific topics when you don't know which page to look at.\\n\\n## Making and Committing Schema Changes\\n\\nFirst decide which schema workflow the project uses.\\n\\n### Option A: Declarative schemas\\n\\nUse this when `supabase/schemas/` exists or `config.toml` sets `schema_paths`. Edit the desired schema state in those files, then generate and review the migration. Do not start by hand-writing a migration. See the [Declarative database schemas guide](https://supabase.com/docs/guides/local-development/declarative-database-schemas).\\n\\n### Option B: Imperative migrations\\n\\nUse this when the project does not use declarative schemas.\\n\\n**To make schema changes, use `execute_sql` (MCP) or `supabase db query` (CLI).** These run SQL directly on the database without creating migration history entries, so you can iterate freely and generate a clean migration when ready.\\n\\nDo NOT use `apply_migration` to change a local database schema — it writes a migration history entry on every call, which means you can't iterate, and `supabase db diff` / `supabase db pull` will produce empty or conflicting diffs. If you use it, you'll be stuck with whatever SQL you passed on the first try.\\n\\n**When ready to commit** your changes to a migration file:\\n\\n1. **Run advisors** → `supabase db advisors` (CLI v2.81.3+) or MCP `get_advisors`. Fix any issues.\\n2. **Review the Security Checklist above** if your changes involve views, functions, triggers, or storage.\\n3. **Generate the migration** → `supabase db pull --local --yes`\\n4. **Verify** → `supabase migration list --local`\\n\\n## Reference Guides\\n\\n- **Skill Feedback** → [references/skill-feedback.md](references/skill-feedback.md)\\n **MUST read when** the user reports that this skill gave incorrect guidance or is missing information.\\n\"\n}" + }, + { + "id": "build-cli-003-pg-cron-queue-workflow-tool-4", + "title": "Tool: files_read", + "startTime": "2026-08-01T16:00:07.981Z", + "endTime": "2026-08-01T16:00:07.982Z", + "duration": 1, + "type": "tool_execution", + "status": "success", + "raw": "{\n \"contents\": \"---\\nname: supabase-postgres-best-practices\\ndescription: \\\"Postgres best practices maintained by Supabase, for Postgres running anywhere. Load this skill BEFORE writing or changing anything that lives in a Postgres database: creating or altering tables and columns (including choosing column types), schema design, migrations and declarative schema files, RLS policies and the tests that verify them, indexes, triggers, database functions, queues and scheduled jobs (pg_cron, pgmq), vector/semantic search (pgvector), and restoring dumps (pg_restore) or importing data. Also load it when diagnosing slow queries, high CPU, timeouts, EXPLAIN plans, connection exhaustion, locking, bloat, or rows visible to the wrong user or tenant. This is not just a performance guide — schema, migration, security, and SQL authoring tasks need these rules too, even for a one-column change or a single query.\\\"\\nlicense: MIT\\nmetadata:\\n author: supabase\\n version: \\\"1.1.1\\\"\\n organization: Supabase\\n date: January 2026\\n abstract: Comprehensive Postgres performance optimization guide for developers using Supabase and Postgres. Contains performance rules across 8 categories, prioritized by impact from critical (query performance, connection management) to incremental (advanced features). Each rule includes detailed explanations, incorrect vs. correct SQL examples, query plan analysis, and specific performance metrics to guide automated optimization and code generation.\\n---\\n\\n# Supabase Postgres Best Practices\\n\\nComprehensive performance optimization guide for Postgres, maintained by Supabase. Contains rules across 8 categories, prioritized by impact to guide automated query optimization and schema design.\\n\\n## When to Apply\\n\\nReference these guidelines when:\\n- Writing SQL queries or designing schemas\\n- Implementing indexes or query optimization\\n- Reviewing database performance issues\\n- Configuring connection pooling or scaling\\n- Optimizing for Postgres-specific features\\n- Working with Row-Level Security (RLS)\\n\\n## Rule Categories by Priority\\n\\n| Priority | Category | Impact | Prefix |\\n|----------|----------|--------|--------|\\n| 1 | Query Performance | CRITICAL | `query-` |\\n| 2 | Connection Management | CRITICAL | `conn-` |\\n| 3 | Security & RLS | CRITICAL | `security-` |\\n| 4 | Schema Design | HIGH | `schema-` |\\n| 5 | Concurrency & Locking | MEDIUM-HIGH | `lock-` |\\n| 6 | Data Access Patterns | MEDIUM | `data-` |\\n| 7 | Monitoring & Diagnostics | LOW-MEDIUM | `monitor-` |\\n| 8 | Advanced Features | LOW | `advanced-` |\\n\\n## How to Use\\n\\nRead individual rule files for detailed explanations and SQL examples:\\n\\n```\\nreferences/query-missing-indexes.md\\nreferences/query-partial-indexes.md\\nreferences/_sections.md\\n```\\n\\nEach rule file contains:\\n- Brief explanation of why it matters\\n- Incorrect SQL example with explanation\\n- Correct SQL example with explanation\\n- Optional EXPLAIN output or metrics\\n- Additional context and references\\n- Supabase-specific notes (when applicable)\\n\\n## References\\n\\n- https://www.postgresql.org/docs/current/\\n- https://supabase.com/docs\\n- https://wiki.postgresql.org/wiki/Performance_Optimization\\n- https://supabase.com/docs/guides/database/overview\\n- https://supabase.com/docs/guides/auth/row-level-security\\n\"\n}", + "input": "{\n \"path\": \".claude/skills/supabase-postgres-best-practices/SKILL.md\"\n}", + "output": "{\n \"contents\": \"---\\nname: supabase-postgres-best-practices\\ndescription: \\\"Postgres best practices maintained by Supabase, for Postgres running anywhere. Load this skill BEFORE writing or changing anything that lives in a Postgres database: creating or altering tables and columns (including choosing column types), schema design, migrations and declarative schema files, RLS policies and the tests that verify them, indexes, triggers, database functions, queues and scheduled jobs (pg_cron, pgmq), vector/semantic search (pgvector), and restoring dumps (pg_restore) or importing data. Also load it when diagnosing slow queries, high CPU, timeouts, EXPLAIN plans, connection exhaustion, locking, bloat, or rows visible to the wrong user or tenant. This is not just a performance guide — schema, migration, security, and SQL authoring tasks need these rules too, even for a one-column change or a single query.\\\"\\nlicense: MIT\\nmetadata:\\n author: supabase\\n version: \\\"1.1.1\\\"\\n organization: Supabase\\n date: January 2026\\n abstract: Comprehensive Postgres performance optimization guide for developers using Supabase and Postgres. Contains performance rules across 8 categories, prioritized by impact from critical (query performance, connection management) to incremental (advanced features). Each rule includes detailed explanations, incorrect vs. correct SQL examples, query plan analysis, and specific performance metrics to guide automated optimization and code generation.\\n---\\n\\n# Supabase Postgres Best Practices\\n\\nComprehensive performance optimization guide for Postgres, maintained by Supabase. Contains rules across 8 categories, prioritized by impact to guide automated query optimization and schema design.\\n\\n## When to Apply\\n\\nReference these guidelines when:\\n- Writing SQL queries or designing schemas\\n- Implementing indexes or query optimization\\n- Reviewing database performance issues\\n- Configuring connection pooling or scaling\\n- Optimizing for Postgres-specific features\\n- Working with Row-Level Security (RLS)\\n\\n## Rule Categories by Priority\\n\\n| Priority | Category | Impact | Prefix |\\n|----------|----------|--------|--------|\\n| 1 | Query Performance | CRITICAL | `query-` |\\n| 2 | Connection Management | CRITICAL | `conn-` |\\n| 3 | Security & RLS | CRITICAL | `security-` |\\n| 4 | Schema Design | HIGH | `schema-` |\\n| 5 | Concurrency & Locking | MEDIUM-HIGH | `lock-` |\\n| 6 | Data Access Patterns | MEDIUM | `data-` |\\n| 7 | Monitoring & Diagnostics | LOW-MEDIUM | `monitor-` |\\n| 8 | Advanced Features | LOW | `advanced-` |\\n\\n## How to Use\\n\\nRead individual rule files for detailed explanations and SQL examples:\\n\\n```\\nreferences/query-missing-indexes.md\\nreferences/query-partial-indexes.md\\nreferences/_sections.md\\n```\\n\\nEach rule file contains:\\n- Brief explanation of why it matters\\n- Incorrect SQL example with explanation\\n- Correct SQL example with explanation\\n- Optional EXPLAIN output or metrics\\n- Additional context and references\\n- Supabase-specific notes (when applicable)\\n\\n## References\\n\\n- https://www.postgresql.org/docs/current/\\n- https://supabase.com/docs\\n- https://wiki.postgresql.org/wiki/Performance_Optimization\\n- https://supabase.com/docs/guides/database/overview\\n- https://supabase.com/docs/guides/auth/row-level-security\\n\"\n}" + }, + { + "id": "build-cli-003-pg-cron-queue-workflow-tool-5", + "title": "Tool: files_list", + "startTime": "2026-08-01T16:00:07.982Z", + "endTime": "2026-08-01T16:00:12.611Z", + "duration": 4629, + "type": "tool_execution", + "status": "error", + "raw": "path must be relative to the workspace", + "input": "{\n \"path\": \"\"\n}", + "output": "path must be relative to the workspace" + }, + { + "id": "build-cli-003-pg-cron-queue-workflow-tool-6", + "title": "Tool: search_docs", + "startTime": "2026-08-01T16:00:12.611Z", + "endTime": "2026-08-01T16:00:15.950Z", + "duration": 3339, + "type": "tool_execution", + "status": "success", + "raw": "{\n \"content\": [\n {\n \"type\": \"text\",\n \"text\": \"{\\\"result\\\":{\\\"searchDocs\\\":{\\\"nodes\\\":[{\\\"title\\\":\\\"Automatic embeddings\\\",\\\"href\\\":\\\"https://supabase.com/docs/guides/ai/automatic-embeddings\\\",\\\"content\\\":\\\"# Automatic embeddings\\\\n\\\\n\\\\n\\\\nVector embeddings enable powerful [semantic search](/docs/guides/ai/semantic-search) capabilities in Postgres, but managing them alongside your content has traditionally been complex. This guide demonstrates how to automate embedding generation and updates using Supabase [Edge Functions](/docs/guides/functions), [pgmq](/docs/guides/database/extensions/pgmq), [pg\\\\\\\\_net](/docs/guides/database/extensions/pg_net), and [pg\\\\\\\\_cron](/docs/guides/cron).\\\\n\\\\n\\\\n## Understanding the challenge\\\\n\\\\nWhen implementing semantic search with pgvector, developers typically need to:\\\\n\\\\n1. Generate embeddings via an external API (like OpenAI)\\\\n2. Store these embeddings alongside the content\\\\n3. Keep embeddings in sync when content changes\\\\n4. Handle failures and retries in the embedding generation process\\\\n\\\\nWhile Postgres [full-text search](/docs/guides/database/full-text-search) can handle this internally through synchronous calls to `to_tsvector` and [triggers](https://www.postgresql.org/docs/current/textsearch-features.html#TEXTSEARCH-UPDATE-TRIGGERS), semantic search requires asynchronous API calls to a provider like OpenAI to generate vector embeddings. This guide demonstrates how to use triggers, queues, and Supabase Edge Functions to bridge this gap.\\\\n\\\\n\\\\n## Understanding the architecture\\\\n\\\\nWe'll leverage the following Postgres and Supabase features to create the automated embedding system:\\\\n\\\\n1. [pgvector](/docs/guides/database/extensions/pgvector): Stores and queries vector embeddings\\\\n2. [pgmq](/docs/guides/queues): Queues embedding generation requests for processing and retries\\\\n3. [pg\\\\\\\\_net](/docs/guides/database/extensions/pg_net): Handles asynchronous HTTP requests to Edge Functions directly from Postgres\\\\n4. [pg\\\\\\\\_cron](/docs/guides/cron): Automatically processes and retries embedding generations\\\\n5. [Triggers](/docs/guides/database/postgres/triggers): Detects content changes and enqueues embedding generation requests\\\\n6. [Edge Functions](/docs/guides/functions): Generates embeddings via an API like OpenAI (customizable)\\\\n\\\\nWe'll design the system to:\\\\n\\\\n1. Be generic, so that it can be used with any table and content. This allows you to configure embeddings in multiple places, each with the ability to customize the input used for embedding generation. These will all use the same queue infrastructure and Edge Function to generate the embeddings.\\\\n\\\\n2. Handle failures gracefully, by retrying failed jobs and providing detailed information about the status of each job.\\\\n\\\\n\\\\n## Implementation\\\\n\\\\nWe'll start by setting up the infrastructure needed to queue and process embedding generation requests. Then we'll create an example table with triggers to enqueue these embedding requests whenever content is inserted or updated.\\\\n\\\\n\\\\n### Step 1: Enable extensions\\\\n\\\\nFirst, let's enable the required extensions:\\\\n\\\\n\\\\n \\\\n ```sql\\\\n -- For vector operations\\\\n create extension if not exists vector\\\\n with\\\\n schema extensions;\\\\n\\\\n -- For queueing and processing jobs\\\\n -- (pgmq will create its own schema)\\\\n create extension if not exists pgmq;\\\\n\\\\n -- For async HTTP requests\\\\n create extension if not exists pg_net\\\\n with\\\\n schema extensions;\\\\n\\\\n -- For scheduled processing and retries\\\\n -- (pg_cron will create its own schema)\\\\n create extension if not exists pg_cron;\\\\n\\\\n -- For clearing embeddings during updates\\\\n create extension if not exists hstore\\\\n with\\\\n schema extensions;\\\\n ```\\\\n\\\\n Even though the SQL code is `create extension`, this is the equivalent of \\\\\\\"enabling the extension\\\\\\\".\\\\n To disable an extension, call `drop extension`.\\\\n \\\\n\\\\n \\\\n 1. Go to the [Extensions](/dashboard/project/_/database/extensions) page in the Dashboard.\\\\n 2. Search for and enable the following extensions:\\\\n * `vector`\\\\n * `pgmq`\\\\n * `pg_net`\\\\n * `pg_cron`\\\\n * `hstore`\\\\n \\\\n\\\\n\\\\n\\\\n### Step 2: Create utility functions\\\\n\\\\nBefore we set up our embedding logic, we need to create some utility functions:\\\\n\\\\n```sql\\\\n-- Schema for utility functions\\\\ncreate schema util;\\\\n\\\\n-- Utility function to get the Supabase project URL (required for Edge Functions)\\\\ncreate function util.project_url()\\\\nreturns text\\\\nlanguage plpgsql\\\\nsecurity definer\\\\nas $$\\\\ndeclare\\\\n secret_value text;\\\\nbegin\\\\n -- Retrieve the project URL from Vault\\\\n select decrypted_secret into secret_value from vault.decrypted_secrets where name = 'project_url';\\\\n return secret_value;\\\\nend;\\\\n$$;\\\\n\\\\n-- Generic function to invoke any Edge Function\\\\ncreate or replace function util.invoke_edge_function(\\\\n name text,\\\\n body jsonb,\\\\n timeout_milliseconds int = 5 * 60 * 1000 -- default 5 minute timeout\\\\n)\\\\nreturns void\\\\nlanguage plpgsql\\\\nas $$\\\\ndeclare\\\\n headers_raw text;\\\\n auth_header text;\\\\nbegin\\\\n -- If we're in a PostgREST session, reuse the request headers for authorization\\\\n headers_raw := current_setting('request.headers', true);\\\\n\\\\n -- Only try to parse if headers are present\\\\n auth_header := case\\\\n when headers_raw is not null then\\\\n (headers_raw::json->>'authorization')\\\\n else\\\\n null\\\\n end;\\\\n\\\\n -- Perform async HTTP request to the edge function\\\\n perform net.http_post(\\\\n url => util.project_url() || '/functions/v1/' || name,\\\\n headers => jsonb_build_object(\\\\n 'Content-Type', 'application/json',\\\\n 'Authorization', auth_header\\\\n ),\\\\n body => body,\\\\n timeout_milliseconds => timeout_milliseconds\\\\n );\\\\nend;\\\\n$$;\\\\n\\\\n-- Generic trigger function to clear a column on update\\\\ncreate or replace function util.clear_column()\\\\nreturns trigger\\\\nlanguage plpgsql as $$\\\\ndeclare\\\\n clear_column text := TG_ARGV[0];\\\\nbegin\\\\n NEW := NEW #= hstore(clear_column, NULL);\\\\n return NEW;\\\\nend;\\\\n$$;\\\\n```\\\\n\\\\nHere we create:\\\\n\\\\n* A schema `util` to store utility functions.\\\\n* A function to retrieve the Supabase project URL from [Vault](/docs/guides/database/vault). We'll add this secret next.\\\\n* A generic function to invoke any Edge Function with a given name and request body.\\\\n* A generic trigger function to clear a column on update. This function accepts the column name as an argument and sets it to `NULL` in the `NEW` record. We'll explain how to use this function later.\\\\n\\\\nEvery project has a unique API URL that is required to invoke Edge Functions. Let's go ahead and add the project URL secret to Vault depending on your environment.\\\\n\\\\nWhen working with a local Supabase stack, add the following to your `supabase/seed.sql` file:\\\\n\\\\n```sql\\\\nselect\\\\n vault.create_secret('http://api.supabase.internal:8000', 'project_url');\\\\n```\\\\n\\\\nWhen deploying to the cloud platform, open the [SQL editor](/dashboard/project/_/sql/new) and run the following, replacing `` with your [project's API URL](/dashboard/project/_/settings/api):\\\\n\\\\n```sql\\\\nselect\\\\n vault.create_secret('', 'project_url');\\\\n```\\\\n\\\\n\\\\n### Step 3: Create queue and triggers\\\\n\\\\nOur goal is to automatically generate embeddings whenever content is inserted or updated within a table. We can use triggers and queues to achieve this. Our approach is to automatically queue embedding jobs whenever records are inserted or updated in a table, then process them asynchronously using a cron job. If a job fails, it will remain in the queue and be retried in the next scheduled task.\\\\n\\\\nFirst we create a `pgmq` queue for processing embedding requests:\\\\n\\\\n```sql\\\\n-- Queue for processing embedding jobs\\\\nselect pgmq.create('embedding_jobs');\\\\n```\\\\n\\\\nNext we create a trigger function to queue embedding jobs. We'll use this function to handle both insert and update events:\\\\n\\\\n```sql\\\\n-- Generic trigger function to queue embedding jobs\\\\ncreate or replace function util.queue_embeddings()\\\\nreturns trigger\\\\nlanguage plpgsql\\\\nsecurity definer\\\\nset search_path = ''\\\\nas $$\\\\ndeclare\\\\n content_function text = TG_ARGV[0];\\\\n embedding_column text = TG_ARGV[1];\\\\nbegin\\\\n perform pgmq.send(\\\\n queue_name => 'embedding_jobs',\\\\n msg => jsonb_build_object(\\\\n 'id', NEW.id,\\\\n 'schema', TG_TABLE_SCHEMA,\\\\n 'table', TG_TABLE_NAME,\\\\n 'contentFunction', content_function,\\\\n 'embeddingColumn', embedding_column\\\\n )\\\\n );\\\\n return NEW;\\\\nend;\\\\n$$;\\\\n```\\\\n\\\\nOur `util.queue_embeddings` trigger function is generic and can be used with any table and content function. It accepts two arguments:\\\\n\\\\n1. `content_function`: The name of a function that returns the text content to be embedded. The function should accept a single row as input and return text (see the `embedding_input` example).\\\\n\\\\n This allows you to customize the text input passed to the embedding model - for example, you could concatenate multiple columns together like `title` and `content` and use the result as input.\\\\n\\\\n2. `embedding_column`: The name of the destination column where the embedding will be stored.\\\\n\\\\nNote that the `util.queue_embeddings` trigger function requires a `for each row` clause to work correctly. See [Usage](#usage) for an example of how to use this trigger function with your table.\\\\n\\\\nNext we'll create a function to process the embedding jobs. This function will read jobs from the queue, group them into batches, and invoke the Edge Function to generate embeddings. We'll use `pg_cron` to schedule this function to run every 10 seconds.\\\\n\\\\n```sql\\\\n-- Function to process embedding jobs from the queue\\\\ncreate or replace function util.process_embeddings(\\\\n batch_size int = 10,\\\\n max_requests int = 10,\\\\n timeout_milliseconds int = 5 * 60 * 1000 -- default 5 minute timeout\\\\n)\\\\nreturns void\\\\nlanguage plpgsql\\\\nas $$\\\\ndeclare\\\\n job_batches jsonb[];\\\\n batch jsonb;\\\\nbegin\\\\n with\\\\n -- First get jobs and assign batch numbers\\\\n numbered_jobs as (\\\\n select\\\\n message || jsonb_build_object('jobId', msg_id) as job_info,\\\\n (row_number() over (order by 1) - 1) / batch_size as batch_num\\\\n from pgmq.read(\\\\n queue_name => 'embedding_jobs',\\\\n vt => timeout_milliseconds / 1000,\\\\n qty => max_requests * batch_size\\\\n )\\\\n ),\\\\n -- Then group jobs into batches\\\\n batched_jobs as (\\\\n select\\\\n jsonb_agg(job_info) as batch_array,\\\\n batch_num\\\\n from numbered_jobs\\\\n group by batch_num\\\\n )\\\\n -- Finally aggregate all batches into array\\\\n select coalesce(array_agg(batch_array), array[]::jsonb[])\\\\n from batched_jobs\\\\n into job_batches;\\\\n\\\\n -- Invoke the embed edge function for each batch\\\\n foreach batch in array job_batches loop\\\\n perform util.invoke_edge_function(\\\\n name => 'embed',\\\\n body => batch,\\\\n timeout_milliseconds => timeout_milliseconds\\\\n );\\\\n end loop;\\\\nend;\\\\n$$;\\\\n\\\\n-- Schedule the embedding processing\\\\nselect\\\\n cron.schedule(\\\\n 'process-embeddings',\\\\n '10 seconds',\\\\n $$\\\\n select util.process_embeddings();\\\\n $$\\\\n );\\\\n```\\\\n\\\\nLet's discuss some common questions about this approach:\\\\n\\\\n\\\\n#### Why not generate all embeddings in a single Edge Function request?\\\\n\\\\nWhile this is possible, it can lead to long processing times and potential timeouts. Batching allows us to process multiple embeddings concurrently and handle failures more effectively.\\\\n\\\\n\\\\n#### Why not one request per row?\\\\n\\\\nThis approach can lead to API rate limiting and performance issues. Batching provides a balance between efficiency and reliability.\\\\n\\\\n\\\\n#### Why queue requests instead of processing them immediately?\\\\n\\\\nQueuing allows us to handle failures gracefully, retry requests, and manage concurrency more effectively. Specifically we are using `pgmq`'s visibility timeouts to ensure that failed requests are retried.\\\\n\\\\n\\\\n#### How do visibility timeouts work?\\\\n\\\\nEvery time we read a message from the queue, we set a visibility timeout which tells `pgmq` to hide the message from other readers for a certain period. If the Edge Function fails to process the message within this period, the message becomes visible again and will be retried by the next scheduled task.\\\\n\\\\n\\\\n#### How do we handle retries?\\\\n\\\\nWe use `pg_cron` to schedule a task that reads messages from the queue and processes them. If the Edge Function fails to process a message, it becomes visible again after a timeout and can be retried by the next scheduled task.\\\\n\\\\n\\\\n#### Is 10 seconds a good interval for processing?\\\\n\\\\nThis interval is a good starting point, but you may need to adjust it based on your workload and the time it takes to generate embeddings. You can adjust the `batch_size`, `max_requests`, and `timeout_milliseconds` parameters to optimize performance.\\\\n\\\\n\\\\n### Step 4: Create the Edge Function\\\\n\\\\nFinally we'll create the Edge Function to generate embeddings. We'll use OpenAI's API in this example, but you can replace it with any other embedding generation service.\\\\n\\\\nUse the Supabase CLI to create a new Edge Function:\\\\n\\\\n```bash\\\\nsupabase functions new embed\\\\n```\\\\n\\\\nThis will create a new directory `supabase/functions/embed` with an `index.ts` file. Replace the contents of this file with the following:\\\\n\\\\n*supabase/functions/embed/index.ts*:\\\\n\\\\n```typescript\\\\n// Setup type definitions for built-in Supabase Runtime APIs\\\\nimport 'jsr:@supabase/functions-js/edge-runtime.d.ts'\\\\n\\\\n// We'll use the OpenAI API to generate embeddings\\\\nimport OpenAI from 'jsr:@openai/openai'\\\\n\\\\nimport { z } from 'npm:zod'\\\\n\\\\n// We'll make a direct Postgres connection to update the document\\\\nimport postgres from 'https://deno.land/x/postgresjs@v3.4.5/mod.js'\\\\n\\\\n// Initialize OpenAI client\\\\nconst openai = new OpenAI({\\\\n // We'll need to manually set the `OPENAI_API_KEY` environment variable\\\\n apiKey: Deno.env.get('OPENAI_API_KEY'),\\\\n})\\\\n\\\\n// Initialize Postgres client\\\\nconst sql = postgres(\\\\n // `SUPABASE_DB_URL` is a built-in environment variable\\\\n Deno.env.get('SUPABASE_DB_URL')!\\\\n)\\\\n\\\\nconst jobSchema = z.object({\\\\n jobId: z.number(),\\\\n id: z.number(),\\\\n schema: z.string(),\\\\n table: z.string(),\\\\n contentFunction: z.string(),\\\\n embeddingColumn: z.string(),\\\\n})\\\\n\\\\nconst failedJobSchema = jobSchema.extend({\\\\n error: z.string(),\\\\n})\\\\n\\\\ntype Job = z.infer\\\\ntype FailedJob = z.infer\\\\n\\\\ntype Row = {\\\\n id: string\\\\n content: unknown\\\\n}\\\\n\\\\nconst QUEUE_NAME = 'embedding_jobs'\\\\n\\\\n// Listen for HTTP requests\\\\nDeno.serve(async (req) => {\\\\n if (req.method !== 'POST') {\\\\n return new Response('expected POST request', { status: 405 })\\\\n }\\\\n\\\\n if (req.headers.get('content-type') !== 'application/json') {\\\\n return new Response('expected json body', { status: 400 })\\\\n }\\\\n\\\\n // Use Zod to parse and validate the request body\\\\n const parseResult = z.array(jobSchema).safeParse(await req.json())\\\\n\\\\n if (parseResult.error) {\\\\n return new Response(`invalid request body: ${parseResult.error.message}`, {\\\\n status: 400,\\\\n })\\\\n }\\\\n\\\\n const pendingJobs = parseResult.data\\\\n\\\\n // Track jobs that completed successfully\\\\n const completedJobs: Job[] = []\\\\n\\\\n // Track jobs that failed due to an error\\\\n const failedJobs: FailedJob[] = []\\\\n\\\\n async function processJobs() {\\\\n let currentJob: Job | undefined\\\\n\\\\n while ((currentJob = pendingJobs.shift()) !== undefined) {\\\\n try {\\\\n await processJob(currentJob)\\\\n completedJobs.push(currentJob)\\\\n } catch (error) {\\\\n failedJobs.push({\\\\n ...currentJob,\\\\n error: error instanceof Error ? error.message : JSON.stringify(error),\\\\n })\\\\n }\\\\n }\\\\n }\\\\n\\\\n try {\\\\n // Process jobs while listening for worker termination\\\\n await Promise.race([processJobs(), catchUnload()])\\\\n } catch (error) {\\\\n // If the worker is terminating (e.g. wall clock limit reached),\\\\n // add pending jobs to fail list with termination reason\\\\n failedJobs.push(\\\\n ...pendingJobs.map((job) => ({\\\\n ...job,\\\\n error: error instanceof Error ? error.message : JSON.stringify(error),\\\\n }))\\\\n )\\\\n }\\\\n\\\\n // Log completed and failed jobs for traceability\\\\n console.log('finished processing jobs:', {\\\\n completedJobs: completedJobs.length,\\\\n failedJobs: failedJobs.length,\\\\n })\\\\n\\\\n return new Response(\\\\n JSON.stringify({\\\\n completedJobs,\\\\n failedJobs,\\\\n }),\\\\n {\\\\n // 200 OK response\\\\n status: 200,\\\\n\\\\n // Custom headers to report job status\\\\n headers: {\\\\n 'content-type': 'application/json',\\\\n 'x-completed-jobs': completedJobs.length.toString(),\\\\n 'x-failed-jobs': failedJobs.length.toString(),\\\\n },\\\\n }\\\\n )\\\\n})\\\\n\\\\n/**\\\\n * Generates an embedding for the given text.\\\\n */\\\\nasync function generateEmbedding(text: string) {\\\\n const response = await openai.embeddings.create({\\\\n model: 'text-embedding-3-small',\\\\n input: text,\\\\n })\\\\n const [data] = response.data\\\\n\\\\n if (!data) {\\\\n throw new Error('failed to generate embedding')\\\\n }\\\\n\\\\n return data.embedding\\\\n}\\\\n\\\\n/**\\\\n * Processes an embedding job.\\\\n */\\\\nasync function processJob(job: Job) {\\\\n const { jobId, id, schema, table, contentFunction, embeddingColumn } = job\\\\n\\\\n // Fetch content for the schema/table/row combination\\\\n const [row]: [Row] = await sql`\\\\n select\\\\n id,\\\\n ${sql(contentFunction)}(t) as content\\\\n from\\\\n ${sql(schema)}.${sql(table)} t\\\\n where\\\\n id = ${id}\\\\n `\\\\n\\\\n if (!row) {\\\\n throw new Error(`row not found: ${schema}.${table}/${id}`)\\\\n }\\\\n\\\\n if (typeof row.content !== 'string') {\\\\n throw new Error(`invalid content - expected string: ${schema}.${table}/${id}`)\\\\n }\\\\n\\\\n const embedding = await generateEmbedding(row.content)\\\\n\\\\n await sql`\\\\n update\\\\n ${sql(schema)}.${sql(table)}\\\\n set\\\\n ${sql(embeddingColumn)} = ${JSON.stringify(embedding)}\\\\n where\\\\n id = ${id}\\\\n `\\\\n\\\\n await sql`\\\\n select pgmq.delete(${QUEUE_NAME}, ${jobId}::bigint)\\\\n `\\\\n}\\\\n\\\\n/**\\\\n * Returns a promise that rejects if the worker is terminating.\\\\n */\\\\nfunction catchUnload() {\\\\n return new Promise((reject) => {\\\\n addEventListener('beforeunload', (ev: any) => {\\\\n reject(new Error(ev.detail?.reason))\\\\n })\\\\n })\\\\n}\\\\n```\\\\n\\\\nThe Edge Function listens for incoming HTTP requests from `pg_net` and processes each embedding job. It is a generic worker that can handle embedding jobs for any table and column. It uses OpenAI's API to generate embeddings and updates the corresponding row in the database. It also deletes the job from the queue once it has been processed.\\\\n\\\\nThe function is designed to process multiple jobs independently. If one job fails, it will not affect the processing of other jobs. The function returns a `200 OK` response with a list of completed and failed jobs. We can use this information to diagnose failed jobs. See [Troubleshooting](#troubleshooting) for more details.\\\\n\\\\nYou will need to set the `OPENAI_API_KEY` environment variable to authenticate with OpenAI. When running the Edge Function locally, you can add it to a `.env` file:\\\\n\\\\n*.env*:\\\\n\\\\n```\\\\nOPENAI_API_KEY=your-api-key\\\\n```\\\\n\\\\nWhen you're ready to deploy the Edge Function, set can set the environment variable using the Supabase CLI:\\\\n\\\\n```shell\\\\nsupabase secrets set --env-file .env\\\\n```\\\\n\\\\nor\\\\n\\\\n```shell\\\\nsupabase secrets set OPENAI_API_KEY=\\\\n```\\\\n\\\\nAlternatively, you can replace the `generateEmbedding` function with your own embedding generation logic.\\\\n\\\\nSee [Deploy to Production](/docs/guides/functions/deploy) for more information on how to deploy the Edge Function.\\\\n\\\\n\\\\n## Usage\\\\n\\\\nNow that the infrastructure is in place, let's go through an example of how to use this system to automatically generate embeddings for a table of documents. You can use this approach with multiple tables and customize the input for each embedding generation as needed.\\\\n\\\\n\\\\n### 1. Create table to store documents with embeddings\\\\n\\\\nWe'll set up a new `documents` table that will store our content and embeddings:\\\\n\\\\n```sql\\\\n-- Table to store documents with embeddings\\\\ncreate table documents (\\\\n id integer primary key generated always as identity,\\\\n title text not null,\\\\n content text not null,\\\\n embedding halfvec(1536),\\\\n created_at timestamp with time zone default now()\\\\n);\\\\n\\\\n-- Index for vector search over document embeddings\\\\ncreate index on documents using hnsw (embedding halfvec_cosine_ops);\\\\n```\\\\n\\\\nOur `documents` table stores the title and content of each document along with its vector embedding. We use a `halfvec(1536)` column to store the embeddings.\\\\n\\\\n`halfvec` is a `pgvector` data type that stores float values in half precision (16 bits) to save space. Our Edge Function used OpenAI's `text-embedding-3-small` model which generates 1536-dimensional embeddings, so we use the same dimensionality here. Adjust this based on the number of dimensions your embedding model generates.\\\\n\\\\nWe use an [HNSW index](/docs/guides/ai/vector-indexes/hnsw-indexes) on the vector column. Note that we are choosing `halfvec_cosine_ops` as the index method, which means our future queries will need to use cosine distance (`<=>`) to find similar embeddings. Also note that HNSW indexes support a maximum of 4000 dimensions for `halfvec` vectors, so keep this in mind when choosing an embedding model. If your model generates embeddings with more than 4000 dimensions, you will need to reduce the dimensionality before indexing them. See [Matryoshka embeddings](/blog/matryoshka-embeddings) for a potential solution to shortening dimensions.\\\\n\\\\nAlso note that the table must have a primary key column named `id` for our triggers to work correctly with the `util.queue_embeddings` function and for our Edge Function to update the correct row.\\\\n\\\\n\\\\n### 2. Create triggers to enqueue embedding jobs\\\\n\\\\nNow we'll set up the triggers to enqueue embedding jobs whenever content is inserted or updated:\\\\n\\\\n```sql\\\\n-- Customize the input for embedding generation\\\\n-- e.g. Concatenate title and content with a markdown header\\\\ncreate or replace function embedding_input(doc documents)\\\\nreturns text\\\\nlanguage plpgsql\\\\nimmutable\\\\nas $$\\\\nbegin\\\\n return '# ' || doc.title || E'\\\\\\\\n\\\\\\\\n' || doc.content;\\\\nend;\\\\n$$;\\\\n\\\\n-- Trigger for insert events\\\\ncreate trigger embed_documents_on_insert\\\\n after insert\\\\n on documents\\\\n for each row\\\\n execute function util.queue_embeddings('embedding_input', 'embedding');\\\\n\\\\n-- Trigger for update events\\\\ncreate trigger embed_documents_on_update\\\\n after update of title, content -- must match the columns in embedding_input()\\\\n on documents\\\\n for each row\\\\n execute function util.queue_embeddings('embedding_input', 'embedding');\\\\n```\\\\n\\\\nWe create 2 triggers:\\\\n\\\\n1. `embed_documents_on_insert`: Enqueues embedding jobs whenever new rows are inserted into the `documents` table.\\\\n\\\\n2. `embed_documents_on_update`: Enqueues embedding jobs whenever the `title` or `content` columns are updated in the `documents` table.\\\\n\\\\nBoth of these triggers use the same `util.queue_embeddings` function that will queue the embedding jobs for processing. They accept 2 arguments:\\\\n\\\\n1. `embedding_input`: The name of the function that generates the input for embedding generation. This function allows you to customize the text input passed to the embedding model (e.g. concatenating the title and content). The function should accept a single row as input and return text.\\\\n\\\\n2. `embedding`: The name of the destination column where the embedding will be stored.\\\\n\\\\nNote that the update trigger only fires when the `title` or `content` columns are updated. This is to avoid unnecessary updates to the embedding column when other columns are updated. Make sure that these columns match the columns used in the `embedding_input` function.\\\\n\\\\n{/* supa-mdx-lint-disable-next-line Rule001HeadingCase */}\\\\n\\\\n\\\\n#### (Optional) Clearing embeddings on update\\\\n\\\\nNote that our trigger will enqueue new embedding jobs when content is updated, but it will not clear any existing embeddings. This means that an embedding can be temporarily out of sync with the content until the new embedding is generated and updated.\\\\n\\\\nIf it is more important to have *accurate* embeddings than *any* embedding, you can add another trigger to clear the existing embedding until the new one is generated:\\\\n\\\\n```sql\\\\n-- Trigger to clear the embedding column on update\\\\ncreate trigger clear_document_embedding_on_update\\\\n before update of title, content -- must match the columns in embedding_input()\\\\n on documents\\\\n for each row\\\\n execute function util.clear_column('embedding');\\\\n```\\\\n\\\\n`util.clear_column` is a generic trigger function we created earlier that can be used to clear any column in a table.\\\\n\\\\n* It accepts the column name as an argument. This column must be nullable.\\\\n* It requires a `before` trigger with a `for each row` clause.\\\\n* It requires the `hstore` extension we created earlier.\\\\n\\\\nThis example will clear the `embedding` column whenever the `title` or `content` columns are updated (note the `of title, content` clause). This ensures that the embedding is always in sync with the title and content, but it will result in temporary gaps in search results until the new embedding is generated.\\\\n\\\\nWe intentionally use a `before` trigger because it allows us to modify the record before it's written to disk, avoiding an extra `update` statement that would be needed with an `after` trigger.\\\\n\\\\n\\\\n### 3. Insert and update documents\\\\n\\\\nLet's insert a new document and update its content to see the embedding generation in action:\\\\n\\\\n```sql\\\\n-- Insert a new document\\\\ninsert into documents (title, content)\\\\nvalues\\\\n ('Understanding Vector Databases', 'Vector databases are specialized...');\\\\n\\\\n-- Immediately check the embedding column\\\\nselect id, embedding\\\\nfrom documents\\\\nwhere title = 'Understanding Vector Databases';\\\\n```\\\\n\\\\nYou should observe that the `embedding` column is initially `null` after inserting the document. This is because the embedding generation is asynchronous and will be processed by the Edge Function in the next scheduled task.\\\\n\\\\nWait up to 10 seconds for the next task to run, then check the `embedding` column again:\\\\n\\\\n```sql\\\\nselect id, embedding\\\\nfrom documents\\\\nwhere title = 'Understanding Vector Databases';\\\\n```\\\\n\\\\nYou should see the generated embedding for the document.\\\\n\\\\nNext let's update the content of the document:\\\\n\\\\n```sql\\\\n-- Update the content of the document\\\\nupdate documents\\\\nset content = 'Vector databases allow you to query...'\\\\nwhere title = 'Understanding Vector Databases';\\\\n\\\\n-- Immediately check the embedding column\\\\nselect id, embedding\\\\nfrom documents\\\\nwhere title = 'Understanding Vector Databases';\\\\n```\\\\n\\\\nYou should observe that the `embedding` column is reset to `null` after updating the content. This is because of the trigger we added to clear existing embeddings whenever the content is updated. The embedding will be regenerated by the Edge Function in the next scheduled task.\\\\n\\\\nWait up to 10 seconds for the next task to run, then check the `embedding` column again:\\\\n\\\\n```sql\\\\nselect id, embedding\\\\nfrom documents\\\\nwhere title = 'Understanding Vector Databases';\\\\n```\\\\n\\\\nYou should see the updated embedding for the document.\\\\n\\\\nFinally we'll update the title of the document:\\\\n\\\\n```sql\\\\n-- Update the title of the document\\\\nupdate documents\\\\nset title = 'Understanding Vector Databases with Supabase'\\\\nwhere title = 'Understanding Vector Databases';\\\\n```\\\\n\\\\nYou should observe that the `embedding` column is once again reset to `null` after updating the title. This is because the trigger we added to clear existing embeddings fires when either the `content` or `title` columns are updated. The embedding will be regenerated by the Edge Function in the next scheduled task.\\\\n\\\\nWait up to 10 seconds for the next task to run, then check the `embedding` column again:\\\\n\\\\n```sql\\\\nselect id, embedding\\\\nfrom documents\\\\nwhere title = 'Understanding Vector Databases with Supabase';\\\\n```\\\\n\\\\nYou should see the updated embedding for the document.\\\\n\\\\n\\\\n## Troubleshooting\\\\n\\\\nThe `embed` Edge Function processes a batch of embedding jobs and returns a `200 OK` response with a list of completed and failed jobs in the body. For example:\\\\n\\\\n```json\\\\n{\\\\n \\\\\\\"completedJobs\\\\\\\": [\\\\n {\\\\n \\\\\\\"jobId\\\\\\\": \\\\\\\"1\\\\\\\",\\\\n \\\\\\\"id\\\\\\\": \\\\\\\"1\\\\\\\",\\\\n \\\\\\\"schema\\\\\\\": \\\\\\\"public\\\\\\\",\\\\n \\\\\\\"table\\\\\\\": \\\\\\\"documents\\\\\\\",\\\\n \\\\\\\"contentFunction\\\\\\\": \\\\\\\"embedding_input\\\\\\\",\\\\n \\\\\\\"embeddingColumn\\\\\\\": \\\\\\\"embedding\\\\\\\"\\\\n }\\\\n ],\\\\n \\\\\\\"failedJobs\\\\\\\": [\\\\n {\\\\n \\\\\\\"jobId\\\\\\\": \\\\\\\"2\\\\\\\",\\\\n \\\\\\\"id\\\\\\\": \\\\\\\"2\\\\\\\",\\\\n \\\\\\\"schema\\\\\\\": \\\\\\\"public\\\\\\\",\\\\n \\\\\\\"table\\\\\\\": \\\\\\\"documents\\\\\\\",\\\\n \\\\\\\"contentFunction\\\\\\\": \\\\\\\"embedding_input\\\\\\\",\\\\n \\\\\\\"embeddingColumn\\\\\\\": \\\\\\\"embedding\\\\\\\",\\\\n \\\\\\\"error\\\\\\\": \\\\\\\"error connecting to openai api\\\\\\\"\\\\n }\\\\n ]\\\\n}\\\\n```\\\\n\\\\nIt also returns the number of completed and failed jobs in the response headers. For example:\\\\n\\\\n```\\\\nx-completed-jobs: 1\\\\nx-failed-jobs: 1\\\\n```\\\\n\\\\nYou can also use the `x-deno-execution-id` header to trace the execution of the Edge Function within the [dashboard](/dashboard/project/_/functions) logs.\\\\n\\\\nEach failed job includes an `error` field with a description of the failure. Reasons for a job failing could include:\\\\n\\\\n* An error generating the embedding via external API\\\\n* An error connecting to the database\\\\n* The edge function being terminated (e.g. due to a wall clock limit)\\\\n* Any other error thrown during processing\\\\n\\\\n`pg_net` stores HTTP responses in the `net._http_response` table, which can be queried to diagnose issues with the embedding generation process.\\\\n\\\\n```sql\\\\nselect\\\\n *\\\\nfrom\\\\n net._http_response\\\\nwhere\\\\n (headers->>'x-failed-jobs')::int > 0;\\\\n```\\\\n\\\\n\\\\n## Conclusion\\\\n\\\\nAutomating embedding generation and updates in Postgres allow you to build powerful semantic search capabilities without the complexity of managing embeddings manually.\\\\n\\\\nBy combining Postgres features like triggers, queues, and other extensions with Supabase Edge Functions, we can create a robust system that handles embedding generation asynchronously and retries failed jobs automatically.\\\\n\\\\nThis system can be customized to work with any content and embedding generation service, providing a flexible and scalable solution for semantic search in Postgres.\\\\n\\\\n\\\\n## See also\\\\n\\\\n* [What are embeddings?](/docs/guides/ai/concepts)\\\\n* [Semantic search](/docs/guides/ai/semantic-search)\\\\n* [Vector indexes](/docs/guides/ai/vector-indexes)\\\\n* [Supabase Edge Functions](/docs/guides/functions)\\\\n\\\"},{\\\"title\\\":\\\"Scheduling Edge Functions\\\",\\\"href\\\":\\\"https://supabase.com/docs/guides/functions/schedule-functions\\\",\\\"content\\\":\\\"# Scheduling Edge Functions\\\\n\\\\n\\\\n\\\\n
    \\\\n