Skip to content

feat(cli): add supabase workers new - #6261

Draft
johnstonmatt wants to merge 3 commits into
FUNC-753/workers-configfrom
FUNC-753/workers-new
Draft

feat(cli): add supabase workers new#6261
johnstonmatt wants to merge 3 commits into
FUNC-753/workers-configfrom
FUNC-753/workers-new

Conversation

@johnstonmatt

@johnstonmatt johnstonmatt commented Aug 19, 2026

Copy link
Copy Markdown
Contributor

Summary

Adds supabase workers new, plus the project layout and config.toml editing the
whole command family builds on:

  • shared/workers/ — worker path resolution, config.toml section reading and
    patching (toml-section.ts preserves surrounding formatting), the runtime/size
    envelope, and the starter files.
  • Starter files live as ordinary files under shared/workers/stacks/<runtime>/
    rather than string literals, and are embedded into the compiled binary through a
    Bun macro — the directory is expanded at transpile time and inlined.
    A completeness check inside the macro fails the build if WORKER_RUNTIMES and
    the directory drift apart.

Stack 2 of 4, on top of the config schema (#6260).

Reviewer note: the third commit is where the embedding mechanism is explained; the
starters are deliberately kept out of the type program (a deno starter is not
valid under this workspace's Bun types), which is why tsconfig.json excludes the
directory and nothing imports the files.

Linked issue

FUNC-753 (Linear). Supabase maintainer, exempt from the open-for-contribution flow.

Checklist

@johnstonmatt johnstonmatt changed the title FUNC 753/workers new feat(cli): add supabase workers new Aug 19, 2026
The two pieces every `supabase workers` command rests on, landed on their
own because they are the subtle ones and deserve their own diff. Both live
in `shared/`, so neither carries a shell prefix and neither is specific to
one command tree.

`worker-paths.ts` resolves the project layout: `supabase/<root>/<name>/`,
mirroring `supabase/functions/<slug>/`, with `[workers] root` moving the
grouping directory and `[workers.<name>] source` moving one worker's code
anywhere in the project. Both are validated rather than joined blindly.
`resolveWorkerSource` is load-bearing for safety, not tidiness: the path it
returns is the directory `workers new --force` deletes outright, so a value
naming the project root, `supabase/`, `functions/`, `migrations/`, or
anywhere outside the project is refused before anything is removed.

`toml-section.ts` edits one `[section]` of a TOML file textually rather than
round-tripping it. `config.toml` belongs to the whole CLI — users hand-edit,
comment and commit it — and reserialising preserves the data while
discarding every comment and normalising the formatting they chose. Existing
keys are rewritten in place, keeping any comment trailing the value; new keys
are appended; everything else is left byte for byte. A value spanning several
lines cannot be swapped one line at a time, so it is reported as unsupported
and the file is left untouched instead of stranded half-rewritten. This
follows the approach `legacy-pgdelta.write.ts` already takes for
`[db.migrations] schema_paths`, generalised from one hard-coded key.
Scaffolds `supabase/workers/<name>/` from a runtime's starter files and
records the choice in `config.toml`. Entirely local disk — nothing is
deployed and no network is involved, which is why it lands before the API
seam.

The runtime and instance size are resolved before anything is written, so
cancelling either prompt leaves nothing behind — including the name, which
is only generated once both questions are answered. A worker already
described in `config.toml` reuses those values instead of re-asking, and
says so only when something recorded actually answers for an omitted flag.

Two closed sets, both narrow on purpose. The runtimes are the catalog images
plus `dockerfile`; the sizes are the alpha envelope's two, each implying its
own vCPU count. `root` is refused as a worker name: `[workers] root` is the
scalar key in the same table, so `[workers.root]` would stop the whole
config parsing. Every runtime's starter runs as scaffolded, `dockerfile`
included — its Dockerfile's `CMD` names a `server.js`, so that file is
scaffolded too, alongside a `package.json` whose `"type": "module"` is what
makes it ESM rather than Node's syntax detection.

This also brings the command family's shell wiring, which is where the
conventions here differ from a command tree's usual shape:

- The project directory is `LegacyCliConfig.workdir`, so `--workdir` and
  `SUPABASE_WORKDIR` select the project exactly as they do for every sibling
  command, rather than an ancestor walk of the process's own directory.
- Output goes through `output.raw` as plain text with no `intro`/`outro`
  framing, and tables through `renderGlamourTable`, so `workers` reads like
  `functions` and `projects` rather than like a second CLI.
- `-o`/`--output` is honoured (`workers.output.ts`). It is a global flag 33
  of this shell's 37 command families answer to, so ignoring it would print
  human text to a stdout the user asked to be machine-readable. What workers
  does not inherit is the Go-parity obligation behind the struct encoders:
  there is no Go counterpart to be byte-identical to, so the payload is
  serialised through the generic encoders instead. `-o env` is refused for a
  payload containing a list, because `encodeEnv` reproduces
  `godotenv.Marshal`, whose flattening does not descend into slices.
- Telemetry state is flushed in `Effect.ensuring`, matching Go's
  `PersistentPostRun`.

Two shell-wide registries have to move in step with the command appearing,
and both are enforced by tests rather than convention: `LEGACY_DOCS_TAGS`,
without which the generated CLI reference refuses to build, and
`VALUE_CONSUMING_LONG_FLAGS`, without which the telemetry argv scan treats
`--runtime`'s value as a flag and can fabricate one Go never recorded.

`supabase workers` has no Go equivalent, so it is recorded in
`docs/go-cli-divergences.md` as TS-only.
…acro

The starter files `workers new` writes lived as string literals with their
newlines and `${}` escaped. Hold them as ordinary files under
`shared/workers/stacks/<runtime>/` instead, authored in the language they are
written in, and narrow the offered runtimes to the three that have starters.

A shipped binary has no `stacks/` directory to read, so the directory is
expanded through a Bun macro: it runs while `worker-stacks.ts` is transpiled
and its return value is inlined as a literal, which means the content is
carried with nothing to pass at a build site and no directory to find at
runtime. Bun expands macros in the runtime transpiler too, so running from
source behaves the same; Vitest does not implement them and degrades to
calling the function against the source tree, which is why the path comes
from `import.meta.url` rather than Bun's `import.meta.dir`.

Discovery stays directory-driven — a new runtime is a new directory plus its
`WORKER_RUNTIMES` entry — and a completeness check inside the macro fails the
build rather than the binary when the two drift. Bun reports a throwing macro
as one it could not coerce to AST, so the reason is logged first to keep the
diagnostic legible.

Nothing imports the starters, which is what keeps them out of the type
program: a `deno` starter is not valid under this workspace's Bun types, and
`tsconfig.json` excludes the directory.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant