Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions apps/cli/docs/supabase/db/advisors.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
## supabase-db-advisors

`supabase db advisors` is planned for deprecation in a future release. It remains available alongside `supabase issues`. Existing flags, defaults, output, and exit behavior remain supported. An informational migration notice is written to stderr.

Inspects the database for common security and performance issues such as missing RLS policies, unindexed foreign keys, exposed auth.users, and more.

Inspects the local database by default. Pass `--remote` for the linked project, or `--remote=<ref|branch|url>` for another project, a branch of the linked project, or a database URL. The command prints a `Target:` line on stderr naming the database it resolved, and never asks for confirmation.
Expand Down
15 changes: 15 additions & 0 deletions apps/cli/docs/supabase/issues.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
## supabase-issues

Checks a project for security, performance, and health issues. Findings are grouped by check, with every affected resource and its evidence preserved. Severities are `info`, `warning`, and `critical`, corresponding to the underlying INFO, WARN, and ERROR findings.

The default `--type all --severity info` displays every issue. Use `--fail-on warning` to exit with status 1 when a displayed issue is warning or critical. The severity filter applies before the exit threshold.

Inspects the local database by default. Use `--remote` for the linked project, or `--remote=<ref|branch|url>` for another target. Security and performance checks use a transaction that is always rolled back for local databases and direct database URLs. Health checks require a remote project: `--type health` rejects local and direct database targets, while `--type all` reports health coverage as unavailable for those targets.

By default, the command prints a table with severity, category, issue title, and the number of affected resources, followed by check coverage. Long titles wrap to fit the terminal. Narrow terminals show stacked fields when the table cannot fit. Unavailable checks include their reason and an incomplete-results warning. An empty result means no issues match the selected severity; it does not establish that the project is healthy.

Structured output contains `issues` and `checks`. Each issue includes its stable check ID, severity, category, documentation URL, and affected resources. Coverage reports each requested category as complete or unavailable. A remote check that cannot run is reported as unavailable and exits with status 1; it is never reported as a clean check.

`supabase db advisors` is planned for deprecation in a future release and remains available alongside `supabase issues`. Its existing flags, defaults, output, and exit behavior remain supported.

Use `--output-format json` or `--output-format stream-json` for structured output. The legacy `-o` / `--output` flag is not supported by this command. Check IDs identify check types within a project; they are not persistent incident IDs.
2 changes: 2 additions & 0 deletions apps/cli/src/cli/root.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ import { functionsCommand } from "../commands/functions/functions.command.ts";
import { genCommand } from "../commands/gen/gen.command.ts";
import { initCommand } from "../commands/init/init.command.ts";
import { inspectCommand } from "../commands/inspect/inspect.command.ts";
import { issuesCommand } from "../commands/issues/issues.command.ts";
import { issueCommand } from "../commands/issue/issue.command.ts";
import { linkCommand } from "../commands/link/link.command.ts";
import { logsCommandFor } from "../commands/logs/logs.command.ts";
Expand Down Expand Up @@ -177,6 +178,7 @@ export const rootCommandForFeatures = (options: RootCommandFeatures = {}) =>
initCommand,
inspectCommand,
issueCommand,
issuesCommand,
linkCommand,
loginCommand,
logoutCommand,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import {
type CliErrorActionabilityDeclaration,
ErrorActionabilityId,
statusCodeActionability,
} from "../../../shared/telemetry/error-actionability.ts";
} from "../../shared/telemetry/error-actionability.ts";

/**
* Tagged errors for `db advisors`, one per failure path. Message text is an
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { describe, expect, it } from "vitest";
import { classifyCliErrorActionability } from "../../../shared/telemetry/error-actionability.ts";
import { classifyCliErrorActionability } from "../../shared/telemetry/error-actionability.ts";
import { DbAdvisorsInvalidTokenError } from "./advisors.errors.ts";

describe("DbAdvisorsInvalidTokenError actionability", () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,8 @@
* byte-for-byte. The only `omitempty` field is `metadata`.
*/

