Step 4: Now let's make it need a key
Most real APIs want a key. Here's the one door a handler can reach one through — and the one that looks right but silently isn't.
random.org's basic integer generator is free and needs no key — good for getting this build off the ground fast. Most APIs worth calling aren't. Say your group starts streaming sessions and wants provably-fair rolls: random.org's signed API returns a cryptographic signature with every draw, proof after the fact that nobody touched the number, but it wants a subscription key on every request:
import { defineHandler } from "@vvd/sdk/server"
export const config = {
secrets: ["RANDOM_ORG_KEY"], // UPPER_SNAKE_CASE — the only name ctx.secrets.get resolves
fetch: ["api.random.org"], // egress allowlist — exact hostnames
}
export default defineHandler(async (ctx) => {
const key = await ctx.secrets.get("RANDOM_ORG_KEY")
const res = await ctx.fetch("https://api.random.org/json-rpc/4/invoke", {
method: "POST",
headers: { "content-type": "application/json" },
body: JSON.stringify({
jsonrpc: "2.0",
method: "generateSignedIntegers",
params: { apiKey: key, n: 1, min: 1, max: 20 },
id: 1,
}),
})
const { result } = (await res.json()) as { result: { random: { data: number[] } } }
return { roll: bandResult(result.random.data[0]) }
})
function bandResult(n: number): string {
if (n <= 5) return "No — and it gets worse."
if (n <= 10) return "No, but it's not hopeless."
if (n <= 15) return "Yes, but there's a complication."
return "Yes."
}ctx.secrets.get only resolves names your config.secrets declared — an undeclared name is
unreachable, and a declared name with nothing stored fails loudly and tells you to set it,
rather than sending undefined as a bearer token.
Store the value once, from your project folder. The platform encrypts it server-side and hands it only to your own handlers, at run time:
vvd secret set RANDOM_ORG_KEY 9f2b-live-key --scope dev
✓ RANDOM_ORG_KEY set (dev)
encrypted, this-tool-only — read it in api/ with ctx.secrets.get("RANDOM_ORG_KEY").vvd secret list
● RANDOM_ORG_KEY set values are write-only — update one with vvd secret set <NAME>
A value is write-only. After set, nothing reads it back — not the CLI, not the web UI,
not another tool. Only ctx.secrets.get, inside your own declared handler, ever sees it.
--scope is dev (your vvd run sessions) or published (the version other people install).
process.env is always {} in here
The instinct every JavaScript developer has is process.env.RANDOM_ORG_KEY. Try it — right
now, in the playground below. You won't get an error. You'll get undefined, silently, and
lose an hour wondering why your key "isn't working." A tool bundle never sees real
environment variables, anywhere, ever — client or server. The only door to a secret is
ctx.secrets.get, inside a declared api/ handler.
Edit and the example re-runs. Tab indents; press Escape to leave the editor.
You should see: press the button — the page prints "Result: undefined". That's not a
playground quirk; it's the same undefined you'd get in a real, published tool. process is
stubbed here so a stray reference doesn't crash the example, but the emptiness is real —
config.secrets plus ctx.secrets.get is the only door that actually works.