Skip to content

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.

CallShapeReturns
secrets.read(key)read a secret by namePromise<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")

read returns the secret’s value as a string, or null when the key is not set. It never throws for a missing keynull 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 service

The 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.

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).

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_secret tool 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.

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.

BackendWhat it isNotes
Anyknown-managedthe encrypted platform store (default)zero-config; reads are cached
HashiCorp Vaultyour own Vault (KV v2)values never pass through the Anyknown store; reads are not cached
AWS Secrets Manageryour own AWS Secrets Managervalues 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.

ErrorWhen it is thrown
InvalidKeyErrorThrown 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.

These are the details that most often trip people up when wiring secrets into handlers.

  • null means not found, not an error. A missing key returns null — it never throws. Always write const 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.read and set the value through set_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.read reads from the local .env file in your App directory — no cloud credentials needed to iterate. The same read code then works in production against the encrypted store. Local reads are not audited.

  • External backends are not cached. Every read against Vault or AWS Secrets Manager fetches from the external store on each call. If that store is unavailable, the read returns null (the fetch failure degrades to not-found) — so your null guard doubles as your outage handler. Reads from the default Anyknown-managed backend are cached.

  • Reads are audited automatically. In the cloud, every successful read emits 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.