vb_secrets
A stack app has three audiences for its configuration: the server, the build, and the browser. One declaration covers all three, and each key says which of them may read it.
The declaration
main.ts belongs in the repository. secrets.json beside it holds the values on your machine and must be git-ignored.
// vb_secrets/main.ts
import { defineSecrets, secret, server, browser, local, flag, string, number, boolean } from "@voidbase-cloud/voidbase/secrets";
export default defineSecrets({
STRIPE_SECRET_KEY: secret(string(), "server side only, never in a build"),
MAX_UPLOAD_MB: server(number().default(10)),
VOIDBASE_DOMAINS: server(string().optional(), "the hostnames, the first canonical"),
PUBLIC_SITE_NAME: browser(string().default("My App")),
PUBLIC_STRIPE_KEY: browser(string()),
FEATURE_COMMENTS: flag(boolean().default(true), "a Flagship flag, per request"),
VOIDBASE_DEPLOY_CF_API_KEY: local(string(), "the deploy token"),
VOIDBASE_DEPLOY_NAME: local(string().default("my-app"), "the Worker this deploys to"),
});The audiences
| Wrapper | Who sees the value |
|---|---|
secret() | The deployed Worker only, encrypted. Never in a build, never listed back. |
server() | Routes, middleware, crons, queues and hooks. A plain variable on the Worker. |
browser() | The same, and inlined into the client bundle, so anyone who loads the page can read it. |
flag() | A boolean feature flag from Cloudflare Flagship, created at deploy with its default and evaluated per request; readers see a boolean without knowing it is a flag. |
local() | Your machine and your CI: the deploy token, the Worker's name. Never deployed. |
Every key must be wrapped in one of them; a bare validator is refused at build time. So is a value in secrets.json that main.ts does not declare, by name, because it would silently never reach the Worker. That is the whole point of the file: the person maintaining it is the one deciding what reaches a browser, so it makes them say so rather than letting a naming convention decide.
Reading them
In the client, a browser key is inlined by the build, so it is a constant by the time the page runs:
// in a page or a component: the value is inlined at build time
const name = import.meta.env.PUBLIC_SITE_NAME;On the server, it is the environment, or the typed values through the declaration itself:
import { defineHandler } from "void";
import { pb } from "@voidbase-cloud/voidbase/adapter";
import config from "../../vb_secrets/main";
// in a route: the Worker's env
export const GET = defineHandler((c) => ({ limit: Number(c.env.MAX_UPLOAD_MB) }));
// in a hook, or anywhere the hook globals are: the stored string
const limit = Number(pb.$os.getenv("MAX_UPLOAD_MB"));
// or the typed values, through the same declaration
const { MAX_UPLOAD_MB } = await config.read((n) => pb.$os.getenv(n)); // a number, on both runtimesA browser() key is public the moment you deploy. Anything that would embarrass you in view source belongs in secret(), and a build that inlines it will not tell you afterwards.
Seeing the state of it
voidbase secrets, run inside the generated .voidbase/, lists every declared key, its audience, whether it has a value here or a default, and whether the Worker already has it. Everything on a project's pb_secrets page applies: the same validators, the same defaults, the same knobs the deploy reads, the same refusal to deploy an invalid or missing value, and the same rule that a deploy stores a secret the Worker lacks but never replaces one it has.
The adapter writes a re-export of this declaration into .voidbase/pb_secrets/main.ts and copies secrets.json beside it on build, so the deployed Worker validates against the same file you edited.
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.