Secrets
Store credentials once in an environment, attach them to the apps that need them, and read them as environment variables at runtime.
Introduction
A secret is stored once in an environment and attached to the apps that need it. The value stays out of your source, your image, and the definition you deploy.
A name belongs to one environment, so the same name in another is a different secret with its own value. One call copies a secret into other environments, value included, so the same credential reaches staging without being sent again.
At runtime it arrives as an environment variable, so your code reads it the way it reads any other.
import os
@serve
class FluxDev:
def load(self) -> None:
token = os.environ["HF_TOKEN"]Two steps, deliberately
Creating a secret and using it are separate operations, because one secret usually serves several apps.
It is also why naming a secret on the deploy that creates an app refuses the whole create when the environment holds no active secret of that name, with one secret-not-found entry per attachment that missed. Create the secret first, then attach it.
runware serverless secrets set HF_TOKEN
runware serverless secrets attach my-app HF_TOKENThe first stores it against your organization. The second records that this app may read it and, on the next rollout, delivers it.
Detaching is the same in reverse, and it removes the app's access without touching the secret or any other app using it. Deleting the secret itself is refused while any app still attaches it, so detach every holder first.
The name it lands under
An attachment resolves to the environment variable name the value is injected as. By default that is the secret's own name, and envVarName overrides it when the two should differ.
{
"secretName": "hf-token-prod",
"envVarName": "HF_TOKEN"
}That indirection is what lets the same code run against a production and a staging credential: the app always reads HF_TOKEN, and the attachment decides which stored secret fills it.
Secrets and plain environment variables share one namespace on the worker. A resolved secret name that collides with an existing variable is rejected with a 422, and so is the reverse. There is no last-one-wins, on purpose: a silent override of a credential is not a failure mode worth having.
Two attachments on the same app cannot resolve to the same name either. That returns 409, as does attaching a secret the app already has.
Copying it into another environment
One call creates a secret in each of targetEnvironments with this one's name, type, metadata and value. It copies the secret in environment, production when omitted, and the value is copied inside the platform and never returned.
curl -X POST https://api.serverless.runware.ai/v1/secrets/HF_TOKEN/copy \
-H "Authorization: Bearer $RUNWARE_API_KEY" \
-H "Content-Type: application/json" \
-d '{ "targetEnvironments": ["staging"] }'Each copy is a separate secret from then on, so changing or deleting one leaves the original and the other copies as they are. The copy is one write: every target gets the secret, or none does.
A target where the name is already in use returns 409, with one entry per taken target in the order you sent them, so a copy into several reports all of them at once. A pending_destroy secret of that name counts as in use. Naming the secret's own environment returns 422.
Copying attaches nothing. A copy reaches a worker only once you attach it to an app in that environment.
Creating in several at once works from the other end: a batch item names its own environments, and then every item in that batch names them.
Attaching reaches a running worker
This is where secrets differ from most configuration. Attaching one rolls the app's live deployment in place, so a worker that is already serving picks up the value from the attach alone.
Changing the value does the same. A write re-encrypts it under the same name and rolls every live app that attaches it, which is how a credential is rotated without touching the apps.
There are two edges. If a rollout is already in progress when the write commits, it may not land on that one and arrives on a later redeploy instead. And an app that is not live records the change only, because the next deploy reads the attachment set and the value fresh anyway.
What secrets are not for
A secret's type is generic or registry, both stored the same way, and only generic attaches. A registry secret cannot be attached to an app, with secret-not-attachable: a registry login is read when an image is pulled, which is before a worker exists, so it has no unseal path into a worker's environment. It is stored for a build to read, and a private base image is refused with base-image-unreadable.
Cloud credentials usually do not need to be secrets either. For AWS, Google Cloud or a service of your own that trusts OpenID Connect, an OIDC connector gives each worker a short-lived token instead of a key you store and rotate.
Secrets are for sensitive values. Anything else belongs in an environment variable, which is simpler and visible when you are debugging.
Keeping the value out of your shell
--value takes the secret on the command line, and you should assume anything passed that way is recorded. It appears in the process list while the command runs and in your shell history afterwards. --value-file reads it from a file instead, and --value-file - from standard input.
runware serverless secrets set HF_TOKEN --value-file ./token.txt
printf '%s' "$HF_TOKEN" | runware serverless secrets set HF_TOKEN --value-file -Reading the value from a file avoids both. .env files and anything you keep the value in are never uploaded with your source, so one sitting in your project directory does not travel to the build.