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.