Waleed 9d2dd8f550 improvement(helm): helm chart updates with security, ESO, and docs overhaul (#4565)
* improvement(helm): production-ready chart with security, ESO, and docs overhaul

Comprehensive Helm chart improvements bringing the chart up to industry
standards for security, secret management, and documentation.

Security
- Pod Security Standards "restricted" defaults on every pod and container
  (runAsNonRoot, allowPrivilegeEscalation=false, capabilities.drop=[ALL],
  seccompProfile=RuntimeDefault)
- automountServiceAccountToken=false on ServiceAccount and every pod
- NetworkPolicy egress blocks cloud metadata endpoints by default
- Sensitive app/realtime env keys auto-partitioned into chart-managed Secret
  via envFrom; no more plaintext secrets on container specs

Secret management
- Three modes: inline, existingSecret, ExternalSecrets Operator (ESO)
- ESO sync supports arbitrary sensitive keys
- Fail-fast template rendering when ESO enabled but sensitive key unmapped
- AWS/Azure/GCP example files document all three modes

Reliability
- Headless Services for both Postgres StatefulSets
- HPA-aware replicas (omits spec.replicas when autoscaling.enabled)
- PodDisruptionBudget auto-activates when replicaCount > 1
- Startup / liveness / readiness probes with distinct timings
- CronJob ttlSecondsAfterFinished for automatic cleanup

Chart hygiene
- Image tags default to Chart.AppVersion; pullPolicy IfNotPresent
- Optional image.digest pin for content-addressed deploys
- kubeVersion >=1.25.0-0 enforced
- Ollama pinned to 0.23.2; mount moved to /data

Documentation
- README rewritten in cert-manager / Bitnami style
- NOTES.txt with post-install guidance
- Example values files annotated with usage and secret-strategy guidance

* fix(helm): correct resource names in README (sim-sim-* → sim-*)

The sim.fullname helper collapses to the release name when the release
name contains the chart name. With the documented release name 'sim',
actual resources are 'sim-app', 'sim-postgresql', etc. — not the
'sim-sim-*' form previously documented. Fixes copy-paste commands in the
pre-1.0.0 upgrade walkthrough and several troubleshooting snippets.

Also expands the cronjobs component description to reflect the full set
of 13 scheduled jobs (was understated as just Gmail/Outlook polling).

* improvement(helm): split app/realtime env into Secret-bound + inline defaults

- Add app.envDefaults / realtime.envDefaults for chart-shipped operational
  tunables (rate limits, timeouts, IVM, feature-flag defaults, localhost URL
  fallbacks). Rendered inline on the container, not into the Secret
- Remove operational defaults from app.env / realtime.env so the chart-managed
  Secret stays minimal and External Secrets Operator users only map keys they
  actually set, not every chart default
- Skip an envDefaults key when the user explicitly sets it in env (K8s `env`
  overrides `envFrom`, so an inline default would otherwise mask a Secret
  value at runtime)
- Relax values.schema.json to allow empty strings on NEXT_PUBLIC_APP_URL,
  BETTER_AUTH_URL, NEXT_PUBLIC_SUPPORT_EMAIL (defaults supplied via envDefaults)

* fix(helm): address PR review — cronjob validation, ESO apiVersion, secret merge order, image guard

- CronJobs reference CRON_SECRET via secretKeyRef; fail-fast at template
  time when cronjobs.enabled=true and app.env.CRON_SECRET is empty so users
  get a clear error instead of a CreateContainerConfigError loop
- Default externalSecrets.apiVersion to "v1beta1" (supported by every ESO
  release since v0.7). The previous "v1" default targets only ESO v0.17+
- Swap merge order in secrets-app.yaml so app.env wins over realtime.env
  for shared keys (BETTER_AUTH_SECRET, BETTER_AUTH_URL, …) — both pods
  consume the same Secret via envFrom, so the app value must be canonical
- Add `required` guard on sim.image so an empty tag + empty digest +
  empty Chart.AppVersion surfaces as a clear template-time error instead
  of rendering an invalid `repo:` reference

* fix(helm): require critical secrets to be mapped when ESO is enabled

Previously, enabling externalSecrets without mapping BETTER_AUTH_SECRET /
ENCRYPTION_KEY / INTERNAL_API_SECRET (and CRON_SECRET when cronjobs are
on) rendered cleanly but produced CrashLoopBackOff at runtime with
cryptic missing-env errors. Fail at template time instead.

* fix(helm): auto-enable PDB when HPA minReplicas > 1

Previously the auto-enable predicate only checked the static
app.replicaCount, which defaults to 1 even when autoscaling is on
(HPA owns spec.replicas). PDB now also activates when
autoscaling.enabled=true and minReplicas > 1.

* fix(helm): prevent realtime envDefaults from masking app.env Secret values; add StatefulSet upgrade NOTES

- Realtime override-skip now considers keys set in either app.env or
  realtime.env. The shared app Secret is mounted via envFrom on the
  realtime pod, so a key set in app.env (e.g. NEXT_PUBLIC_APP_URL) would
  previously be masked by the realtime envDefault (inline env overrides
  envFrom in K8s).
- NOTES.txt now prints a StatefulSet orphan-delete reminder on upgrade,
  surfacing the immutable serviceName issue documented in the README.

* feat(helm): add Claude Skill for chart deployment

Adds a skill at helm/sim/.claude/skills/sim-helm/ that teaches agents how
to deploy and troubleshoot the Sim Helm chart: install path selection
(inline / existingSecret / ESO), secret generation, the values.yaml
four-layer mental model, common-failure troubleshooting, and the
pre-1.0.0 StatefulSet orphan-delete upgrade procedure.

Skill is loadable by Claude Code, Codex, and OpenCode via the standard
skills convention (directory name matches frontmatter name).

* docs(helm): add CRON_SECRET to TL;DR, dry-run, and example install headers

The validateSecrets guard requires CRON_SECRET when cronjobs.enabled=true
(the default), but the quickstart and example file install commands
omitted it — users following the docs hit a hard template-render failure.
Adds CRON_SECRET to README TL;DR, validate-the-install dry-run snippet,
and the install command headers in all example values files.

* fix(helm): require INTERNAL_API_SECRET in inline secret mode

The ESO coverage validator already required INTERNAL_API_SECRET, but the
inline validateSecrets path only checked BETTER_AUTH_SECRET, ENCRYPTION_KEY,
and CRON_SECRET — letting inline installs render successfully and then
crash at runtime when the realtime↔app shared auth secret was missing.
Adds the same fail-fast check to the inline path.

* docs(helm): surface INTERNAL_API_SECRET upgrade requirement in NOTES.txt

The new validateSecrets check makes app.env.INTERNAL_API_SECRET mandatory
on upgrade. Existing installs that never set it would hit a template
render failure with no in-context guidance. Adds an upgrade-only note
with the generation snippet and storage guidance alongside the existing
StatefulSet orphan-delete instructions.

* fix(helm): NetworkPolicy egress to OTEL collector + external-db example format

- Add app/realtime NetworkPolicy egress rules for the OpenTelemetry
  collector pod on ports 4317 (OTLP gRPC) and 4318 (OTLP HTTP) when
  telemetry.enabled=true. Without these, traces and metrics were silently
  dropped with connection-refused errors when both telemetry and
  networkPolicy were enabled.
- Migrate values-external-db.yaml from the legacy list-shaped egress
  format to the new {exceptCidrs, extraRules} object. The list form would
  replace the default object on merge and crash template rendering when
  the chart tried to access .exceptCidrs on a list.

* fix(helm): NOTES.txt no longer prints false secret warning for ESO users

The secrets-empty warning only checked app.secrets.existingSecret.enabled
before scanning app.env. ESO users intentionally leave app.env empty —
secrets come from the ESO-synced Secret — so every ESO install/upgrade
printed a misleading 'pods will fail to start' warning.

Reorders the branches so externalSecrets.enabled takes precedence: ESO
users now see a confirmation message with kubectl commands to verify the
ExternalSecret has synced. The empty-app.env warning only fires when
both ESO and existingSecret are disabled.

* fix(helm): existingSecret mode no longer drops app.env / realtime.env values

In existingSecret mode the chart-managed Secret is not rendered, so non-empty
values in app.env / realtime.env had nowhere to land — yet the envDefaults
skip logic still suppressed the matching defaults. Result: keys like
NEXT_PUBLIC_APP_URL, BETTER_AUTH_URL, and NODE_ENV silently went missing
on both pods (the example values-existing-secret.yaml hit this directly).

Both app and realtime deployments now inline non-empty values from app.env
(plus realtime.env on the realtime container) when existingSecret is enabled
and ESO is not. Inline / ESO modes are unchanged: inline still flows through
the chart-managed Secret, ESO still owns the synced Secret.

* fix(helm): correct realtime env overlay + filter chart-computed keys in existingSecret mode

Realtime: Sprig merge gives the first source precedence and treats "" as a
real value, so realtime.env empty defaults for shared keys shadowed
non-empty app.env values. Replace with deepCopy($appEnv) base + manual
non-empty overlay of $rtEnv.

Both deployments: exclude DATABASE_URL/SOCKET_SERVER_URL/OLLAMA_URL from
the existingSecret inline path so user-supplied values can't override
chart-computed ones via last-wins env semantics.

* fix(helm): skip envDefaults in existingSecret mode + document egress rename

In existingSecret mode the user's pre-existing Secret is the source of
truth (loaded via envFrom). Inlining localhost envDefaults for URL keys
(BETTER_AUTH_URL, NEXT_PUBLIC_APP_URL, ALLOWED_ORIGINS) silently shadowed
the Secret-bound values because K8s env always wins over envFrom. Skip
envDefaults entirely on both deployments when existingSecret is enabled.

Also call out the networkPolicy.egress shape change (list -> map with
exceptCidrs + extraRules) in the NOTES.txt upgrade block so operators
migrate their custom rules rather than silently losing them.

* fix(helm): copy-pasteable install commands in copilot + ESO examples

values-copilot.yaml: the install header was missing every required
copilot.server.env.* secret (AGENT_API_DB_ENCRYPTION_KEY, INTERNAL_API_SECRET,
LICENSE_KEY, SIM_BASE_URL, SIM_AGENT_API_KEY, REDIS_URL, one model key) plus
copilot.postgresql.auth.password. Pasting it as-is failed at template render.

values-external-secrets.yaml: NEXT_PUBLIC_APP_URL, BETTER_AUTH_URL, etc. were
declared under app.env / realtime.env. In ESO mode the chart-managed Secret
isn't rendered, so the validator (rightly) rejects keys in app.env that
aren't mapped under externalSecrets.remoteRefs. Moved non-secret URL/config
to envDefaults, which is inlined and not subject to the ESO mapping rule.

* polish(helm): configurable NetworkPolicy ingress peers + clearer API_ENCRYPTION_KEY comment

- networkPolicy.ingressFrom lets operators scope the ingress-controller
  rule to a specific namespace/podSelector. Defaults to a single empty
  peer (`- {}`), which is the explicit form of "any source" — same
  effective behavior as the old `from: []` but unambiguous across CNIs.
  To restrict, override with e.g.:
    networkPolicy:
      ingressFrom:
        - namespaceSelector:
            matchLabels:
              kubernetes.io/metadata.name: ingress-nginx

- API_ENCRYPTION_KEY comment: drop the "must be exactly 64 hex
  characters" phrasing that sat awkwardly next to `openssl rand -hex 32`.
  The generation command already produces the required length.

* test(helm): add helm-unittest suites + CI workflow + ci values matrix

- 7 helm-unittest suites covering smoke, validators, secret modes,
  envDefaults secret-mode-aware inlining (round-9 regression net),
  chart-computed env keys (round-8 regression net), NetworkPolicy
  shape, and PDB/HPA conditional rendering (38 tests, ~265ms).
- ci/*.yaml render fixtures for default, production, existingSecret,
  ESO, and external-db install modes.
- GitHub Actions workflow runs helm lint --strict, helm unittest,
  helm template across the ci matrix, and kubeconform validation
  against Kubernetes 1.30 schemas.
- CONTRIBUTING.md documents how to run the same gates locally.

* test(helm): add helm test hook + kind apiserver dry-run in CI

- New templates/tests/test-connection.yaml renders a Pod with
  helm.sh/hook=test that wgets the app Service (and realtime when
  enabled). Lets users run `helm test <release>` after install for
  a real in-cluster connectivity check. Restricted PSS context.
- tests.* values block (image, timeoutSeconds, resources) is the
  knob to disable or tune the probe; documented in values.schema.json.
- 3 helm-unittest tests cover the hook annotations, PSS context,
  and tests.enabled=false skip path (41 tests total).
- New CI job spins up a kind v1.30 cluster and runs
  `kubectl apply --dry-run=server` against the rendered manifests
  for the CRD-free ci fixtures (default / existing-secret /
  external-db). Catches admission and validation issues the static
  kubeconform schema check can't see.

* chore(helm): remove pre-1.0.0 upgrade fluff + tighten .helmignore

This is the 1.0.0 release of the chart — there is no pre-1.0.0
predecessor for users to upgrade from, so all of the dedicated upgrade
narration was hypothetical.

- Drop the 'Upgrading from a pre-1.0.0 build' README section and the
  matching troubleshooting entry.
- Drop the .Release.IsUpgrade block from NOTES.txt: items 5 (StatefulSet
  orphan-delete), 6 (INTERNAL_API_SECRET 'new in 1.0.0'), 7
  (networkPolicy.egress shape change). Each described a migration off a
  chart version that never shipped.
- Delete references/upgrade-pre-1.0.0.md and remove the corresponding
  pointers from SKILL.md.
- Anchor .helmignore patterns to chart root so /tests/ (unit suites)
  and /examples/ are dropped from the packaged tarball without also
  dropping templates/tests/ (the helm test hook).

* chore(helm): drop CI workflow + ci/ fixtures + CONTRIBUTING.md

The helm-unittest suites in helm/sim/tests/ and the helm test hook
in helm/sim/templates/tests/ stay — those are chart-internal quality
scaffolding, not CI. Removed:

- .github/workflows/helm-chart.yml
- helm/sim/ci/*.yaml (5 render fixtures used only by the workflow)
- helm/sim/CONTRIBUTING.md (mostly documented those gates)
- dead /ci/ and /CONTRIBUTING.md entries in .helmignore

* feat(helm): pod rollout on Secret change + topologySpreadConstraints

- Add checksum/secret pod annotations on app, realtime, and copilot
  Deployments (plus checksum/config on app when branding ConfigMap is
  enabled). Closes the long-standing footgun where 'helm upgrade' with
  a changed Secret would silently leave pods running the old values
  until a manual rollout restart.
- New top-level topologySpreadConstraints value (and sim.topologySpreadConstraints
  helper) applied to app and realtime Deployments. Mirrors how affinity
  and tolerations are plumbed; users supply their own labelSelector
  to mirror Bitnami convention.
- 5 helm-unittest cases cover the checksum annotations and topology
  spread rendering (46 tests total).

* fix(helm): drop empty-string shadowing in app/realtime env merge

Sprig 'merge' treats "" as a real value, so a default-empty
app.env.BETTER_AUTH_URL would shadow a non-empty realtime.env override
and the URL would never reach the rendered Secret. Replace 'merge'
with an explicit two-pass overlay that filters empties before writing,
mirroring the same pattern already used in deployment-realtime.yaml's
existingSecret block.

Adds two regression tests: realtime.env-only value reaches the Secret
when app.env is empty, and app.env still wins on collision when both
are non-empty (48 tests total).

* fix(helm): make topologySpreadConstraints per-component to match docstring

Greptile flagged that sim.topologySpreadConstraints helper docstring promised
per-component config (.Values.app, .Values.realtime, ...) but call sites
passed .Values, so any app.topologySpreadConstraints / realtime.topologySpreadConstraints
set by the user was silently dropped. The single global key also prevented
distinct app-vs-realtime spread rules.

Pass .Values.app / .Values.realtime to the helper at each call site; move
the top-level topologySpreadConstraints key into both component sections in
values.yaml. Adds a regression test that app constraints don't leak onto
the realtime pod.

* fix(helm): allow cron pods through app NetworkPolicy

Cursor flagged that when networkPolicy.enabled=true and cronjobs.enabled=true
(the recommended production config), the app NetworkPolicy only allowed
ingress from realtime and the ingress controller — silently blocking every
cron pod's HTTP call to /api/schedules/execute, webhook polls, etc. All 13
default cronjobs would fail.

Tag cron pods with a stable simstudio.ai/component-group: cronjob label so
the app NetworkPolicy can allow them with a single rule (no per-job
enumeration). Rule is conditional on cronjobs.enabled. Adds positive and
negative regression tests.
2026-05-12 10:44:33 -07:00

Sim Logo

The open-source platform to build AI agents and run your agentic workforce. Connect 1,000+ integrations and LLMs to orchestrate agentic workflows.

Sim.ai Discord Twitter Documentation

Ask DeepWiki Set Up with Cursor

Build Workflows with Ease

Design agent workflows visually on a canvas—connect agents, tools, and blocks, then run them instantly.

Workflow Builder Demo

Supercharge with Copilot

Leverage Copilot to generate nodes, fix errors, and iterate on flows directly from natural language.

Copilot Demo

Integrate Vector Databases

Upload documents to a vector store and let agents answer questions grounded in your specific content.

Knowledge Uploads and Retrieval Demo

Quickstart

Cloud-hosted: sim.ai

Sim.ai

Self-hosted: NPM Package

npx simstudio

→ http://localhost:3000

Note

Docker must be installed and running on your machine.

Options

Flag Description
-p, --port <port> Port to run Sim on (default 3000)
--no-pull Skip pulling latest Docker images

Self-hosted: Docker Compose

git clone https://github.com/simstudioai/sim.git && cd sim
docker compose -f docker-compose.prod.yml up -d

Open http://localhost:3000

Sim also supports local models via Ollama and vLLM — see the Docker self-hosting docs for setup details.

Self-hosted: Manual Setup

Requirements: Bun, Node.js v20+, PostgreSQL 12+ with pgvector

  1. Clone and install:
git clone https://github.com/simstudioai/sim.git
cd sim
bun install
bun run prepare  # Set up pre-commit hooks
  1. Set up PostgreSQL with pgvector:
docker run --name simstudio-db -e POSTGRES_PASSWORD=your_password -e POSTGRES_DB=simstudio -p 5432:5432 -d pgvector/pgvector:pg17

Or install manually via the pgvector guide.

  1. Configure environment:
cp apps/sim/.env.example apps/sim/.env
# Create your secrets
perl -i -pe "s/your_encryption_key/$(openssl rand -hex 32)/" apps/sim/.env
perl -i -pe "s/your_internal_api_secret/$(openssl rand -hex 32)/" apps/sim/.env
perl -i -pe "s/your_api_encryption_key/$(openssl rand -hex 32)/" apps/sim/.env
# DB configs for migration
cp packages/db/.env.example packages/db/.env
# Edit both .env files to set DATABASE_URL="postgresql://postgres:your_password@localhost:5432/simstudio"
  1. Run migrations:
cd packages/db && bun run db:migrate
  1. Start development servers:
bun run dev:full  # Starts Next.js app and realtime socket server

Or run separately: bun run dev (Next.js) and cd apps/sim && bun run dev:sockets (realtime).

Copilot API Keys

Copilot is a Sim-managed service. To use Copilot on a self-hosted instance:

  • Go to https://sim.ai → Settings → Copilot and generate a Copilot API key
  • Set COPILOT_API_KEY environment variable in your self-hosted apps/sim/.env file to that value

Environment Variables

See the environment variables reference for the full list, or apps/sim/.env.example for defaults.

Tech Stack

Contributing

We welcome contributions! Please see our Contributing Guide for details.

License

This project is licensed under the Apache License 2.0 - see the LICENSE file for details.

Made with ❤️ by the Sim Team

Languages
TypeScript 77%
MDX 20.8%
JavaScript 1.9%
CSS 0.1%