Secret Storage
Secret Storage gives your App one type-safe way to read named secrets at runtime: sdk.secrets.read("KEY"). Secrets are values — third-party API keys, tokens, passwords — that you set once and then read inside handler code by name.
You set a secret through the Builder’s set_secret tool or the Anyknown dashboard. The platform stores the value encrypted at rest; your handler code only ever sees the value it reads back, never the ciphertext and never the encryption layer. There is nothing to configure and no plan flag — Secret Storage is available to every App.
Quick reference
Section titled “Quick reference”| Call | Shape | Returns |
|---|---|---|
secrets.read(key) | read a secret by name | Promise<string | null> |
secrets.read(key, opts?) | read with an optional { env } (forward-compat) | Promise<string | null> |
You reach it through the SDK inside a handler:
const sdk = c.get("sdk")const apiKey = await sdk.secrets.read("RESEND_API_KEY")Reading a secret
Section titled “Reading a secret”read returns the secret’s value as a string, or null when the key is not set. It never throws for a missing key — null is the not-found signal. Always guard before using the result, so an unconfigured secret degrades gracefully instead of crashing the request.
read(key: string, opts?: { env?: "prod" | "staging" }): Promise<string | null>const sdk = c.get("sdk")
const apiKey = await sdk.secrets.read("RESEND_API_KEY")if (!apiKey) { return c.json({ error: "messaging is not configured" }, 503)}// use apiKey to call the third-party serviceThe second argument is an optional environment hint:
const apiKey = await sdk.secrets.read("RESEND_API_KEY", { env: "staging" })env accepts "prod" (the default when omitted) or "staging". It is a forward-compatibility seam: it is type-safe and accepted today, but environment scoping is currently determined by which deployment serves the request — production and staging deployments are separate — not by this per-request argument. Pass it if you want your code to be ready for per-request scoping later; today it is a no-op at the read level.
Key naming rules
Section titled “Key naming rules”A secret name (the key) is the contract surface — your code references it, and you never write the value into source. Valid Customer keys are:
UPPER_SNAKE_CASE: uppercase letters, digits, and underscores only.- Start with an uppercase letter.
- At most 128 characters.
A malformed key — lowercase, starting with a digit, or otherwise outside ^[A-Z][A-Z0-9_]{0,127}$ — throws InvalidKeyError immediately, before any network round-trip.
Common names you might register and read:
await sdk.secrets.read("RESEND_API_KEY")await sdk.secrets.read("OPENAI_API_KEY")await sdk.secrets.read("STRIPE_SECRET_KEY")await sdk.secrets.read("TWILIO_AUTH_TOKEN")These are all names. Never hardcode the value itself into your App source (see Things to know).
Setting a secret vs. reading it
Section titled “Setting a secret vs. reading it”There are two distinct interactions, and it helps to keep them separate:
- Set once (onboarding). You register a secret’s name and value through the Builder’s
set_secrettool or the Anyknown dashboard. This is where the value lives; it goes straight into the encrypted store and never appears in your App source. - Read per request (runtime). Your handler calls
sdk.secrets.read("KEY")to retrieve the value when it needs it.
You set the value out-of-band; your code only ever reads it.
Where secrets are stored
Section titled “Where secrets are stored”By default, secrets live in the Anyknown-managed encrypted store — zero configuration, and reads are cached for speed. For Apps that must keep secret values entirely inside their own infrastructure, the platform can route reads to an external secret manager instead. Either way, your code does not change: sdk.secrets.read("KEY") is byte-identical regardless of which backend is active. Provider selection is a platform setting (configured in the Anyknown dashboard or by the Builder), never an SDK argument.
| Backend | What it is | Notes |
|---|---|---|
| Anyknown-managed | the encrypted platform store (default) | zero-config; reads are cached |
| HashiCorp Vault | your own Vault (KV v2) | values never pass through the Anyknown store; reads are not cached |
| AWS Secrets Manager | your own AWS Secrets Manager | values never pass through the Anyknown store; reads are not cached |
When the external backend is selected, you store its connection settings as ordinary secrets through set_secret (e.g. VAULT_ADDR, VAULT_ROLE_ID, VAULT_SECRET_ID, and an optional VAULT_MOUNT; or AWS_SM_REGION, AWS_SM_ACCESS_KEY_ID, AWS_SM_SECRET_ACCESS_KEY, and an optional AWS_SM_PREFIX). These are configuration names you register once, not values that appear in your handler code.
Errors
Section titled “Errors”| Error | When it is thrown |
|---|---|
InvalidKeyError | Thrown synchronously when the key is malformed (not UPPER_SNAKE_CASE, or starts with a digit), or when it is a reserved key. No network round-trip is made. |
A missing key does not throw — it returns null. Reserve your error handling for InvalidKeyError (a code bug, since key names are static) and guard every read for null.
Things to know
Section titled “Things to know”These are the details that most often trip people up when wiring secrets into handlers.
-
nullmeans not found, not an error. A missing key returnsnull— it never throws. Always writeconst v = await sdk.secrets.read("KEY"); if (!v) { ... }so an unconfigured secret degrades gracefully instead of throwing an unhandled error mid-request. -
Never hardcode a secret value in source. Read it at runtime with
sdk.secrets.readand set the value throughset_secret. If a value is hardcoded, Anyknown’s build pipeline detects it and records a finding — the deploy is not blocked, but the value is exposed and should be rotated. If a secret is exposed, the dashboard offers an emergency rotation that marks it compromised and clears the cached value. -
Platform-internal keys are off-limits. Keys that name Anyknown’s own infrastructure secrets cannot be read from App code — attempting to read one throws
InvalidKeyError. Your App only reaches the secrets you registered. This boundary is enforced structurally, and those platform secrets always resolve from the Anyknown store even when your App uses an external backend. -
Local development is zero-config. When you run
anyknown apps dev,sdk.secrets.readreads from the local.envfile in your App directory — no cloud credentials needed to iterate. The samereadcode then works in production against the encrypted store. Local reads are not audited. -
External backends are not cached. Every
readagainst Vault or AWS Secrets Manager fetches from the external store on each call. If that store is unavailable, the read returnsnull(the fetch failure degrades to not-found) — so yournullguard doubles as your outage handler. Reads from the default Anyknown-managed backend are cached. -
Reads are audited automatically. In the cloud, every successful
reademits a fire-and-forget background audit event. This never changes the return value or adds latency to your call. -
EU data residency. If your organization is in the EU, the default Anyknown-managed backend rejects secret writes with a residency-denied response. To hold secret values entirely within your own infrastructure, switch to the Vault or AWS Secrets Manager backend in the platform settings.