Set your provider, defaults, and behaviour once and have them apply to every send. A postboi.config.ts at your project root is the committed home for everything except
secrets. bunx postboi init writes it for you. It’s also the only place hooks can live, since they’re functions.
It auto-loads on the first send (Node/Bun) — mail(), sms(), or any other channel.
Keep secrets out of this file: API keys and tokens belong in the environment. To vary the provider per environment, see Per-environment config.
Precedence
Lowest to highest (later sources win):
postboi.config.ts- environment variables:
POSTBOI_PROVIDER,POSTBOI_*defaults, and each provider’s own field vars (e.g.MAILGUN_DOMAIN) - options passed explicitly to
mail()or a provider constructor
Bundled and deployed servers
The config file is found by walking up from process.cwd() at runtime. That covers local
dev and any long-running server started from the project root — but not a deployed
bundle. Nothing imports postboi.config.*, so file tracing leaves it out of a serverless
function, and cwd isn’t the project root either. Edge runtimes (Cloudflare Workers) have no
filesystem at all.
The symptom is confusing, because it looks like the config is wrong rather than absent:
everything works locally, then every send in production fails with No recipient address provided (to or default.to) even though default.to is right there
in the file. Only the file’s contents go missing — POSTBOI_TOKEN still selects the
provider, and the captcha key is baked into the installed package at build time.
Building with Vite? Add the plugin and the config ships inside the server bundle — nothing else changes:
It bundles the first postboi.config.* it finds from the Vite root upward (pass postboi({ config: 'path/to/config.ts' }) to point elsewhere, or config: false to skip
it), and only into the server build — a config file with hooks and secrets in it never
reaches the browser. The plugin also carries the optimizeDeps exclude that remote forms need.
Without a bundler in the picture, register config at startup instead with configure({ ... }):