xecret with Next.js
Replace .env.local with xecret in a Next.js app — local development, NEXT_PUBLIC build-time variables, multiple environments, and deployment.
Updated 2026-08-16 · read this page as markdown
On this page
Next.js reads configuration from process.env, which is exactly what
xecret run fills in. Your code does not change.
Local development
xecret init # once per repository; commit .xecret.yaml
xecret run -- next devDelete .env.local, and make sure .env* is in .gitignore. Everything the
server side reads through process.env is already there.
Two Next.js-specific things are worth knowing.
NEXT_PUBLIC_ variables are inlined at build time
Anything prefixed NEXT_PUBLIC_ is substituted into the JavaScript bundle
during next build. That means:
- It must be present in the environment of the build, so CI runs
xecret run -- next build, not justxecret run -- next start. - It is not a secret once built. It ships to every browser that loads your site. Storing it in xecret keeps it managed and versioned; it does not make it private.
Keep real credentials out of that prefix regardless of where they are stored.
next dev does not re-read secrets on restart
The environment is injected when xecret run starts the process. Hot reload
does not re-fetch. After changing a secret, stop the dev server and run the
command again — the same moment you would have edited .env.local.
Server-only secrets
Next.js server components, route handlers and server actions can read anything
in process.env. The rule to hold onto is Next's own: a variable without the
NEXT_PUBLIC_ prefix is never sent to the browser, so a database URL read in a
server component stays on the server.
// app/api/health/route.ts — runs on the server, never in a bundle
export async function GET() {
const url = process.env.DATABASE_URL;
// …
}Multiple environments
.xecret.yaml names the default, usually development. Point a single command
somewhere else without editing it:
xecret run --environment staging -- next start
xecret run --environment production -- next buildProduction is deny-by-default for the developer role: reaching it takes an
explicit grant from somebody who manages members, not a flag. See
teams, roles and access.
CI and deployment
# GitHub Actions
- uses: playxoft/xecret@v1
- run: xecret run -- next build
env:
XECRET_TOKEN: ${{ secrets.XECRET_TOKEN }}Because NEXT_PUBLIC_ values are baked in at build time, the build step is the
one that must run under xecret run. Full CI recipes: secrets in
CI.
Platforms that inject the environment themselves
Vercel, Cloudflare Pages and similar platforms set environment variables from their own dashboards at deploy time. Two options:
- Keep xecret as the source of truth and sync at deploy. Pipe
xecret pull --format envinto the platform's own CLI as a deploy step. Nothing is committed, and there is still one place a value is edited. - Build in CI under
xecret run, deploy the artefact. Preferable where the platform accepts a prebuilt output, because nothing has to be synchronised at all.
# option 1, sketched — never commit the output of this
xecret pull --format env | while IFS='=' read -r key value; do
your-platform-cli env set "$key" "$value" --environment production
doneNext.js on Cloudflare Workers
If you deploy through @opennextjs/cloudflare — as xecret itself does — note
that Worker bindings and process.env are two different things at runtime. Use
xecret for build-time configuration and for anything your Node-compatible code
reads from process.env; use Wrangler's own vars and secrets bindings for
values the Worker runtime resolves.
Next
- Node.js — the same patterns without the framework.
- Secrets in CI — service tokens end to end.
- Command reference — every flag.