Skip to content

Hosted mode

This page explains how hosted Otto serves many users from one deployment while it keeps each user's data, keys and computers apart.

The short version: a gateway signs you in with Google and forwards each request, signed, to a tenant that belongs only to you. Your browser and workspace run in their own containers and save checkpoints to private storage.

flowchart TD
  you([You]):::accent --> worker[Public Worker<br/>serves the web app]
  worker -- /api/* --> gateway[Node gateway<br/>Google sessions]
  gateway -- signed request --> tenant[Your tenant<br/>fixed owner]:::accent
  tenant --> db[(Your SQL role<br/>and schemas)]
  tenant --> browser[Browser container]:::muted
  tenant --> workspace[Workspace container]:::muted
  browser -. checkpoints .-> r2[(Private R2)]
  workspace -. checkpoints .-> r2

The gateway and the tenants run the same server image. OTTO_MODE=gateway runs the gateway, and OTTO_MODE=tenant runs one user's Otto.

Container One per Job
GatewayContainer Deployment Authenticates Google sessions, provisions tenants, signs and forwards requests.
TenantContainer User Runs that user's Otto: API, worker, runtime and Pi sessions.
BrowserComputerContainer User The user's browser.
WorkspaceComputerContainer User The user's workspace and connected CLIs.

Provider keys stay in the Node gateway and tenant processes. They never enter the browser or workspace containers.

Fixed owner

A tenant's owner is set when it starts and never changes per request. Public owner or account headers never select a tenant.

Own database login

The gateway provisions a SQL role with private app and jobs schemas. A tenant can't read another tenant's schema or assume its role.

Leased generation

Each tenant start takes a numbered lease. A replacement starts only after the previous generation's stop is confirmed.

Derived keys

OTTO_MASTER_KEY stays in the gateway. Each tenant gets only the keys derived for its own user, including the secret that checks the gateway's signed requests. It never gets the gateway's control or sign-in credentials.

OTTO_MASTER_KEY derives each user's database password, runtime signing secret and Vault key. Admission defaults to eight users and is set with OTTO_MAX_USERS. The tenant code is in src/server/tenants.ts.

The gateway picks your tenant from your signed-in Google session, then signs each forwarded request with an HMAC envelope. The tenant rejects anything that doesn't match. See src/server/gateway.ts and src/server/runtime-auth.ts.

sequenceDiagram
  actor You
  participant G as Gateway
  participant T as Your tenant
  You->>G: Request with your Google session
  G->>G: Find your tenant from the session
  G->>T: Request and signed envelope
  T->>T: Check signature, claims and nonce
  T-->>G: Response
  G-->>You: Response
Envelope field Binds the request to
Owner Your user ID
Environment The deployment, such as development or production
Generation The tenant generation that holds the lease
Method and target The exact HTTP method and path
Body hash A SHA-256 hash of the exact body, so an altered body is rejected
Nonce A one-time ID, so a replayed request is rejected
Expiry 30 seconds after signing
Developer capability Whether you're in OTTO_DEVELOPER_IDS. Only developers reach the standalone /api/browser routes.

The gateway forwards only the content type, accept and range headers. For streams such as chat events, it rechecks your session while the stream stays open.

Both containers start on first use and save checkpoints to private R2, under <environment>/<owner>/.

Browser Workspace
Checkpoints When it goes idle, which closes its pages About 20 seconds after its last command, and before it sleeps, stops or is drained
Saves Cookies, Local Storage, IndexedDB and Service Worker data; skips caches /home/node/state, without __pycache__ and .cache folders
Size limit 1 GiB uncompressed, 512 MiB per file, 100,000 entries The same

While a conversation turn waits on you for a question, approval or credential request, a running browser is held for up to 30 minutes so its pages survive. The next turn or a reset releases it. A hold never starts a stopped browser.

A workspace too large to save keeps running on its previous checkpoint, and every command still runs. The model's next bash output starts with the sizes, the largest files and what to delete. The unsaved changes are discarded when the workspace sleeps, stops or is drained, and the next bash command says so.

Workspace commands run as node, and connected CLI commands run as otto-connected. A deployed container gives every process root's capabilities, whatever its user. So both users start through setpriv --no-new-privs with their capabilities cleared. Otherwise, a workspace command could read a CLI token from /proc.

Connected CLIs run from sealed installs under /home/otto-connected/cli. Those installs aren't checkpointed, so each CLI is installed again from its declared source on first use after a restart. otto-connected can read shared workspace files but can't write them.

  • Saving new cards, updating cards and paying with a card. Cards saved earlier stay readable and removable.
  • Any request where the signed owner, environment, generation, method, target or body doesn't match.
  • Starting a replacement tenant before the previous generation has stopped.