Environment variables & secrets
ayjnt agents and middleware read env vars exactly like raw Cloudflare Workers — through env.<NAME>. The framework only adds two things on top: a typed GeneratedEnv you can extend, and an automatic .dev.vars sync so wrangler can find your project-root secrets during dev.
Reading values
In an agent
Every agent gets this.env typed as its Env generic parameter. Extend the framework's GeneratedEnv (which already carries every DO
binding ayjnt scanned from agents/) with the
secrets and config the agent reads at runtime:
import { Agent } from "agents";
import type { GeneratedEnv } from "@ayjnt/env";
// GeneratedEnv carries every DO binding ayjnt scanned out of agents/.
// Extend it with the secrets and config the agent reads at runtime.
type Env = GeneratedEnv & {
GOOGLE_API_KEY: string;
FEATURE_FLAG_NEW_INDEX?: string;
};
export default class ChatAgent extends Agent<Env> {
override async onRequest(request: Request): Promise<Response> {
const apiKey = this.env.GOOGLE_API_KEY; // typed string
const flag = this.env.FEATURE_FLAG_NEW_INDEX ?? "off";
// ...
return Response.json({ ok: true, flag });
}
}Field names are conventionally uppercase snake_case to match the wrangler / shell convention, but TypeScript doesn't require it. Mark fields optional when the agent has a fallback path for the missing-value case.
In middleware
Middleware sees the same env on its Context argument. Type the Middleware generic to get autocomplete on c.env.*:
import type { Middleware } from "ayjnt/middleware";
import type { GeneratedEnv } from "@ayjnt/env";
type Env = GeneratedEnv & { AUTH_SECRET: string };
const middleware = (async (c, next) => {
const header = c.request.headers.get("authorization");
if (header !== `Bearer ${c.env.AUTH_SECRET}`) { // typed string
return c.text("forbidden", 403);
}
return next();
}) satisfies Middleware<Env>;
export default middleware;
The same Env type can (and usually should) be
shared between an agent and the middleware that gates it —
either inline-typed in both files or imported from a small
shared module under agents/.
Setting values
Cloudflare Workers separates env vars into secrets
(write-only, encrypted at rest, set via wrangler secret put or the dashboard) and plain vars (declared in wrangler.jsonc, visible in source). For ayjnt
projects the most natural pattern is treat
everything as a secret in production — there's no
loss of expressiveness, and you avoid having to wire a
static-config surface through the generated wrangler.jsonc.
Local dev — .dev.vars at the project root
Drop a .dev.vars file at the project root (the
directory with your package.json) and ayjnt
automatically mirrors it into .ayjnt/dist/ on every build so wrangler picks
it up:
# .dev.vars (project root, gitignored)
GOOGLE_API_KEY=ya29.…
AUTH_SECRET=local-only-secret
FEATURE_FLAG_NEW_INDEX=on
The mirror is a relative symlink, so live
edits to the source file propagate to wrangler dev without a rebuild. On Windows
without developer mode the framework falls back to a copy
and warns that mid-session edits won't auto-reload — re-run ayjnt build after changing secrets.
.dev.vars relative to the
directory containing wrangler.jsonc (called configDir), not its working directory. Our
generated config lives in .ayjnt/dist/, so
without the mirror your project-root .dev.vars
would be invisible. If you're on a version older than v0.5.4
and seeing missing vars in dev, that's the cause — upgrade
or manually copy the file into .ayjnt/dist/.
Per-environment overrides — .dev.vars.<env>
When you run wrangler dev --env staging
(forwarded automatically through ayjnt dev --env staging), wrangler loads .dev.vars.staging in addition to (and
overriding) .dev.vars. ayjnt mirrors every .dev.vars.<env> sibling alongside the
base file, so all environments work the same way:
# Project root
.dev.vars # default values
.dev.vars.staging # loaded by ayjnt dev --env staging
.dev.vars.production # loaded by ayjnt dev --env production
.dev.vars.example # checked-in template; NEVER synced (treated as a sample)Production — wrangler secret put
For deployed workers, set each secret with the wrangler CLI.
Values land in the worker's encrypted secret store and are
visible to your code as env.NAME, exactly like .dev.vars entries during dev:
# Add or overwrite a secret in the deployed worker
wrangler secret put GOOGLE_API_KEY
# (paste the value when prompted)
# List the secret names currently bound to the worker
wrangler secret list
# Remove a secret
wrangler secret delete GOOGLE_API_KEY wrangler secret put writes to whatever worker the
active wrangler.jsonc targets — which means
running it from your project root, with ayjnt's generated
config selected, just works.
Don't commit secrets
The scaffold ayjnt generates with bunx ayjnt new
already includes .dev.vars, .env, and .env.local in .gitignore. If you didn't scaffold with that
template, add them yourself. The conventional way to document
required env vars without leaking real values is to commit a .dev.vars.example:
# .dev.vars.example (checked into git as a template)
#
# Copy to .dev.vars and fill in real values. Listed bindings let
# collaborators know which env vars the agent needs to run.
GOOGLE_API_KEY=
AUTH_SECRET=
FEATURE_FLAG_NEW_INDEX=
The framework skips .dev.vars.example in the
sync — it's a checked-in template, not real config, and
mirroring it would shadow real values during dev.
Typing checklist
-
Extend
GeneratedEnvfrom@ayjnt/envwith each binding the agent or middleware reads. -
Use the extended type as the
Envgeneric on the agent (extends Agent<Env, State>) and on anyMiddleware<Env> satisfies Middleware. -
Mark fields optional when the runtime has a fallback. This
gives you type-narrowed code at the use site (e.g.
this.env.X ?? "default") instead of casting. -
Share
Envacross files by hoisting it into a small module (e.g.agents/<route>/shared.ts) when an agent and its middleware both touch the same bindings.
vars field in wrangler.jsonc is for non-secret static config
that's visible in source control. ayjnt regenerates the
whole wrangler.jsonc on each build, so editing
it by hand doesn't survive. For now, treat every env var
as a secret (either .dev.vars locally or wrangler secret put in prod) — operationally
identical from your code's perspective, and avoids the
static-config gap entirely.
Lifecycle
| Stage | Where values live | How wrangler finds them |
|---|---|---|
ayjnt dev | .dev.vars at the project root (and .dev.vars.<env> for --env).
|
ayjnt symlinks them into .ayjnt/dist/; wrangler reads them as
if they lived next to the config.
|
ayjnt deploy |
Cloudflare's encrypted secret store (set via wrangler secret put or the dashboard).
|
Stored per-worker; merged into env at request time.
|
| Both |
Your code reads this.env.X / c.env.X identically.
| The framework doesn't distinguish — same binding name, same type. |
Troubleshooting
-
Project-root
.dev.varsdoesn't seem to be picked up. Pre-v0.5.4 the framework didn't mirror it. Upgrade, or manually copy.dev.varsinto.ayjnt/dist/.dev.varsas a one-shot. See the gotchas page . -
this.env.Xshows up asunknownin editor. The agent'sEnvgeneric doesn't extendGeneratedEnvwith that field. Add it to the extension above and the autocomplete returns. -
Different vars per environment without restarting the
whole dev loop.
Use
.dev.vars.staging+ayjnt dev --env stagingrather than juggling one file with conditional logic. - I deleted .dev.vars but wrangler still sees old values.
A re-run of
ayjnt build(orayjnt dev) cleans up the stale mirror in.ayjnt/dist/. The cleanup is automatic but only fires on build, not on file deletion alone.
Real-world references:
examples/ai-chatbot
uses a Gemini API key from .dev.vars in dev.
examples/agentic-rag
uses Cloudflare account id + API token, and ships a .dev.vars.example template.