Secrets
Your app calls external APIs using a key the owner sets in the dashboard; the value never reaches your app's code or the model that built it.
What your app can connect to
Want your app to use another service, like a weather feed, Stripe, an LLM provider, or a notification service? The owner adds the key once, by name, in the app’s dashboard, and the app can call that service. Your agent never sees or handles the secret value itself.
Your app’s backend code references the key by name; Charming fills in the real value on its own infrastructure right before the request leaves, so the plaintext never sits in your app’s source, never shows up in a log, and never reaches the model that’s helping you build the app.
This works for any service that accepts a static API key. It doesn’t cover services that require you to sign in through their own login screen (OAuth).
Technical
The app owner sets, replaces, and deletes secret values from the Secrets section on the app’s dashboard page; an agent never sees or types a value, only the NAME.
Copy this prompt for your agent
Connect my Charming app to an external service. Read
https://charm.ing/docs/capabilities/secrets.md first. Ask me which
service if it isn't obvious from the app, or propose one. Then tell
me the exact secret NAME to set in the app's Secrets section; I'll
add the value there myself.
If you haven't built a Charming app before, read
https://charm.ing/docs/build-mcp.md (or build-http.md for the HTTP API)
for the app skeleton first.
How an agent performs this job
Your agent declares buildy:secrets/fetch@1.0 in the app’s manifest.capabilities.imports (claimed apps only; denied on an anonymous, unclaimed app) and lists the exact host the request needs in manifest.capabilities.fetchHosts; a sealed fetch requires an explicit allowlist, so an empty or missing one denies every request. Inside a route handler it calls env.fetch(url, init) with {{secret:NAME}} inside a header value. The agent doesn’t set or read the value: it tells the owner which NAME to use, and the owner enters the value from the app’s Secrets section in the dashboard. An owner (or anything holding their bld_user_* token) can also manage secrets directly over HTTP: GET, POST, PUT, and DELETE /app/{id}/secrets.
The contract
env.fetch only exists if the app declared buildy:secrets/fetch@1.0. Its shape:
env.fetch(url, init); // Promise<Response> - {{secret:NAME}} in a header value gets substituted
- Substitution happens in header values only, never the URL or body, so a secret can’t leak into a log line or query string.
- Secret names match
^[A-Z][A-Z0-9_]*$, the same charset a{{secret:NAME}}reference uses, so any settable name is always referenceable. - Each value is capped at 8 KiB (8192 bytes) of plaintext, checked before it’s encrypted for storage. Need a bigger value? Contact us.
env.fetchis HTTPS-only and enforces the samemanifest.capabilities.fetchHostsallowlist and public-address (SSRF) guard as plain backendfetch: private, loopback, and cloud-metadata addresses stay blocked even on an allowlisted host.- Values are stored encrypted and are never returned by any endpoint, including to the owner; listing secrets returns names only.
Limits, access, and deletion
- Setting values. Owner-only. From the dashboard, or over HTTP with the owner’s
bld_user_*token:POST /app/{id}/secretsto create a new name (409 secret_already_existsif it’s already set; usePUTto replace it),PUTto replace an existing value,DELETE /app/{id}/secrets?name=NAMEto remove one. App and render tokens are rejected on all of these; only a user token works. - Bad name. A name outside
^[A-Z][A-Z0-9_]*$fails withinvalid_secret_name. - Oversize value. A value over 8 KiB fails with
secret_too_large. - Missing secret. If your code references a
{{secret:NAME}}that isn’t set,env.fetchrejects. Catch that and handle the failure, or prompt the owner to add the secret, rather than sending the request with the placeholder still in it. - Deletion. Deleting a secret removes it immediately. Any code that still references its NAME fails on the next call until the owner sets it again or the code stops referencing it.