---
title: Secrets and environment variables | Runware Docs
url: https://runware.ai/docs/serverless/examples/config-and-secrets
description: Read configuration and credentials on a worker, and see what the platform will and will not give you back.
relatedDocuments:
  - https://runware.ai/docs/serverless/secrets
  - https://runware.ai/docs/serverless/environment-variables
---
## Introduction

Configuration reaches your worker as environment variables, whether you set it with `--env-var` or attached a secret. This app reports which of the names it expects arrived, and never what they hold.

There are no weights, so the configuration is the whole subject.

## The app

**model.py**:

```python
"""What a worker can see of its own configuration, and what it must never answer with."""

from __future__ import annotations

import os

from pydantic import BaseModel, Field

from runware_serverless import serve

EXPECTED = ("LOG_LEVEL", "CACHE_DIR", "API_TOKEN")
"""The names this app reads. Two are plain configuration and one is a secret, and
nothing below can tell which is which."""

class Binding(BaseModel):
    name: str
    present: bool = Field(description="Whether the name reached the worker. Never the value.")

class Report(BaseModel):
    bindings: list[Binding]
    missing: list[str] = Field(description="The expected names that did not arrive.")

@serve
class ConfigAndSecrets:
    def load(self) -> None:
        # Read once, not per request. Changing a variable or an attachment records a
        # new version and rolls the app, so a running worker never holds a stale
        # value: it is replaced by one that read the new set here.
        self.present = {name: name in os.environ for name in EXPECTED}

    def predict(self) -> Report:
        return Report(
            bindings=[Binding(name=name, present=ok) for name, ok in self.present.items()],
            missing=[name for name, ok in self.present.items() if not ok],
        )
```

## How both arrive

`LOG_LEVEL` comes from `--env-var` and `API_TOKEN` from a secret, and `model.py` reads both with the same line. Your code cannot tell them apart, so **they share one namespace**. Name a plain variable the same as an attached secret's injected name and the platform rejects it, and the reverse too.

What differs is what can be read back:

```text
apps env-vars list     CACHE_DIR = /weights             the value is printed
attachments            DOCS_EXAMPLE_TOKEN -> API_TOKEN  only the names
```

You can read a plain variable's value back from the API and you can never read a secret's, and an attachment is recorded as an audit event on your app.

## Keeping the value in the worker

> [!WARNING]
> A handler that returned a secret would publish a credential to **every caller and into the task record**, where it outlives the request. The response here carries presence and nothing else, and a test asserts the value cannot appear in it.

That means your app cannot tell you whether a secret holds the *right* value. You check that on the attachment.

## Reading it once

Changing a variable or an attachment records a new version and **rolls the app**, so the worker holding the old value is replaced by one that read the new set in `load`.

Re-reading `os.environ` on every request guards against something that cannot happen.

## Renaming on attachment

The attachment decides which stored secret fills a variable, so the same code runs against a production and a staging credential without an edit. This app always reads `API_TOKEN`.

## Deploying it

```bash
runware serverless deploy model.py \
  --id config-and-secrets \
  --gpu-type rtx-pro-6000 \
  --env-var LOG_LEVEL=info \
  --env-var CACHE_DIR=/weights
```

Then the secret, which is two steps because one secret usually serves several apps:

```bash
printf '%s' "$TOKEN" | runware serverless secrets set DOCS_EXAMPLE_TOKEN --value-file -
runware serverless secrets attach config-and-secrets DOCS_EXAMPLE_TOKEN --env-var-name API_TOKEN
```

`--value-file -` reads from stdin, so the value never reaches the process list. A value passed as `--env-var` does, which is one more reason anything sensitive belongs in a secret.

## Calling it

**TypeScript**:

```typescript
import { createClient } from '@runware/sdk'

const client = await createClient({ apiKey: process.env.RUNWARE_API_KEY })

const task = await client.invoke({
  appId: 'config-and-secrets',
  endpointPath: 'predict',
  payload: {}
})
```

**Python**:

```python
import asyncio
import os

from runware import Runware

async def main():
    client = Runware(api_key=os.environ["RUNWARE_API_KEY"])
    try:
        task = await client.invoke(
            "config-and-secrets",
            "predict",
            {},
        )
    finally:
        await client.close()

asyncio.run(main())
```

**cURL**:

```bash
curl https://api.serverless.runware.ai/v1/apps/config-and-secrets/invoke-async/predict \
  -H "Authorization: Bearer $RUNWARE_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "taskId": "7c9e6679-7425-40de-944b-e07fc1f90ae7",
    "payload": {}
  }'
```

**CLI**:

```bash
echo '{}' | runware serverless apps invoke config-and-secrets predict -f -
```

**JSON**:

```json
{
  "taskId": "7c9e6679-7425-40de-944b-e07fc1f90ae7",
  "payload": {}
}
```

Before the attach, `API_TOKEN` comes back under `missing`. After it, `present`, while the stored secret is still named `DOCS_EXAMPLE_TOKEN`.

> [!NOTE]
> The first call answered in **56 seconds**, under the sync deadline, so there is no `202` to poll here. An app that declares no dependencies has a small image to pull, and the difference against one carrying torch is minutes.