Project structure

What the scaffold puts on disk, which files you actually edit, and what is generated.

A scaffolded tool is a normal web project with three extra files. Nothing is hidden in a framework directory, and nothing generated is checked in.

The tree

my-pedal/
├── plugin.config.json     # the manifest — plugin identity, kernel, UI bundle
├── package.json
├── vite.config.ts
├── src/
│   ├── kernel.ts          # your DSP, restricted TypeScript
│   ├── schema.ts          # your ParamSchema — the single source of truth
│   ├── App.tsx            # your UI, a normal web page
│   └── main.tsx
├── profiles/
│   └── tone-pedal.profile.json   # ScriptC lib profile for the kernel
├── presets/
│   └── default.json       # preset JSON = the DAW state chunk, same bytes
├── dist/                  # web build (deployed site)
├── dist-plugin/           # plugin-flavored web build (embedded in the .vst3)
└── build/                 # export output: .vst3, .app — generated, gitignored

The three files that matter

src/kernel.ts is your DSP, and the only file with rules. It exports an AudioKernel<State> and is compiled twice — freestanding WASM for the browser, native library for the plugin. Everything in Rules of the kernel applies here and nowhere else in the project.

src/schema.ts exports a ParamSchema. Its kernelId must match the ABI prefix in your profile and in plugin.config.json's kernel.abiPrefix; the generate step checks this for you. Parameter order in this file is the VST3 automation order, so append rather than reorder.

plugin.config.json is the manifest. It is short because most of it is derivable:

{
  "id": "music.codex.my-pedal",
  "name": "My Pedal",
  "vendor": "Codex Music",
  "version": "0.1.0",
  "category": "Fx",
  "kernel": {
    "package": "@codex-music/audio-kernels",
    "profile": "profiles/tone-pedal.profile.json",
    "abiPrefix": "sctone_"
  },
  "ui": {
    "dist": "dist-plugin"
  }
}

schema is optional: with no schema block the loader looks in src/schema.ts and picks the single export shaped like a ParamSchema whose kernelId matches kernel.abiPrefix. category is "Fx" or "Instrument", and both fields are validated — a typo fails the generate step with a named error rather than producing a broken plugin.

Two web builds, one UI

dist/ is your deployable site. dist-plugin/ is the same source built for the webview inside the plugin: relative asset paths, no service worker, no CDN assumptions, because it is served from plugin resources through a custom scheme handler rather than over HTTP. The bundle is otherwise byte-identical to what you deploy, which is why the plugin editor and the web page are the same UI and not two things kept in sync.

What is generated, and where

Nothing per-plugin is written by hand into the native shell. Running the generator emits a codex_plugin_params.h (the C parameter table with plugin identity, deterministic VST3 class ids, and normalize math matching @codex-music/audio-contracts exactly), a plugin.cmake fragment defining your target, and a resolved manifest.json snapshot for tooling. Output lands under native/plugin-shell/generated/<slug>/, is gitignored, and is deterministic — regenerate it any time; there are no timestamps in it.

What you never open

build/ holds the export output. Inside it you will find CMake caches, the VST3 SDK checkout CMake fetched, object files, and the finished bundles. If you never look in there, nothing is lost — that is the point. See codex export for what the pipeline does on the way there, and plugin-shell-kit if you want to read the codegen itself.