Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -249,6 +249,27 @@ Self-hosted deployments (billing disabled) run without plan limits: no rate limi
Neither deployment presets these. The Helm chart previously did, which enforced hosted-plan caps on self-hosted installs; chart 1.5.0 removed the presets so Compose and Kubernetes behave identically.
</Callout>

### Removing a limit you already inherited

If a limit is still enforced after upgrading — most often a `FREE_TABLES_LIMIT` or `FREE_TABLE_ROWS_LIMIT` carried forward from a chart older than 1.5.0, or copied into your own values file — the variable is still reaching the pod. On Helm, remove it by overriding it with `null`:

```yaml
app:
envDefaults:
FREE_TABLES_LIMIT: null
FREE_TABLE_ROWS_LIMIT: null
```

Setting the variable to an empty string does **not** remove it: the chart reads an empty value as "not specified", so the inherited value still applies.

Null the variable in every layer that sets it. If it appears in both `app.env` and `app.envDefaults`, nulling only the `app.env` entry lets the `envDefaults` value apply again and the limit stays in force. With External Secrets, also drop the key from `externalSecrets.remoteRefs.app`, which keeps syncing it independently. Confirm what the pod will actually receive before rolling out:

```bash
helm template sim ./helm/sim -f values.yaml | grep -A1 FREE_TABLE # expect no output
```

`null` deletion has no effect under `helm upgrade --reuse-values` — pass your full values with `-f`, or use `--reset-then-reuse-values` (Helm 3.14+). If you deploy with Argo CD, put the `null` in `valueFiles` or the `values` string rather than `valuesObject`, which strips nulls. On Docker Compose, delete the line from your `.env` file.

## Example .env

```bash
Expand Down
18 changes: 17 additions & 1 deletion helm/sim/.claude/skills/sim-helm/references/values-model.md
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ The Sim chart splits configuration across **four** layers. Understanding which l

**ESO compatibility.** When `externalSecrets.enabled=true`, the chart-managed Secret is **not rendered** — ESO renders one instead. Anything in Layer 1 must be mapped via `remoteRefs.app.<KEY>` or it's silently missing. Layers 2–4 are unaffected by ESO.

**Override precedence.** Values set in `app.env` (Layer 1 overrides) win over `envDefaults` (Layer 2) — so users who already had operational tunables in `app.env` continue to work.
**Override precedence.** *Non-empty* values set in `app.env` (Layer 1 overrides) win over `envDefaults` (Layer 2) — so users who already had operational tunables in `app.env` continue to work. An *empty* `app.env` value does not override: it reads as "not specified" and the Layer 2 default still applies. To remove a key, override it with `null` (see "I want to REMOVE a key the chart sets" below).

## Where keys live — the canonical list

Expand Down Expand Up @@ -103,6 +103,22 @@ app:

Prefer Layer 2 for non-sensitive tunables — keeps the Secret lean and ESO mapping minimal.

### "I want to REMOVE a key the chart sets"

Override it with `null` — Helm's documented deletion mechanism. It drops the key from the merged values, so no template emits it.

```yaml
app:
envDefaults:
FREE_TABLES_LIMIT: null
```

**Do not use `""` — it is a silent no-op — and do not add a chart-level "unset list" to work around that.** Every key under `app.env` in `values.yaml` ships as a `""` placeholder, so the templates must read `""` as "not specified"; ten of those collide with a real `envDefaults` value (`NEXT_PUBLIC_APP_URL`, `BETTER_AUTH_URL`, `NEXT_PUBLIC_BRAND_NAME`, `VERTEX_LOCATION`, `EMAIL_VERIFICATION_ENABLED`, …) and would blank themselves out on every default install if `""` meant "delete". A list-shaped unset key is also the wrong interface — Helm merges dicts but not lists, so it cannot be modified or unset downstream.

The `(ne (toString $value) "<nil>")` guards throughout the templates are what make `null` deletion work — preserve them in any new render path. Regression net: `tests/env-null-deletion_test.yaml`.

Caveat worth passing to operators: `null` has no effect under `helm upgrade --reuse-values`.

### "I want to set my production app URL"

```yaml
Expand Down
2 changes: 1 addition & 1 deletion helm/sim/Chart.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ apiVersion: v2
name: sim
description: A Helm chart for Sim - the open-source AI workspace where teams build, deploy, and manage AI agents
type: application
version: 1.5.3
version: 1.5.4
appVersion: "v0.7.44"
kubeVersion: ">=1.25.0-0"
home: https://sim.ai
Expand Down
33 changes: 32 additions & 1 deletion helm/sim/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -358,11 +358,42 @@ User-supplied `securityContext` values are merged with the defaults — your val
Other security features:

