Deployment Modes¶
Strata's deployment_mode is personal or service. A personal server
has exactly one user; a service-mode server serves many, each with their
own identity.
Decision matrix¶
| Personal | Service | |
|---|---|---|
| Best for | One person: a laptop, or a server of your own | A team or customer-facing deployment |
| Writes | Enabled | Off by default (server-side transforms); opt-in client write-back via service_writes_enabled |
| Auth | None | Required, one of two modes: X-Strata-Principal + X-Strata-Proxy-Token from a trusted proxy (trusted_proxy), or API keys (api_key) |
| Identity scoping | None (one user) | Per-tenant cache keys, cache dirs, QoS pools; artifacts filtered by tenant |
| Multi-tenancy | n/a | Optional (multi_tenant_enabled=true) |
| ACLs | Not evaluated | Deny-first (acl_config) |
| Default artifact dir | ~/.strata/artifacts |
None; set STRATA_ARTIFACT_DIR explicitly (required with a blob backend too) |
| Network binding | Loopback only, or non-loopback with allow_remote_clients_in_personal=true |
Unrestricted |
| Use in production for sharing? | No: anyone who reaches it can write | Yes |
The rows that drive the choice are typically Writes (does anyone who reaches the URL get to mutate?), Auth (who decides who's allowed?), and Identity scoping (what's isolated and what's shared?). The flags that follow are the consequences.
Choosing a mode¶
- Personal: running the notebook for yourself. Fast to start, nothing to configure, writes land in your home directory. This is the default for Docker Compose and the "from source" instructions. A personal server you reach over the network is still one person's; see Fly.io deployment for a recipe that hosts one.
- Service: one server for more than one person, users you can't
fully trust, sensitive data, or multi-tenant isolation requirements.
Reads are tenant-scoped and ACL-gated; writes are off by default
(routed through server-side transforms), or authenticated clients
can publish directly to a shared store with
service_writes_enabled. See Service Mode.
Setting the mode¶
Or in pyproject.toml:
Default is personal: the common case. A first-time
uv run strata-notebook boots into a single-user, loopback-only
deployment that just works.
Service mode is explicit opt-in. The coherence checker fires clear
errors at startup if you set deployment_mode=service without
matching auth / artifact configuration, so misconfigured production
deploys fail fast rather than silently exposing write endpoints.
Personal mode¶
The server binds to 127.0.0.1 by default and refuses non-loopback
addresses unless you opt in:
STRATA_DEPLOYMENT_MODE=personal \
STRATA_HOST=0.0.0.0 \
STRATA_ALLOW_REMOTE_CLIENTS_IN_PERSONAL=true \
uv run strata-notebook
Opt in only if you have separate protection (firewall, VPN, private network): personal mode exposes write endpoints with no authentication.
Loopback binding alone does not keep web pages out: the browser runs on
the same machine. Personal mode refuses a cross-origin page's writes
and its notebook WebSocket, and answers only to the Host names it
expects, so a DNS-rebinding page that resolves its own name to
127.0.0.1 gets a 400. The expected
names are localhost, 127.0.0.1, [::1], any IP literal,
STRATA_HOST, and whatever you list in STRATA_ALLOWED_HOSTS. When
you reach a remote personal-mode server by name (devbox.lan,
strata.example.com), list that name:
Artifacts persist to ~/.strata/artifacts unless STRATA_ARTIFACT_DIR
is set. Notebook deletion and session discovery/reconnect APIs are
personal-mode-only.
Service mode¶
STRATA_DEPLOYMENT_MODE=service \
STRATA_AUTH_MODE=trusted_proxy \
STRATA_PROXY_TOKEN=<shared-secret> \
uv run strata-notebook
Short version: an upstream proxy authenticates the caller, sets the
identity headers (X-Strata-Principal, tenant header,
X-Strata-Scopes, X-Strata-Proxy-Token) on every request, replacing
any the client sent (empty when the user has none), and is the only
ingress path. A header the proxy does not set reaches Strata from the
client, so a proxy that never sets X-Strata-Scopes lets a user claim
admin:*. Strata trusts the proxy rather than authenticating users itself, so
machine callers of a proxied server go through the proxy too. API keys
(STRATA_AUTH_MODE=api_key) are a separate auth mode, for a server no
proxy fronts; a trusted-proxy server ignores them.
See Service Mode for the full story:
- The trusted-proxy header contract.
- The shipped demo stack (
docker-compose.service.yml+ nginx proxy injecting two demo identities on ports 8865/8866). - Production reference architecture.
- Multi-tenancy, ACLs, server-side transforms.
- Migration path from personal mode.
Sharing with a team¶
Personal mode has no notion of more than one user, so a team has two shapes:
- A personal server per member. Each member runs their own (a laptop, or one container each behind your proxy) and shares results through a service-mode store; see Connecting a notebook to the shared store.
- One shared server in service mode, where every request carries the caller's identity and scopes. See Service Mode.
Coherence enforcement¶
Strata rejects incoherent mode combinations at startup. These combos
raise ValueError during config load:
| Combination | Why it's rejected |
|---|---|
deployment_mode=personal + auth_mode=trusted_proxy |
Personal mode has no upstream proxy; identity headers would come from the loopback client |
deployment_mode=personal + multi_tenant_enabled=True |
Personal mode is single-user; there are no tenants to isolate |
deployment_mode=personal + require_tenant_header=True |
Same reason, no tenant dimension in personal mode |
deployment_mode=personal + auth_mode=api_key |
Personal mode is single-user on loopback; authenticating yourself to your own machine buys nothing |
deployment_mode=service + auth_mode=none (the default) |
Every route, cell execution and the admin worker registry included, would be open to anyone who reaches the server. Set STRATA_AUTH_MODE to trusted_proxy or api_key |
deployment_mode=service + multi_tenant_enabled or acl_config rules or mcp_enabled, without trusted_proxy / api_key auth |
The tenant header would be spoofable, ACL rules are only evaluated for an authenticated caller, and MCP would have no caller to check |
deployment_mode=service + auth_mode=trusted_proxy without proxy_token |
Every request would be accepted and the identity headers could be spoofed |
deployment_mode=service + service_writes_enabled without auth_mode=trusted_proxy |
Writes are stamped with the caller's principal and tenant |
deployment_mode=service + artifact_metadata_dsn with artifact_blob_backend=local |
Another node would resolve an artifact from the shared database and then find no bytes |
deployment_mode=service + an artifact store without artifact_dir (artifact_metadata_dsn, a non-local blob backend, service_writes_enabled, auth_mode=api_key, or transforms enabled) |
The store is only created when artifact_dir is set, so every artifact route would fail |
If you see one of the personal-mode errors, you almost certainly pulled
flags from a service-mode config into a personal-mode deployment. Remove
the service-specific flags or switch to deployment_mode=service. The
service-mode errors name the setting to add.
Other settings are checked at startup in either mode: a SQL catalog in object
storage needs a uri, the team cache needs STRATA_NOTEBOOK_REMOTE_STORE_URL
and that URL must not be this server, and adaptive concurrency needs its slot
counts inside its range and no multi-tenancy.
Mode-independent settings¶
These apply identically in either mode and can be tuned freely:
rate_limit_*, token-bucket rate limitingacl_config, deny/allow rules (only evaluated undertrusted_proxyorapi_keyauth, which only service mode allows)artifact_blob_backend, local / s3 / gcs / azure- Tracing, logging, S3 / GCS / Azure credentials
- Cache size, cache directory, metadata DB path
public_base_urlandpublic_base_path, the address readers reach the server on (below)
Serving under a path¶
A reverse proxy can serve Strata at a path such as
https://app.example.com/o/acme/lab/ instead of a hostname of its own. Tell
Strata the path:
STRATA_PUBLIC_BASE_PATH=/o/acme/lab
STRATA_PUBLIC_BASE_URL=https://app.example.com # origin only; the path comes from above
The proxy may strip the prefix before forwarding or pass it through; both
reach the same routes. The notebook UI, its WebSocket, the API docs, signed
build URLs and every link on a publication page
carry the prefix. Open the UI with the trailing slash
(https://app.example.com/o/acme/lab/), since its assets load relative to
the page. The prefix's first segment must not be one of Strata's own
(v1, p, assets, docs, redoc, openapi.json, health, metrics,
oembed, mcp), or a stripped request is read as
already carrying it.
The UI's pages send Content-Security-Policy: script-src 'self'; object-src
'none'; base-uri 'self', so a proxy that injects its own inline script (an
analytics snippet, a login banner) has to replace that header for its script to
run.
Clients take the full URL, path included (https://app.example.com/o/acme/lab):
the CLI's --server, the TUI and strata_client all append their routes to it.