import { encodeHtmlSafeJsonIndented } from "../../../command-internal/html-safe-json.ts";
import { makeLevelEnum } from "../../../command-internal/fail-on.ts";
import { encodeHtmlSafeJsonIndented } from "../html-safe-json.ts";
import { makeLevelEnum } from "../fail-on.ts";

/** Lowest severity first. */
const ADVISORS_ALLOWED_LEVELS = ["info", "warn", "error"] as const;
Expand Down
59 changes: 59 additions & 0 deletions apps/cli/src/command-internal/advisors/advisors.layers.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
import { Layer } from "effect";

import { commandCredentialsLayer } from "../../auth/command-credentials.layer.ts";
import { httpClientLayer } from "../../auth/http-debug.layer.ts";
import { commandPlatformApiFactoryLayer } from "../../auth/command-platform-api-factory.layer.ts";
import { commandSettingsLayer } from "../../config/command-settings.layer.ts";
import { projectRefLayer } from "../../config/project-ref.layer.ts";
import { dbConfigLayer } from "../db-config.layer.ts";
import { dbConnectionLayer } from "../db-connection.sql-pg.layer.ts";
import { debugLoggerLayer } from "../debug-logger.layer.ts";
import { identityStitchLayer } from "../identity-stitch.ts";
import { linkedProjectCacheLayer } from "../../telemetry/linked-project-cache.layer.ts";
import { telemetryStateLayer } from "../../telemetry/telemetry-state.layer.ts";

const cliSettings = commandSettingsLayer.pipe(Layer.provide(debugLoggerLayer));
const httpClient = httpClientLayer.pipe(Layer.provide(debugLoggerLayer));
const credentials = commandCredentialsLayer.pipe(
Layer.provide(cliSettings),
Layer.provide(debugLoggerLayer),
);

const platformApiFactory = commandPlatformApiFactoryLayer.pipe(
Layer.provide(credentials),
Layer.provide(cliSettings),
Layer.provide(debugLoggerLayer),
Layer.provide(identityStitchLayer),
);

const projectRef = projectRefLayer.pipe(
Layer.provide(platformApiFactory),
Layer.provide(cliSettings),
);

const linkedProjectCache = linkedProjectCacheLayer.pipe(
Layer.provide(credentials),
Layer.provide(cliSettings),
Layer.provide(httpClient),
Layer.provide(identityStitchLayer),
);

const dbConfig = dbConfigLayer.pipe(
Layer.provide(cliSettings),
Layer.provide(dbConnectionLayer),
Layer.provide(debugLoggerLayer),
Layer.provide(identityStitchLayer),
);

export const advisorChecksRuntimeLayer = Layer.mergeAll(
dbConfig,
dbConnectionLayer,
cliSettings,
httpClient,
credentials,
projectRef,
linkedProjectCache,
platformApiFactory,
identityStitchLayer,
telemetryStateLayer,
);
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { Effect } from "effect";

