Architecture

Understanding what runs where makes every other operational decision — scaling, backup, network policy, upgrades — straightforward.

Services

ServiceImagePortStatelessRequired
appghcr.io/simstudioai/simstudio3000Only with object storage configuredYes
realtimeghcr.io/simstudioai/realtime3002YesYes
migrationsghcr.io/simstudioai/migrationsYes (runs once)Yes
postgresqlpgvector/pgvector:pg175432NoYes
redisredis:7-alpine6379MostlyBundled by both; swap for a managed instance in production
cronghcr.io/simstudioai/cron (Compose) / curlimages/curl (CronJobs)YesYes
piighcr.io/simstudioai/pii5001YesOptional
ollamaollama/ollama11434No (model cache)Optional
telemetryotel/opentelemetry-collector-contrib4317/4318YesOptional

app

The Next.js application: the editor UI, every API route, and the workflow execution engine. Workflow runs happen inside the app process by default, using an isolated-vm sandbox, which is why memory rather than CPU is the constraining resource. Both the chart and the compose file request 4 Gi and cap the app at 8 Gi. Configuring a remote sandbox provider (E2B or Daytona) moves code execution out of the process; see Security.

Any replica can serve any request once object storage is configured. Until then the app writes uploads to its own container filesystem, which makes it stateful — see Where state lives. Scale it horizontally only after reading Scaling & HA for the Redis, storage, and connection-pool prerequisites.

realtime

A Bun Socket.IO server handling collaborative editing, live execution updates, and collaborative documents. Clients connect at /socket.io.

It shares the database and BETTER_AUTH_SECRET with the app (Better Auth's shared-database-session pattern), so it authenticates the same users without a separate login.

Scaling realtime past one replica requires REDIS_URL — the Socket.IO Redis adapter is what carries events between pods. Without it, two users on different pods silently stop seeing each other's edits.

migrations

Applies Drizzle schema migrations, then exits. In Docker Compose it is a one-shot service; in Kubernetes it is an init container on the app Deployment, so migrations run before any app pod becomes ready and re-run (as a no-op) on every rollout.

Migrations are forward-only. See Upgrades.

postgresql

PostgreSQL 17 with the pgvector extension, which is required — knowledge base embeddings are stored and searched as vectors. The pgvector/pgvector:pg17 image ships it; a managed instance needs the extension enabled (Sim's migrations issue CREATE EXTENSION automatically where permissions allow).

This holds essentially all durable state: workflows, runs, logs, users, organizations, credentials, knowledge base chunks and embeddings, and table data.

redis

Backs pub/sub, the Socket.IO adapter, the idempotency store, execution progress markers, distributed execution limits, and the CLI-auth approval store. The storage-like uses fall back to Postgres or in-process state. Pub/sub falls back to a process-local emitter, which is fine on one replica and drops every cross-pod event on more than one. See Redis.

cron

Eighteen scheduled jobs that call internal endpoints — schedule execution, polling triggers, webhook-subscription renewal, connector syncs, outbox processing, data drains, and sandbox-image cleanup. Kubernetes runs them as CronJobs; Docker Compose runs them from a single supercronic service. Same paths, same schedules. See Background Jobs.

Where state lives

Three places once the deployment is configured for production. Everything else is disposable.

StoreContentsBackup
PostgreSQLAll application datapg_dump / managed snapshots + PITR
Object storageUploaded files, KB documents, execution outputs, avatars, logosBucket versioning + lifecycle
SecretsENCRYPTION_KEY, API_ENCRYPTION_KEY, BETTER_AUTH_SECRET, INTERNAL_API_SECRET, CRON_SECRETSecret manager

Object storage is not configured by default, and the fallback is not durable. Sim only uses S3, Azure Blob, or GCS when the corresponding variables are set (S3_BUCKET_NAME + AWS_REGION, AZURE_STORAGE_CONTAINER_NAME + credentials, or GCS_BUCKET_NAME). With none set it writes uploads to a directory inside the app container — and neither docker-compose.prod.yml nor the Helm chart mounts a volume there. Files are lost when the container is recreated and are invisible to other replicas. Configure object storage before storing anything you care about, and before scaling past one replica.

ENCRYPTION_KEY is not recoverable and not derivable. It encrypts workspace and personal environment variables, stored provider API keys, MCP OAuth credentials, and deployment/chat secrets at rest — a database restore paired with a different key yields a working app in which none of that can be decrypted. Back it up separately from the database, and never rotate it casually.

Redis is a cache and message bus. Losing it drops in-flight live updates; it does not lose committed data.

Request paths

Editor / API — browser → ingress/reverse proxy → app:3000 → Postgres, Redis, object storage.

Collaboration — browser → ingress → realtime:3002 (/socket.io, WebSocket upgrade) → Redis pub/sub → other realtime pods. The proxy must pass upgrade headers and allow long-lived idle connections; see Networking.

File upload (object storage configured) — browser asks app for a presigned URL → browser PUTs directly to object storage → app records metadata. This is why buckets need a CORS policy naming your Sim origin. Downloads are proxied back through the app.

File upload (local disk) — the presigned endpoint reports directUploadSupported: false and the browser uploads through the app instead. No CORS configuration is involved, and no bucket is used.

Workflow execution — trigger (manual, API, webhook, or schedule) → app enqueues or runs inline → isolated-vm sandbox → results and logs to Postgres, progress markers to Redis.

Background work — CronJob → Authorization: Bearer $CRON_SECRET → app endpoint → same execution path.

Network boundaries

FromToPurpose
Internetapp:3000, realtime:3002Users
app, realtimepostgresql:5432Data
app, realtimeredis:6379Pub/sub, cache
appObject storage endpointFiles (server side)
BrowserObject storage endpointPresigned uploads — must be publicly reachable
appModel provider APIs, integration APIs, SMTP/email providerOutbound
cronapp:3000 (internal Service / compose network)Scheduled triggers

The chart's optional NetworkPolicy blocks cloud metadata endpoints (169.254.169.254) by default but allows ingress from any pod in the cluster unless you scope networkPolicy.ingressFrom. See Security.

Common Questions

A realtime server must exist — it carries collaborative editing and live execution updates, and the editor degrades badly without one. You can set realtime.enabled=false in the chart, but only when pointing app.env.SOCKET_SERVER_URL at a realtime instance you run yourself.
By default, no — executions run inside the app process using an isolated-vm sandbox, which is why the app has an 8 Gi memory limit. Setting E2B_ENABLED or SANDBOX_PROVIDER=daytona moves user code to a remote sandbox, and TRIGGER_DEV_ENABLED routes async jobs to Trigger.dev; otherwise the database-backed job queue is used.

On this page