pb_secrets

Every piece of configuration a project has, declared in one file, each key saying who is allowed to read it. The values live beside it in a file that never enters version control, so a project needs no .env at all.

Two files

main.ts is the declaration and belongs in the repository. secrets.json holds the values on your machine and is git-ignored, which voidbase init has already arranged.

// pb_secrets/main.ts
import { defineSecrets, secret, server, browser, local, flag, string, number, boolean } from "@voidbase-cloud/voidbase/secrets";

export default defineSecrets({
  SMTP_PASSWORD:  secret(string(), "the mail provider's password"),
  STRIPE_SECRET_KEY: secret(string().optional(), "set it and the stripe plugin takes money"),

  MAX_UPLOAD_MB:  server(number().default(10)),
  VOIDBASE_MAIL_DOMAIN: server(string().optional(), "mail leaves through Cloudflare from this domain"),

  PUBLIC_SITE_URL: browser(string().default("https://example.com")),

  FEATURE_DIGEST: flag(boolean().default(false), "a Flagship feature flag, evaluated per request"),

  VOIDBASE_DEPLOY_CF_API_KEY: local(string(), "the deploy token"),
  VOIDBASE_DEPLOY_NAME:       local(string().default("blog-api"), "the Worker this project deploys to"),
});
JSON
{
  "SMTP_PASSWORD": "...",
  "STRIPE_SECRET_KEY": "...",
  "VOIDBASE_DEPLOY_CF_API_KEY": "..."
}

Who may read it

Every key is wrapped in one of five words, and a bare validator is refused. That is deliberate: the person maintaining this file is the one answering for who can see what, so the file makes them say it rather than letting it be inferred.

WrapperWhere the value ends up
secret()The deployed instance only, as an encrypted secret. Never listed back, never in a build. Stored on the first deploy and never silently replaced afterwards.
server()A plain variable on the instance, readable by hooks and routes. Set by every deploy.
browser()The same, plus inlined into any client build. Treat it as public, because it is; the listing calls this tier public.
flag()A boolean feature flag from Cloudflare Flagship. The deploy creates a Flagship app named after the Worker with every declared flag at its default and binds it; a change in the dashboard wins without a deploy. On every request the flag is evaluated with a targeting key, the signed-in record or else the client's address, so a percentage rollout is sticky per person, and $os.getenv sees the answer without knowing it is a flag. Where Flagship is not reachable, the default answers.
local()Your own tooling: the deploy token, the Worker's name. Read on your machine and in CI, deployed nowhere.

Validation, defaults and types

The validators are string(), number(), boolean(), url(), email(), oneOf() and json(), each with .optional() and .default(value), and any Standard Schema validator works inside the wrappers. A default is a real value, so a key with one needs no entry in secrets.json and no entry in production either. A deploy stops on a missing or invalid value and names the key, never the value, so a typo is caught before it ships rather than after.

Reading them

// in a hook, the way PocketBase reads configuration: the stored string
const limit = Number($os.getenv("MAX_UPLOAD_MB"));
// in TypeScript, the typed values through the same declaration
import config from "../pb_secrets/main";
const { MAX_UPLOAD_MB, FEATURE_DIGEST } = await config.read((n) => $os.getenv(n));   // a number and a boolean

Locally the declared values, defaults included, are put into the environment when the server starts: the shell outranks secrets.json, which outranks .env. On Cloudflare the secrets are the Worker's secrets and the rest are its variables, so the same call works in both places.

The knobs a deploy reads from here

Some names mean something to the deploy itself, and the shipped plugins read theirs from the same place. A declared server() or secret() key with one of these names is enough; the deploy wires the binding, and the plugin does the rest.

KeyWhat it turns on
VOIDBASE_DEPLOY_NAME, VOIDBASE_DOMAINSThe Worker's name, and the hostnames the domains plugin attaches, the first canonical.
VOIDBASE_DATABASE=durableThe data in a SQLite-backed Durable Object instead of D1; a batch is a real transaction.
VOIDBASE_MAIL_DOMAINThe send_email binding: mail from that domain leaves through Cloudflare Email Service.
VOIDBASE_AIThe Workers AI binding, and POST /api/ai/chat over the instance's own tools.
VOIDBASE_TRANSLATABLE, VOIDBASE_LOCALESWhich fields have translations and in which locales; the records API answers in ?locale=.
VOIDBASE_SITE_URL, VOIDBASE_SITEMAP, VOIDBASE_SEOWhat robots.txt, the sitemap, llms.txt and the page metadata are generated from.
VOIDBASE_BACKUP_KIND, VOIDBASE_BACKUP_KEEP, VOIDBASE_BACKUP_S3_*What the scheduled backup writes, how many it keeps, and the bucket outside the account it copies to.
STRIPE_SECRET_KEY, POLAR_ACCESS_TOKEN, LEMONSQUEEZY_API_KEYWhich payments plugin answers, each with its webhook secret beside it.
VOIDBASE_CORS_ORIGINS, VOIDBASE_HSTS, VOIDBASE_CSPThe response policy the hardening plugin applies; all off unless set.
VOIDBASE_SECRETS_STOREThe id of the account's Secrets Store. Every secret() is then stored there as <worker>__<KEY> and bound by name instead of living on the Worker; nothing in the app changes.

The full list, with what each accepts, is the environment table in docs/deploy.md.

Seeing the state of it

voidbase secrets
pb_secrets: 8 declared (2 secret, 2 server, 1 public, 1 flag, 2 local, never deployed), 3 valued in secrets.json, worker "blog-api" has 2 of the secrets
  SMTP_PASSWORD                  secret  local value         on the worker  the mail provider's password
  STRIPE_SECRET_KEY              secret  local value         on the worker  set it and the stripe plugin takes money
  MAX_UPLOAD_MB                  server  default 10
  VOIDBASE_MAIL_DOMAIN           server  optional, unset     mail leaves through Cloudflare from this domain
  PUBLIC_SITE_URL                public  default "https://example.com"
  FEATURE_DIGEST                 flag    default false       a Flagship feature flag, evaluated per request
  VOIDBASE_DEPLOY_CF_API_KEY     local   local value         the deploy token
  VOIDBASE_DEPLOY_NAME           local   default "blog-api"  the Worker this project deploys to

One row per declared key: its tier, whether it has a value here or a default, and, when the deploy token is available, whether the Worker already has it (or the Secrets Store, when one is named). A value in secrets.json that the declaration does not name is listed as never deployed.

voidbase secrets push         # store the local secrets on the Worker, replacing, without redeploying

A deploy stores a secret the Worker lacks and leaves one it already holds alone, because a checkout whose secrets.json carries dev values must not overwrite production by deploying. Replacing is explicit, and that is what push is for. It is also what keeps CI simple: a checkout without secrets.json deploys with nothing but the deploy token, because the secrets were pushed once from a machine that has them.

secrets.json is the one file in a project that must never be committed. Check your .gitignore has it before the first commit, especially in a repository that did not come from voidbase init.

Found something wrong on this page?

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.