* `automountServiceAccountToken: false` on the ServiceAccount **and** every pod.
* Every value in `app.env` and `realtime.env` is written to a chart-managed Secret and mounted via `envFrom: secretRef` — no values are inlined on the container spec. This eliminates a sensitivity classifier (no static list of "secret" keys to maintain) and ensures new provider keys can never accidentally leak into pod manifests. Two categories are inlined on the container instead: chart-computed values (`DATABASE_URL`, `SOCKET_SERVER_URL`, `OLLAMA_URL`, `PII_URL`) and operational defaults under `app.envDefaults` / `realtime.envDefaults` (rate limits, timeouts, IVM tunables, feature-flag defaults, branding defaults, `http://localhost:3000` URL fallbacks). Operational defaults are non-sensitive by design — moving them out of `app.env` keeps the Secret small and means External Secrets Operator users only have to map the keys they actually set, not every chart default. A value placed in `app.env` always wins over the same key in `app.envDefaults` (the template skips the inline default when an override exists).
* Every value in `app.env` and `realtime.env` is written to a chart-managed Secret and mounted via `envFrom: secretRef` — no values are inlined on the container spec. This eliminates a sensitivity classifier (no static list of "secret" keys to maintain) and ensures new provider keys can never accidentally leak into pod manifests. Two categories are inlined on the container instead: chart-computed values (`DATABASE_URL`, `SOCKET_SERVER_URL`, `OLLAMA_URL`, `PII_URL`) and operational defaults under `app.envDefaults` / `realtime.envDefaults` (rate limits, timeouts, IVM tunables, feature-flag defaults, branding defaults, `http://localhost:3000` URL fallbacks). Operational defaults are non-sensitive by design — moving them out of `app.env` keeps the Secret small and means External Secrets Operator users only have to map the keys they actually set, not every chart default. A **non-empty** value placed in `app.env` wins over the same key in `app.envDefaults` (the template skips the inline default when an override exists). An **empty** value does not — to remove a key rather than change it, see [Removing an inherited env key](#removing-an-inherited-env-key).
* Optional `networkPolicy.enabled=true` enforces east-west isolation and blocks cloud metadata endpoints in egress.

---

## Removing an inherited env key

To remove a key the chart (or an older values file) sets, override it with `null` — [Helm's documented way](https://helm.sh/docs/chart_template_guide/values_files/) to delete a default key:

```yaml
app:
envDefaults:
FREE_TABLES_LIMIT: null
FREE_TABLE_ROWS_LIMIT: null
```

Or on the CLI: `--set app.envDefaults.FREE_TABLES_LIMIT=null`.

**Null the key in every layer that sets it.** `null` deletes the key from the map you null, not from the pod — so if a key is set in both `app.env` and `app.envDefaults`, nulling only the `app.env` entry makes the inline `envDefaults` value apply again and the variable stays on the pod. The same holds for `realtime.env` / `realtime.envDefaults`. Under ESO there is a third source: a key mapped in `externalSecrets.remoteRefs.app` keeps being synced into the Secret regardless of `app.env`, so remove that mapping too. Rendering the manifest (below) is the reliable way to confirm the key is actually gone.

**Setting the key to `""` instead does not remove it.** Every key under `app.env` in `values.yaml` ships as a `""` placeholder, so the templates have to treat an empty string as "the operator said nothing" — if they did not, the ten placeholders that collide with a real `app.envDefaults` value (`NEXT_PUBLIC_APP_URL`, `BETTER_AUTH_URL`, `NEXT_PUBLIC_BRAND_NAME`, `VERTEX_LOCATION`, `EMAIL_VERIFICATION_ENABLED`, …) would blank themselves out on every default install. An empty entry is a silent no-op; `null` is the deletion.

With the chart-managed Secret (the default), nulling a key the application cannot start without (`BETTER_AUTH_SECRET`, `ENCRYPTION_KEY`, `INTERNAL_API_SECRET`, or `CRON_SECRET` with `cronjobs.enabled=true`) fails at template time with the existing required-secret error rather than at runtime. In `existingSecret` mode the chart skips that validation entirely — those values come from your pre-created Secret, which the chart cannot read — so a null there renders successfully and the key is simply absent from `app.env`. Under ESO the key must still be mapped in `externalSecrets.remoteRefs.app`, which is validated at template time.

> **Caveats.** `null` deletion does not take effect under `helm upgrade --reuse-values` ([helm#30765](https://github.com/helm/helm/issues/30765)) — pass your full values with `-f`, or use `--reset-then-reuse-values` (Helm ≥ 3.14).
>
> On **Argo CD**, put the `null` in `spec.source.helm.valueFiles` or the `values` string. Argo CD strips nulls from the structured `valuesObject` field ([argo-cd#16312](https://github.com/argoproj/argo-cd/issues/16312), [#19781](https://github.com/argoproj/argo-cd/issues/19781)), so a null written there silently does nothing.

The common case is a free-tier cap inherited from a chart release older than the one that stopped presetting them, which shipped `FREE_TABLES_LIMIT: "3"` and `FREE_TABLE_ROWS_LIMIT: "1000"` under `app.envDefaults`. With billing disabled, Sim reads an unset limit as unlimited, so nulling these lifts the cap. Verify before rolling out:

```bash
helm template sim ./helm/sim -f values.yaml | grep -A1 FREE_TABLE # expect no output
```

---

## Autoscaling

```yaml
Expand Down
141 changes: 141 additions & 0 deletions helm/sim/tests/env-null-deletion_test.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,141 @@
suite: removing an inherited env key — null deletion, and why "" does not work
release:
name: t
namespace: sim
defaults: &defaults
app.env.BETTER_AUTH_SECRET: xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
app.env.ENCRYPTION_KEY: xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
app.env.INTERNAL_API_SECRET: x
app.env.CRON_SECRET: x
postgresql.auth.password: xxxxxxxx
tests:
- it: baseline — a preset envDefaults cap is inlined on the app pod
template: deployment-app.yaml
values:
- values/preset-free-limits.yaml
set:
<<: *defaults
asserts:
- contains:
path: spec.template.spec.containers[0].env
content:
name: FREE_TABLES_LIMIT
value: "3"
- contains:
path: spec.template.spec.containers[0].env
content:
name: FREE_TABLE_ROWS_LIMIT
value: "1000"

- it: an empty string does NOT clear an envDefaults key — it reads as "unspecified"
template: deployment-app.yaml
values:
- values/preset-free-limits.yaml
set:
<<: *defaults
app.env.FREE_TABLES_LIMIT: ""
asserts:
- contains:
path: spec.template.spec.containers[0].env
content:
name: FREE_TABLES_LIMIT
value: "3"

- it: null removes the key from the inlined envDefaults
template: deployment-app.yaml
values:
- values/preset-free-limits.yaml
- values/unset-free-limits.yaml
set:
<<: *defaults
asserts:
- notContains:
path: spec.template.spec.containers[0].env
content:
name: FREE_TABLES_LIMIT
value: "3"
- notContains:
path: spec.template.spec.containers[0].env
content:
name: FREE_TABLE_ROWS_LIMIT
value: "1000"

- it: null removes an app.env key from the chart-managed Secret
template: secrets-app.yaml
values:
- values/preset-free-limits.yaml
- values/unset-free-limits.yaml
set:
<<: *defaults
asserts:
- isNull:
path: stringData.FREE_STORAGE_LIMIT_GB

- it: the app.env key is present in the Secret before it is nulled
template: secrets-app.yaml
values:
- values/preset-free-limits.yaml
set:
<<: *defaults
asserts:
- equal:
path: stringData.FREE_STORAGE_LIMIT_GB
value: "5"

- it: nulling unrelated keys leaves the rest of envDefaults intact
template: deployment-app.yaml
values:
- values/preset-free-limits.yaml
- values/unset-free-limits.yaml
set:
<<: *defaults
asserts:
- contains:
path: spec.template.spec.containers[0].env
content:
name: BETTER_AUTH_URL
value: http://localhost:3000
- contains:
path: spec.template.spec.containers[0].env
content:
name: BILLING_CONCURRENCY_LIMIT_FREE
value: "10"

- it: nulling only app.env lets a matching envDefaults value apply again
template: deployment-app.yaml
values:
- values/preset-free-limits.yaml
set:
<<: *defaults
app.env.FREE_TABLES_LIMIT: null
asserts:
- contains:
path: spec.template.spec.containers[0].env
content:
name: FREE_TABLES_LIMIT
value: "3"

- it: nulling both layers is what actually removes the key
template: deployment-app.yaml
values:
- values/preset-free-limits.yaml
- values/unset-free-limits.yaml
set:
<<: *defaults
app.env.FREE_TABLES_LIMIT: null
asserts:
- notContains:
path: spec.template.spec.containers[0].env
content:
name: FREE_TABLES_LIMIT
value: "3"

- it: nulling a boot-critical key is still refused by the existing validator
values:
- values/preset-free-limits.yaml
set:
<<: *defaults
app.env.ENCRYPTION_KEY: null
asserts:
- failedTemplate:
errorMessage: app.env.ENCRYPTION_KEY is required for production deployment
8 changes: 8 additions & 0 deletions helm/sim/tests/values/preset-free-limits.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
# Simulates a chart release older than the one that stopped presetting free-tier
# caps, plus a values file that carried those keys forward.
app:
envDefaults:
FREE_TABLES_LIMIT: "3"
FREE_TABLE_ROWS_LIMIT: "1000"
env:
FREE_STORAGE_LIMIT_GB: "5"
7 changes: 7 additions & 0 deletions helm/sim/tests/values/unset-free-limits.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
# Helm removes a key from the merged values when it is overridden with null.
app:
envDefaults:
FREE_TABLES_LIMIT: null
FREE_TABLE_ROWS_LIMIT: null
env:
FREE_STORAGE_LIMIT_GB: null
5 changes: 5 additions & 0 deletions helm/sim/values.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -322,6 +322,11 @@ app:
# app container — NOT written into the chart-managed Secret and NOT required to be mapped
# when externalSecrets.enabled=true. Override any key by setting `app.envDefaults.KEY` in
# your values file. Move a key into `app.env` above only if it must be treated as secret.
#
# To REMOVE a key rather than change it, override it with `null` — Helm deletes a default
# key when it is overridden with null (e.g. `FREE_TABLES_LIMIT: null`). Setting it to ""
# does NOT remove it: every key under `app.env` ships as a "" placeholder, so the templates
# must read "" as "unspecified". See "Removing an inherited env key" in README.md.
envDefaults:
# Application URLs (override in app.env or app.envDefaults for production)
NEXT_PUBLIC_APP_URL: "http://localhost:3000"
Expand Down
Loading