Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
17a9058
feat(config): add the [workers] section to the project config schema
johnstonmatt Aug 18, 2026
0b1b9b3
feat(cli): add supabase workers new
johnstonmatt Aug 18, 2026
69c83d2
feat(config): add the [workers] section to the project config schema
johnstonmatt Aug 18, 2026
81cfd54
Merge branch 'FUNC-753/workers-config' of https://github.com/supabase…
johnstonmatt Aug 25, 2026
9c5cd2d
Merge branch 'develop' of https://github.com/supabase/cli into FUNC-7…
johnstonmatt Aug 25, 2026
2ea6b94
fix(config): let the Go config loader accept the [workers] section
johnstonmatt Aug 25, 2026
3408a66
test(config): add workers.ts to the pure runtime graph allowlist
johnstonmatt Aug 25, 2026
66a8383
Merge branch 'FUNC-753/workers-config' into FUNC-753/workers-new
johnstonmatt Aug 25, 2026
d50c763
fix(cli): import loadProjectConfig from the effect entrypoint
johnstonmatt Aug 25, 2026
58302a4
refactor(cli): read Option through its public helpers in workers
johnstonmatt Aug 25, 2026
59707a6
docs(cli): give the first workers new example its required name
johnstonmatt Aug 25, 2026
93bcf8e
docs(cli): stop pointing users at a [workers] root that no longer exists
johnstonmatt Aug 25, 2026
32d265f
docs(cli): describe the shipped refusal semantics in workers new SIDE…
johnstonmatt Aug 25, 2026
fb04deb
fix(cli): keep workers new out of config.json
johnstonmatt Aug 25, 2026
72ec632
Merge remote-tracking branch 'origin/develop' into FUNC-753/workers-new
johnstonmatt Aug 25, 2026
81e864e
fix(workers): pin config loader to workdir with search: false
johnstonmatt Aug 26, 2026
39ae31b
Merge branch 'develop' into FUNC-753/workers-new
kanadgupta Aug 26, 2026
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/src/legacy/cli/root.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -35,6 +35,7 @@ import { legacyStorageCommand } from "../commands/storage/storage.command.ts";
import { legacyTestCommand } from "../commands/test/test.command.ts";
import { legacyTelemetryCommand } from "../commands/telemetry/telemetry.command.ts";
import { legacyUnlinkCommand } from "../commands/unlink/unlink.command.ts";
import { legacyWorkersCommand } from "../commands/workers/workers.command.ts";
import { legacyVanitySubdomainsCommand } from "../commands/vanity-subdomains/vanity-subdomains.command.ts";
import { OutputFormatFlag } from "../../shared/cli/global-flags.ts";
import { outputLayerFor } from "../../shared/output/output.layer.ts";
Expand DownExpand Up@@ -70,6 +71,7 @@ export const legacyRoot = Command.make("supabase").pipe(
legacyDomainsCommand,
legacyEncryptionCommand,
legacyFunctionsCommand,
legacyWorkersCommand,
legacyGenCommand,
legacyInitCommand,
legacyInspectCommand,
Expand Down
89 changes: 89 additions & 0 deletions apps/cli/src/legacy/commands/workers/new/SIDE_EFFECTS.md
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
# `supabase workers new <name>`

> **Local-disk only.** Nothing is deployed and no Management API route is
> called; `workers push` is what talks to the platform.

## Files Read

| Path | Format | When |
| ---------------------------------------- | ---------- | ----------------------------------------------------------------------------------------------------------- |
| `<workdir>/supabase/config.toml` | TOML | always — decoded to refuse a worker that is already recorded, then re-read as text to append the new entry |
| `<destination>/` | dir | always, to refuse a destination that is not empty |
Comment thread
johnstonmatt marked this conversation as resolved.
| `<SUPABASE_HOME or ~/.supabase>/profile` | plain text | when neither `--profile` nor `SUPABASE_PROFILE` is set — names the profile, defaulting to `supabase` |
Comment thread
kanadgupta marked this conversation as resolved.
| `<SUPABASE_PROFILE>` (YAML) | YAML | when `SUPABASE_PROFILE` is a filesystem path rather than a built-in name; a read failure aborts the command |
Comment thread
kanadgupta marked this conversation as resolved.

## Files Written

| Path | Format | When |
| ----------------------------------------------- | ------ | -------------------------------------------------------------------------- |
| `<workdir>/supabase/config.toml` | TOML | on success — appends `[workers.<name>]`, preserving surrounding formatting |
| `<workdir>/supabase/workers/<name>/*` | varies | on success, unless `--source` names another directory |
| `<workdir>/<source>/*` | varies | on success, when `--source` is given |
| `<SUPABASE_HOME or ~/.supabase>/telemetry.json` | JSON | whenever the handler runs — flushed on success and on failure |

Workers are recorded in `config.toml` only. The project config loader prefers
`supabase/config.json` when one exists, but the entry writer is a TOML text
editor, so this command pins the loader to `config.toml` (`tomlOnly`). In a
project that has a `config.json`, the worker is therefore written to
`config.toml` — which that loader lists in `ignoredPaths` — and the `config.json`
is left byte-for-byte alone. A rendered edit that would not parse is refused
before anything reaches disk.

`<workdir>` above is exact: the loader is pinned to it (`search: false`, the
same resolver `start`/`stop`/`status` use) and never climbs to an ancestor. A
`--workdir` pointing at a bare directory inside another Supabase project
therefore records the worker in that directory's own `config.toml` — created if
absent — rather than in the ancestor project's.

Writes to `config.toml` are append-only. A worker already recorded under
`[workers.<name>]` is refused outright — before the runtime and size prompts,
and before anything reaches disk — because editing an entry the user owns is
not this command's job.

Nothing at the destination is ever removed or overwritten: a destination that
exists and is not empty is refused, and clearing it is left to the user.
`--source` is refused when it resolves to the project root, `supabase/`,
`supabase/functions/`, `supabase/migrations/`, or outside the project. Symlinks
are resolved first, so a path inside the project that points outside it is
refused too. A relative `--source` is resolved against the directory the command
was run in; a `source` recorded in `config.toml` is resolved against the project
root.

## API Routes

| Method | Path | Auth | Request body | Response (used fields) |
| ------ | ---- | ---- | ------------ | ---------------------- |
| — | — | — | — | — |

## Exit Codes

| Code | Condition |
| ---- | ----------------------------------------------------------------------------------- |
| `0` | success |
| `1` | invalid worker name — the name must be a DNS label |
| `1` | bad `--source`: outside the project, or a path the CLI owns |
| `1` | destination exists and is not empty |
| `1` | the worker is already recorded in `config.toml`, in any form |
| `1` | the rendered `config.toml` would not parse, or `[workers]` is a sealed inline table |

## Environment Variables

| Variable | Purpose | Required? |
| ------------------ | --------------------------------------- | ------------------------------------------------------ |
| `SUPABASE_PROFILE` | built-in profile name or YAML file path | no (falls back to `~/.supabase/profile` -> `supabase`) |
| `SUPABASE_WORKDIR` | project directory the command acts on | no (falls back to `--workdir`, then the ancestor walk) |
Comment thread
johnstonmatt marked this conversation as resolved.
| `SUPABASE_HOME` | directory holding `telemetry.json` | no (falls back to `~/.supabase`) |

## Telemetry Events Fired

| Event | When | Notable properties / groups |
| ---------------------- | ---------------------------------------------- | ----------------------------------- |
| `cli_command_executed` | post-handler, success or failure (via wrapper) | `exit_code`, `duration_ms`, `flags` |

No custom events — only the `cli_command_executed` that the instrumentation
wrapper emits for every command.
Comment thread
johnstonmatt marked this conversation as resolved.
Comment thread
johnstonmatt marked this conversation as resolved.

Nothing is emitted for a failure the parser catches, such as a missing worker
name or a `--runtime`/`--size` value outside the choice list. The wrapper is
installed by `Command.withHandler`, so a command that never reaches its handler
never reaches the instrumentation either — and `telemetry.json` is not written.
74 changes: 74 additions & 0 deletions apps/cli/src/legacy/commands/workers/new/new.command.ts
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
import { Layer } from "effect";
import { Argument, Command, Flag } from "effect/unstable/cli";
import type * as CliCommand from "effect/unstable/cli/Command";
import { withJsonErrorHandling } from "../../../../shared/output/json-error-handling.ts";
import { commandRuntimeLayer } from "../../../../shared/runtime/command-runtime.layer.ts";
import { WORKER_RUNTIMES, WORKER_SIZES } from "../../../../shared/workers/worker-runtimes.ts";
import { legacyCliSettingsLayer } from "../../../config/legacy-cli-settings.layer.ts";
import { legacyDebugLoggerLayer } from "../../../shared/legacy-debug-logger.layer.ts";
import { legacyTelemetryStateLayer } from "../../../telemetry/legacy-telemetry-state.layer.ts";
import { withLegacyCommandInstrumentation } from "../../../telemetry/legacy-command-instrumentation.ts";
import { legacyWorkersNew } from "./new.handler.ts";

const config = {
name: Argument.string("name").pipe(
Argument.withDescription("Worker name. Doubles as its directory, and its hostname."),
),
runtime: Flag.choice("runtime", WORKER_RUNTIMES).pipe(
Comment thread
johnstonmatt marked this conversation as resolved.
Flag.withDescription(
"Runtime to scaffold and record in supabase/config.toml. Prompted when omitted.",
),
Flag.optional,
),
size: Flag.choice("size", WORKER_SIZES).pipe(
Flag.withDescription(
"Instance size to record in supabase/config.toml. Each size implies its own vCPU count, so there is no separate --cpu. Prompted when omitted.",
),
Flag.optional,
),
source: Flag.string("source").pipe(
Flag.withDescription(
"Scaffold the worker here instead of the default workers directory, recorded as `source` in supabase/config.toml.",
),
Flag.optional,
),
} as const;

export type LegacyWorkersNewFlags = CliCommand.Command.Config.Infer<typeof config>;

const cliSettings = legacyCliSettingsLayer.pipe(Layer.provide(legacyDebugLoggerLayer));

/** Local-disk only: no Management API, so no platform stack is built. */
const legacyWorkersNewRuntimeLayer = Layer.mergeAll(
cliSettings,
legacyTelemetryStateLayer,
commandRuntimeLayer(["workers", "new"]),
);

export const legacyWorkersNewCommand = Command.make("new", config).pipe(
Command.withDescription(
"Scaffold a worker directory from a runtime's starter files and record the choice in supabase/config.toml. Nothing is deployed.",
),
Command.withShortDescription("Scaffold a worker locally"),
Command.withExamples([
{
command: "supabase workers new api",
description: "Scaffold supabase/workers/api, prompting for runtime and size",
},
{
command: "supabase workers new api --runtime node",
description: "Scaffold supabase/workers/api on the node runtime",
},
{
command: "supabase workers new api --source packages/api",
description: "Scaffold the worker outside the workers directory",
},
]),
Command.withHandler((flags) =>
legacyWorkersNew(flags).pipe(
withLegacyCommandInstrumentation({ flags, config }),
withJsonErrorHandling,
),
),
Command.provide(legacyWorkersNewRuntimeLayer),
);
Loading
Loading