Skip to content
Tools— browse docs
On this page

Step 5: Ship it

A handful of commands, and the mock from every step above is gone for good — a real endpoint, a real key, a real roll.

This is not a step to complete. It's the same commands from every build on this site, plus one you haven't used yet: storing the secret your handler needs.

Note:

No live preview for this step. Real secrets, a real api/ deploy and a real outbound call to a real hostname are the entire subject here — nothing this sandboxed page can do safely or honestly. The terminal output below is the exact shape of it, run for real.

vvd create oracle-roll --tool
Expected output:
→ Creating tool oracle-roll in /Users/you/dev/oracle-roll — from the hello template
✓ Created oracle-roll (tool) → /Users/you/dev/oracle-roll

Replace src/tool.tsx with the real thing — Step 3's answer, with the mock fetchOracle swapped for the useApi<OracleApi>() from that step's first code block. Here's the whole file, so you have something to check your merge against. Note that Break it, on purpose is gone: api.rollOracle() takes no fail flag — from here on, failure is the real network's job:

src/tool.tsx
import { useState } from "react"

import { type ToolRenderProps, WorkshopApiError, defineTool, useApi } from "@vvd/sdk"

interface OracleApi {
  rollOracle(): Promise<{ roll: string }>
}

function OracleRoll({ context }: ToolRenderProps) {
  const api = useApi<OracleApi>()
  const [roll, setRoll] = useState<string | null>(null)
  const [status, setStatus] = useState<"idle" | "loading" | "error">("idle")

  const rollOracle = async () => {
    setStatus("loading")
    try {
      const { roll } = await api.rollOracle()
      setRoll(roll)
      setStatus("idle")
    } catch (err) {
      // A throw inside the handler comes back SANITIZED — the real error goes to
      // the server log, never the browser. err.message is always safe to show.
      setStatus("error")
      console.error(err instanceof WorkshopApiError ? err.message : err)
    }
  }

  return (
    <div style={{ padding: 24, display: "grid", gap: 12, maxWidth: 420, font: "inherit" }}>
      <strong style={{ fontSize: 15 }}>Fate oracle</strong>
      <p style={{ minHeight: 44, margin: 0 }}>
        {status === "loading"
          ? "Consulting…"
          : status === "error"
            ? "Couldn't reach the oracle. Try again?"
            : (roll ?? "Ask when the table needs a fair answer.")}
      </p>
      <div style={{ display: "flex", gap: 8 }}>
        <button
          type="button"
          disabled={!context.canEdit || status === "loading"}
          onClick={() => rollOracle()}
          style={BUTTON}
        >
          Ask the oracle
        </button>
      </div>
      <span style={{ fontSize: 11, opacity: 0.5 }}>via random.org</span>
    </div>
  )
}

const BUTTON = {
  padding: "6px 16px",
  borderRadius: 999,
  border: "1px solid rgba(127,127,127,0.5)",
  background: "transparent",
  color: "inherit",
  font: "inherit",
  fontSize: 13,
  cursor: "pointer",
}

export default defineTool({
  id: "oracle-roll",
  name: "Oracle roll",
  documentTypes: ["oracle-roll"],
  render: OracleRoll,
})

Then add api/roll-oracle.ts from Step 2 or Step 4 — whichever version you want, mock-free either way:

cd oracle-roll && 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 run --world=test-bench
Expected output:
  ● live in Test Bench
 https://beta.vvd.world/worlds/test-bench?dev=9f21c4   — a private tab, only you can see it

Your browser opens on that URL — the same oracle roll you built here, minus the mock. Click the button and useApi really does dispatch to your api/roll-oracle.ts, which really does call out to the hostname you declared, with the key you set.

vvd save
Expected output:
✓ Saved oracle-roll v1 — a private draft only you can see

A private draft is saved, not shipped. One more command puts it in front of your world:

vvd share
Expected output:
→ Sharing with your world…

✓ Shared oracle-roll v1 with your world.

Ship it covers what each command does, who can see the result afterwards, and how to roll back or unshare.

Recap

  • Each file in api/ is one endpoint; the file's path is the route, and the folder's presence is the whole configuration.
  • config.fetch and config.secrets are enforcement, not documentation — an undeclared name or host is unreachable, by construction.
  • useApi calls your own endpoint as a typed function; a throw inside the handler comes back sanitized, never as your real server error.
  • process.env is always {} in a tool bundle — a secret only ever reaches your code through ctx.secrets.get, inside a handler that declared it.

Where to next