Operations
The deployment shape a Magik app is designed for: stateless app servers behind a load balancer, Puma, Postgres, and a worker process.
Status: spec only — none of this runs yet. There is no server, no worker, no Dockerfile, no chart, and no app to deploy. This page describes the intended topology so the framework is built toward it, not a runbook for anything that exists. Reviewed 2026-08-26.
The shape
┌─────────────────┐
clients ────────▶│ load balancer │ TLS, health checks
└────────┬────────┘
│
┌────────────────┼────────────────┐
▼ ▼ ▼
┌──────────┐ ┌──────────┐ ┌──────────┐
│ app 1 │ │ app 2 │ │ app N │ Puma · stateless · identical
└────┬─────┘ └────┬─────┘ └────┬─────┘
└────────────────┼────────────────┘
▼
┌───────────────┐ ┌──────────────┐
│ Postgres │◀─────────│ worker 1..M │ magik worker
│ data · jobs │ └──────────────┘
│ LISTEN/NOTIFY│
└───────────────┘
Two process roles, one image, one codebase:
| Role |
Command |
Serves |
Scales on |
| app |
magik server |
HTTP, htmx fragments, realtime subscriptions |
request concurrency, socket count |
| worker |
magik worker |
the job queue and scheduled jobs |
queue depth, job latency |
Why stateless is the whole design
Spec item 9. App servers hold no in-process session state and no UI state across requests, and the boot guardrails refuse code that tries (../idea/03-guardrails.md).
| Consequence |
|
| Scaling is "add a server" |
no sticky sessions, no session replication, no shared-nothing exceptions to reason about |
| Any server can serve any request |
including the next request of a multi-step flow, whose state lives server-side keyed by a resumable token (../idea/02-dsl-surface.md) |
| Deploys are boring |
drain, replace, done. There is no in-memory state to migrate |
| Reloading is tractable in dev |
for the same reason (../architecture/08-dev-loop.md) |
The guardrail is what keeps the property true after month six. A stateless architecture maintained by code review is a stateless architecture until someone is in a hurry.
What scales how
| Pressure |
Add |
Watch |
| More requests |
app containers |
p95 latency, Puma backlog depth, thread-pool saturation |
| More realtime subscribers |
app containers |
open sockets per process, LISTEN connections against Postgres. How many one process holds is unmeasured (../architecture/12-runtime-verification.md) |
| Deeper job queue |
worker processes |
oldest-queued age, attempts-per-job |
| More data |
Postgres first (vertical, then read replicas) |
connection count, slow queries, index health |
| More tenants |
nothing yet — that is what magik check --scale pre-empts by keeping queries tenant-scoped (../architecture/06-observability.md) |
unscoped query warnings |
Postgres is the thing that does not scale by adding a process. By default it carries data, the job queue, realtime pub/sub and search — which is deliberate: one datastore until scale forces otherwise. When it does force otherwise, the seams are the swap points, not a rewrite (../idea/04-swap-points.md): realtime moves to Redis or NATS, jobs to a broker, search to a search engine — each a use line, each with its own trade-offs stated on that page.
Runtime
| Piece |
Intent |
| Ruby |
TruffleRuby. CRuby ≥ 3.2 is supported for development tooling — magik check, magik generate, the local test loop — and is not a production target |
| Server |
Puma, single mode: one process, one thread pool. The same server in dev and production. HTTP/1.1 from the app process; TLS, HTTP/2 and HTTP/3 terminate at the load balancer |
| Concurrency |
real, parallel OS threads. One request, one thread. TruffleRuby has no GVL, so a process uses the cores it is given — measured, ../architecture/12-runtime-verification.md |
| Scaling a process |
you do not. fork is unavailable on TruffleRuby, so there is no clustered mode; more capacity is more containers, which is what the topology above already assumes |
| Pool sizing |
the database pool equals Puma's max_threads, per process (../architecture/10-performance-defaults.md) |
| Database |
Postgres, via Sequel. UUIDv7 primary keys, tenant_id on every table |
| Client |
htmx (~14kb), served as a static asset. No bundler, no node_modules, no build step |
Deploy
| Concern |
Intent |
| Artifact |
one container image, two roles selected by the command. Identical across environments |
| Config |
environment variables and encrypted credentials; one key in, nothing secret in the image (../architecture/07-configuration-and-secrets.md) |
| Migrations |
applied by a one-shot magik migrate that gates the app rollout, never by an app process racing its peers at startup |
| Health |
a liveness endpoint (the process is up) and a readiness endpoint (config resolved, database reachable, guardrails passed) — the load balancer uses readiness |
| Boot failure |
a guardrail failure aborts before the socket opens, so a bad release fails its readiness check and is never given traffic. That is the deploy-time payoff of boot-time guardrails |
| Drain |
stop accepting, finish in-flight requests, close realtime subscriptions, exit. Workers finish the current job and stop claiming |
| Zero platform primitives |
anything that runs a container runs this. No edge functions, no vendor KV, no proprietary anything |
Observability in production
| Signal |
Source |
| Structured JSON logs, one event per request/action/job/query |
../architecture/06-observability.md |
| Request traces, sampled plus every failure |
same |
| Error codes in logs |
stable MAGIK_* codes, matchable by an alert rule (../architecture/03-error-codes.md) |
| Metrics and spans |
OpenTelemetry as the export path, opt-in — the local structured logs are the primary tool |
What is missing, honestly
| Missing |
|
| Everything above |
there is no framework. This is a design target, As of 2026-08-26 |
| A Dockerfile, a compose file, a chart |
none is written; the image shape above is intent |
| Resource numbers |
no sizing guidance, because nothing has ever been run and a made-up number is worse than none |
| Runbooks |
there are no incidents to learn from yet |
| A scale ladder |
the shape above is one rung. What the rungs above it look like will be written when something has climbed one |