> ## Documentation Index
> Fetch the complete documentation index at: https://docs.withnubo.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Environment variables

> Inject config and secrets into your app at runtime

Environment variables are how you feed config into your app - API keys, database URLs, feature flags, anything that shouldn't be hardcoded. Nubo encrypts variables at rest and injects them into your app's runtime environment. They are not available during the build step.

## Where variables live

Variables are scoped to either a **Project** or a **Frame**:

* **Project variables** apply to every Frame in the Project. Good for things shared across apps (`SENTRY_DSN`, `DATABASE_URL`).
* **Frame variables** apply to one Frame only. Good for app-specific settings (`PORT`, `NODE_ENV`).

If the same key exists at both levels, the Frame value wins.

<Note>
  When you wire a managed database to an app (via `nubo.toml`), Nubo auto-injects the connection string as a Frame variable named `DATABASE_URL` (the key is configurable with `inject_as` in `nubo.toml`). If you also set `DATABASE_URL` yourself, the platform-managed value can override it.
</Note>

## Add variables

From the dashboard, open the Frame and go to **Frame Settings** to manage its variables. The dashboard manages variables at the Frame level only; to set project-scoped variables, use the API (`POST /v2/projects/<id>/variables`). Keys must start with a letter or underscore and contain only letters, numbers, and underscores (`[A-Za-z_][A-Za-z0-9_]*`).

Via the API (note: the body is a JSON **array**, so you can add multiple at once):

```bash theme={null}
# project-scoped
curl -X POST https://shuttle.withnubo.com/v2/projects/<project_id>/variables \
  -H "Authorization: Bearer <your-token>" \
  -H "Content-Type: application/json" \
  -d '[{"key": "DATABASE_URL", "value": "postgres://..."}]'

# frame-scoped
curl -X POST https://shuttle.withnubo.com/v2/projects/<project_id>/spaces/<space_id>/frames/<frame_id>/variables \
  -H "Authorization: Bearer <your-token>" \
  -H "Content-Type: application/json" \
  -d '[{"key": "LOG_LEVEL", "value": "debug"}, {"key": "API_KEY", "value": "..."}]'
```

## Using variables in your app

Variables are available exactly like any other env var - no special SDK or prefix.

```js theme={null}
// Node.js
const dbUrl = process.env.DATABASE_URL;
```

```python theme={null}
# Python
import os
db_url = os.environ["DATABASE_URL"]
```

```go theme={null}
// Go
import "os"
dbURL := os.Getenv("DATABASE_URL")
```

They're injected into your app's runtime environment and handle the usual connection strings and secrets. They are not available during the build step.

## Update or remove

Changing a variable doesn't automatically redeploy - trigger a new deploy from the dashboard or push a commit to apply the change. To change a value (the rotate-a-secret flow), PATCH the variable by its ID:

```bash theme={null}
curl -X PATCH https://shuttle.withnubo.com/v2/variables/<variable_id> \
  -H "Authorization: Bearer <your-token>" \
  -H "Content-Type: application/json" \
  -d '{"value": "new-value"}'
```

To delete:

```bash theme={null}
curl -X DELETE https://shuttle.withnubo.com/v2/variables/<variable_id> \
  -H "Authorization: Bearer <your-token>"
```

## Secrets

Values are encrypted at rest. Listing variables on a Project or Frame returns the keys and metadata only. To see a single value, use the reveal endpoint:

```bash theme={null}
curl https://shuttle.withnubo.com/v2/variables/<variable_id> \
  -H "Authorization: Bearer <your-token>"
```

Rotate a secret by setting a new value; the old one is overwritten.

<Warning>
  Avoid logging environment variables in your app. Runtime logs stream to the dashboard, and accidental secret leakage there is easy to do and hard to undo.
</Warning>

## The port your app should listen on

Nubo doesn't set `PORT` for you. The port configured on your Frame is the source of truth for routing, so your app must listen on exactly that port. Setting a `PORT` variable is just a convenience your app can read: it does not change the routed port, and its value must match the Frame's configured port.
