Overview
A Plumix site deploys as one Node.js process, or as one Cloudflare Worker — Node is the default pnpm create plumix-app scaffolds, and Cloudflare is one flag away with --runtime cloudflare. This page is the Worker’s story: the build turns plumix.config.ts into a worker entry, stages the admin and your static files beside it, and every resource the site reads at runtime arrives as a binding on the Worker env. Node.js is the other shape, where the same build produces a directory and a command — read that page first if you scaffolded with the default runtime.
What a deploy is
Section titled “What a deploy is”Two files decide what a deploy is. plumix.config.ts picks the runtime adapter and names the binding each slot reads. wrangler.jsonc declares those bindings as real Cloudflare resources and sets the Worker’s compatibility flags. A binding name appears once in each file, and the two spellings have to match or the first request fails with a list of what is missing.
Secret values are in neither file. A secret’s name may appear in wrangler.jsonc under secrets.required, which makes wrangler deploy fail when the value is not configured, but the value itself arrives on env only when a request runs. That is why a secret-bearing slot takes an (env) => value resolver instead of a literal. Read Secrets for that shape.
Everything else is a build output. plumix build writes a generated worker entry, a generated Drizzle schema, and a static directory holding the admin SPA plus anything in your public/ folder. You commit none of it.
There is no origin server. The Worker answers the public site, the admin at /_plumix/admin, and the cron invocations, all from the same script.
Running the commands
Section titled “Running the commands”The scaffolder adds plumix to your project rather than to your PATH, so reach it through your package runner.
pnpm exec plumix deploynpx plumix deployyarn plumix deployThe rest of this page writes the commands bare. wrangler works the same way.
Quickstart
Section titled “Quickstart”This is the path from a scaffolded recipe site to a live one. It assumes a site made with pnpm create plumix-app and a signed-in wrangler.
-
Point the passkey at the deployed host. The scaffolded
plumix.config.tsspreadscloudflareDeployOriginintoauth.passkey, and that spread resolves a real host only on a Workers Builds build. For the local build in step 5, replace it with a literalrpIdofyour-account.workers.dev, anoriginofhttps://recipes.your-account.workers.devandallowedOrigins: ["https://*.your-account.workers.dev"], keeping the localhost pair behindprocess.env.PLUMIX_DEV. That one wildcard covers production and every preview branch. Cloudflare Workers has the block. -
Create the database. The scaffolded
wrangler.jsoncships a placeholderdatabase_idthat works only for local dev.Terminal window wrangler d1 create recipesPaste the returned id into the
d1_databasesentry inwrangler.jsonc. Both this edit and step 1 have to land before the build, because the build copieswrangler.jsoncinto the bundle directory and bundles the config module into the worker entry. -
Generate the migrations.
Terminal window plumix migrate generateThis writes
.plumix/schema.tsfrom your config, then runsdrizzle-kit generateover it. The SQL lands indrizzle/, which you do commit. -
Apply them to the remote database.
Terminal window plumix migrate apply --remoteWithout
--remotethe same command applies to your local D1 copy. Everything after the command name passes through towrangler d1 migrations apply. -
Build the site.
Terminal window plumix buildThe build fails here rather than in production if a plugin registration or a config slot is wrong, because it constructs the whole app before Vite starts.
-
Deploy.
Terminal window plumix deployThis is
wrangler deploywith your arguments appended. -
Set the secrets your resolvers read. One command per key.
Terminal window wrangler secret put RESEND_API_KEYYou can run this before the first deploy too. Point
wrangler secret putat a Worker that does not exist yet and it asks whether to create one. That prompt defaults to yes and answers itself yes in a non-interactive shell, uploads a stub Worker under the name, then stores the secret on it. Name the key undersecrets.requiredinwrangler.jsoncand the deploy refuses until the value is set, telling you to ship it withwrangler deploy --secrets-file <path>. -
Enrol the first passkey. Open
/_plumix/adminon the deployed origin. The first admin has to arrive through the passkey bootstrap rail, so do this before inviting anyone.
What the build produces
Section titled “What the build produces”plumix build regenerates the .plumix/ directory before Vite starts. Nothing in it is yours to edit, and every file carries a header saying so.
.plumix/worker.ts is the Worker entry, and wrangler.jsonc points its main at it. The runtime adapter writes it — config.runtime.generateEntry() — because the entry is the one file whose shape belongs to the platform: a module-worker export default here, something else on a runtime that is not Workers. The entry Cloudflare emits imports your config, calls buildApp once per isolate, asks the adapter for its handler with config.runtime.createHandler(app), and hands each request to that handler along with an invocation built from the Worker’s env and waitUntil. Regenerating it on every build is what keeps the entry in step with the config.
.plumix/schema.ts is the Drizzle schema assembled from core’s tables plus every table a plugin contributes. plumix migrate generate reads it.
.plumix/client-entry.ts is the browser bundle’s entry, and it carries the CSS files your theme declared. Plumix emits it even when it is empty, because the Vite plugin lists it as a client entry unconditionally.
Three more entries are emitted on every build and each becomes its own client chunk. .plumix/islands-entry.ts imports the island runtime that registers the <plumix-island> custom element, and the server injects it only on a page that contains one. .plumix/islands-renderer-entry.ts is the React half of that runtime, dynamically imported on first hydration so React is never fetched for a page whose islands all defer. .plumix/editor-entry.ts carries the editor canvas and the block modules it renders, and the server injects it only when the edit gate authorizes the request.
.plumix/public holds the static half: your public/ directory copied in, and the precompiled admin SPA staged under _plumix/admin/.
Vite then builds twice, client first and worker second, because the worker bakes the client’s asset manifest into its own bundle. The output is a dist/client directory of static assets and a dist/<worker-name>/ directory holding the worker bundle and a wrangler.json the Cloudflare Vite plugin writes from your wrangler.jsonc. The plugin also drops a .wrangler/deploy/config.json redirect pointing at that copy, so plumix deploy ships the built snapshot rather than re-reading the file you edited.
Build time against request time
Section titled “Build time against request time”Your config module evaluates twice. plumix build runs it on your machine to write .plumix/schema.ts and .plumix/worker.ts, and that generated entry imports it again, so it also runs once per isolate inside the deployed Worker. The Vite plugin freezes six process.env member expressions into the bundle as literals, WORKERS_CI, WORKERS_CI_BRANCH, PLUMIX_DEV, PLUMIX_EDITOR, PLUMIX_EDITOR_PATH_MAP and PLUMIX_FORWARD_ERRORS, and it rewrites those spellings and nothing else. Every other read of process.env runs inside the Worker, where your shell is not. Reading one of the six through a variable instead of writing process.env.NAME out in full puts it on the wrong side of that line, and the value never reaches the deploy.
Bindings and secrets exist only inside a request. The handler connects each slot on the first request by handing it the Worker env, so d1({ binding: "DB" }) stores the string "DB" at build time and resolves it to a live D1 handle when the first request arrives. The bound instances are reused for the rest of the isolate’s life, because env does not change under it. The one exception is a database adapter’s connectRequest hook, which runs per request; that is how D1’s Sessions API attaches a bookmark to each response.
That split is why the secret-bearing slots take a resolver. mailer, an OAuth provider’s clientSecret, passkey.origin and R2’s S3 credentials all accept either a literal or an (env) => value function, and the framework calls the function once per isolate with the real env. R2’s S3 credentials are the odd one there. An R2 binding cannot mint a presigned upload URL, so the adapter needs an account id and an S3 key pair on top of the binding before it can offer one.
Migrations
Section titled “Migrations”Plumix generates migrations from your config rather than from a hand-written schema file. Registering an entry type adds no table, so most content modelling needs no migration at all. A plugin that ships its own tables does.
Run plumix migrate generate after adding or removing such a plugin, commit the SQL in drizzle/, and apply it with plumix migrate apply --remote before the deploy that needs it. The command reads the database name out of your wrangler config, so pass one explicitly only when the config declares more than one D1 database.
Scheduled tasks
Section titled “Scheduled tasks”Core registers two scheduled tasks: a daily session cleanup and a publish-scheduled task that flips due entries from scheduled to published. Neither runs unless the Worker declares a matching cron trigger.
{ "triggers": { "crons": ["0 3 * * *", "*/5 * * * *"], },}Preview deploys
Section titled “Preview deploys”Workers Builds gives every branch its own *.workers.dev host. A passkey is anchored to a hostname, so a credential enrolled on production will not verify on a preview unless the two share a registrable domain.
Anchor rpId to your-account.workers.dev and add the allowedOrigins wildcard https://*.your-account.workers.dev, and one passkey spans production and each preview branch, because every one of those hosts sits under the same registrable domain. cloudflareDeployOrigin derives that pair from the build environment for you, on a build that runs on Workers Builds; write it out yourself when you build locally. See Cloudflare Workers for both shapes and for what changes when production runs on a custom domain.
Related
Section titled “Related”Your passkey origin is a deploy concern and an identity concern at once, so Passkeys is worth reading alongside this page. Scheduled publishing is described from the content side in Statuses and Publishing. The generated files under .plumix/ are catalogued from the repository side in Project Structure. Runtime Adapters is the contract a runtime other than Cloudflare Workers would implement, and the obligations it takes on. CDN Caching is the one slot that survives a change of host untouched, and covers putting a CDN in front of a deploy that is not a Worker.
One page in this section is not written yet, and what it covers already ships. CLI Reference is the exhaustive listing of the seven commands and the five global flags.
Next steps
Section titled “Next steps”Read Cloudflare Workers next for the adapters that fill each slot, then Bindings and Environment for how a name in wrangler.jsonc reaches your config. Secrets covers the values neither file may contain.
If you have not deployed anything yet, Deploy Your Site is the shorter path and comes back here for depth. Configuration is the reference for every config slot named above.