import { CommandPlatformApiFactory } from "../../../auth/command-platform-api-factory.service.ts";
import { mapHttpError } from "../../../command-internal/http-errors.ts";
import { CommandPlatformApiFactory } from "../../auth/command-platform-api-factory.service.ts";
import { mapHttpError } from "../http-errors.ts";
import {
DbAdvisorsPerformanceNetworkError,
DbAdvisorsPerformanceStatusError,
Expand Down
93 changes: 93 additions & 0 deletions apps/cli/src/command-internal/advisors/advisors.local.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
import { Effect, Option } from "effect";
import { hostedMutationPreapproval } from "../hosted-consent/hosted-consent.ts";
import type { ResolvedTarget } from "../target/resolve-target.ts";
import { Output } from "../../shared/output/output.service.ts";
import { DbConfigResolver } from "../db-config.service.ts";
import { DbConnection, type DbSession } from "../db-connection.service.ts";
import {
DbAdvisorsBeginTxError,
DbAdvisorsQueryError,
DbAdvisorsSetupError,
} from "./advisors.errors.ts";
import { scanAdvisorLintRow } from "./advisors.format.ts";
import { splitLintsSql } from "./advisors.lints-sql.ts";

const queryLints = Effect.fn("DbAdvisors.queryLints")(function* (session: DbSession) {
const [setupSql, querySql] = splitLintsSql();
yield* session.exec(setupSql).pipe(
Effect.mapError(
(cause) =>
new DbAdvisorsSetupError({
message: `failed to prepare lint session: ${cause.message}`,
}),
),
);
const rows = yield* session
.query(querySql)
.pipe(
Effect.mapError(
(cause) => new DbAdvisorsQueryError({ message: `failed to query lints: ${cause.message}` }),
),
);
yield* Effect.annotateCurrentSpan("advisor.count", rows.length);
return rows.map(scanAdvisorLintRow);
});

const setExposedSchemas = Effect.fn("DbAdvisors.setExposedSchemas")(function* (
session: DbSession,
schemas: ReadonlyArray<string>,
) {
yield* Effect.annotateCurrentSpan("advisor.exposed_schema_count", schemas.length);
yield* session
.query("select set_config('pgrst.db_schemas', $1, true)", [schemas.join(",")])
.pipe(
Effect.mapError(
(cause) =>
new DbAdvisorsSetupError({ message: `failed to prepare lint session: ${cause.message}` }),
),
);
});

export const collectLocalAdvisorLints = Effect.fn("db.advisors.runLocal")(function* (
target: Exclude<ResolvedTarget, { readonly kind: "linked" }>,
dnsResolver: "native" | "https",
) {
const output = yield* Output;
const resolver = yield* DbConfigResolver;
const dbConn = yield* DbConnection;

const cfg = yield* resolver.resolveConnection({
target,
dnsResolver,
password: Option.none(),
networkBanRemoval: yield* hostedMutationPreapproval,
});

const lints = yield* Effect.scoped(
Effect.gen(function* () {
const session = yield* dbConn.connect(cfg.conn, { isLocal: cfg.isLocal, dnsResolver });
yield* session.exec("begin").pipe(
Effect.mapError(
(cause) =>
new DbAdvisorsBeginTxError({
message: `failed to begin transaction: ${cause.message}`,
}),
),
);
return yield* Effect.gen(function* () {
if (cfg.exposedSchemas !== undefined) {
yield* setExposedSchemas(session, cfg.exposedSchemas);
}
return yield* queryLints(session);
}).pipe(
Effect.ensuring(
session
.exec("rollback")
.pipe(Effect.catch((cause) => output.raw(`${cause.message}\n`, "stderr"))),
),
);
}),
);

return lints;
});
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ const VARIANTS: ReadonlyArray<{ readonly name: string; readonly variant: Permiss

/** Non-GET operations a leaf may call without hosted-mutation consent, with the reason. */
const CONSENT_FREE_OPERATIONS: ReadonlyMap<string, string> = new Map([
["v2-run-project-advisors", "runs read-only checks without changing project configuration"],
[
"v1-create-login-role",
"mints a short-lived login role for the connection; revisit when connection setup asks for consent",
Expand Down
2 changes: 2 additions & 0 deletions apps/cli/src/commands/db/advisors/SIDE_EFFECTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@

Target: selectable, no consent — see [Command target](../../../../docs/command-target.md)

Writes an informational migration notice to stderr explaining the planned future deprecation and naming `supabase issues`. Existing flags, defaults, payloads, and exit behavior are preserved.

Checks a database for security and performance issues. Two backends: the local
stack and `--remote=<url>` query the database directly; a project fetches from the
Management API.
Expand Down
Loading
Loading