What git tracks
Everything that decides how the instance behaves belongs in the repository: the endpoints, the schema, the static files, the list of configuration keys, the installed plugins, and the version of voidbase itself. One thing must never be there, and that is the values of your secrets.
In the repository
| What | Why it matters that it is tracked |
|---|---|
| pb_hooks/ | Your endpoints and handlers. Reviewable, revertable, and the diff says what changed about behaviour. |
| pb_migrations/ | The schema. A deploy carries it, so production and a fresh clone end up with the same database. |
| pb_public/ | The static files, if the instance serves any. |
| pb_secrets/main.ts | The names of every configuration key, their types, their defaults and who may read them. The declaration, never the values. |
pb_plugins/, voidbase.lock | The installed plugins, each verified against the hash the lockfile pins. On a project deployed from a repository, installing or removing one is a commit the build deploys. |
package.json, lockfile | The version of voidbase production runs. Without it, what ships is whatever npm served that morning. |
Not in the repository
# a project
pb_data/ the database and the uploaded files
pb_secrets/secrets.json the values of your configuration
.cloud/ the generated Cloudflare project
# a stack app
.voidbase/ the generated instance
vb_secrets/secrets.json the values of your configurationvoidbase init writes the project's three ignore lines for you. If your repository did not come from it, check them before the first commit, and check them again before making the repository public.
A secret that has been committed is a secret that has leaked, even after you delete the line: it is in the history, and on any clone. Rotate it rather than trying to erase it.
So how do the values reach production?
Three different ways, depending on who is allowed to read the key.
Secrets are stored once, from your machine
A secret() value is put on the Worker by a deploy you run yourself, and lives there encrypted. The pipeline never sees it: a build has no secrets.json, and it does not need one, because the Worker already has the value.
voidbase secrets pushA deploy stores a secret the Worker does not have yet, and deliberately will not overwrite one it does: a checkout whose secrets.json carries dev values must not overwrite production by deploying. secrets push is the command that replaces, which is what you want when rotating and never what you want by accident.
A Worker's own secrets are seen by nothing else. If you would rather keep them in the account's Secrets Store, one place with role-based access, name the store and the deploy uses it instead:
{
"VOIDBASE_SECRETS_STORE": "<store id>"
}Every declared secret() is then stored there as blog-api__KEY, bound to the Worker by its key, and the Worker's own secret of that name is retired. secrets push stores into it the same way and voidbase secrets says which names the store holds. Nothing in your code changes. The deploy token needs Secrets Store: Write and the account's Secrets Store Deployer role; the free plan holds 100 secrets per store.
Server and browser values travel with the deploy
A server() or browser() key is not a secret; it is configuration. Its value comes from the default in the declaration, which is tracked, or from the build environment, and every deploy sets it again. Most of them should simply have a default, and then there is nothing to configure anywhere.
A boolean that you want to flip without a deploy is a feature flag:
// pb_secrets/main.ts
import { defineSecrets, flag, boolean } from "@voidbase-cloud/voidbase/secrets";
export default defineSecrets({
NEW_CHECKOUT: flag(boolean().default(false), "the redesigned checkout"),
});The deploy creates a Cloudflare Flagship app named after the Worker, creates each declared flag in it with its default, and binds it. A flag that already exists is left as the dashboard has it, so a change there wins without a deploy. On every request the flags are evaluated for whoever is asking, the signed-in record or else the client's address, so a percentage rollout is sticky per person, and every reader of the key sees the answer as if it were a plain value. Where Flagship is not reachable, on Bun or with a token without Flagship: Write, the defaults answer and the deploy says so. Booleans only.
Tokens are yours and stay yours
A local() key is never deployed at all. The deploy token is the exception that has to reach the pipeline, because a build has to be able to deploy: voidbase sync stores it as a build secret on the trigger, once, from your machine. Cloudflare keeps it and does not show it back.
Checking what production actually has
voidbase secretsOne row per declared key: its tier, whether it has a value here or a default, and whether the Worker has it. This is the command to run when something behaves differently in production, because nine times out of ten the answer is a key that was declared but never given a value.
Rotating one
# change it where the value lives, then send it
$EDITOR pb_secrets/secrets.json
voidbase secrets pushNothing to commit and no redeploy. Removing a key is the reverse, and is two changes rather than one: take it out of the declaration and commit that, so the next person does not go looking for a value nothing reads.
What a build machine can see
Only what it needs: Bun's version, the deploy token as a build secret, and the declared server and browser values your machine had when you ran sync. With previews on, the repository's name as well, the GitHub token for the pull request comment as a build secret, and the superuser on the previews trigger so a preview can seed itself from production. Not your secrets.json, which never leaves your machine, and not the encrypted secrets on the Worker, which nothing can read back.
Fix it yourself. The link below opens this file in GitHub's editor and forks the repository for you if you need one, and your change becomes a pull request without leaving the browser.