Skip to content
Tools— browse docs
On this page

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:

api/roll-oracle.ts
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
Expected output:
✓ RANDOM_ORG_KEY set (dev)
encrypted, this-tool-only — read it in api/ with ctx.secrets.get("RANDOM_ORG_KEY").
vvd secret list
Expected output:
  ● 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).

Danger:

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.

See it for yourself
src/tool.tsx Line 14 highlighted.

Edit and the example re-runs. Tab indents; press Escape to leave the editor.

Running · your edits, live
Starting the example…

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.

Next steps