{"id":"1d4da475-379a-4557-ba84-e2f6d60fa414","slug":"mainbrella-backend","kind":"skill","name":"backend","summary":"backend for web","owner":"mainbrella","repoUrl":"https://github.com/mainbrella/backend","license":"GPL-3.0","licenseTier":"strong-copyleft","trust":"community","stars":3,"match":null,"fewStars":true,"format":"skill","createdAt":"2026-10-02T21:25:50.000Z","addedAt":"2026-10-21T23:56:56.098Z","logoUrl":"https://avatars.githubusercontent.com/u/336963745?v=4","linkOnly":false,"collection":null,"skillPath":null,"categories":["cloud-infra"],"tags":["agents","containers","linux","sandbox","cloudflare","cloudflare-api","cloudflare-d1","cloudflare-workers"],"runtime":"binary","local":true,"cloudCapable":false,"cloudCapability":"unknown","cloudReadyOnPlatform":false,"needsApiKey":false,"reviewed":false,"syncedAt":"2026-10-26T01:10:08.208Z","sample":false,"url":"https://conduits.dev/tools/mainbrella-backend","language":"TypeScript","transports":["stdio"],"install":{},"requirements":{"env":[],"upstreamMayCharge":false},"tools":null,"readme":"# Mainbrella API\n\nServer code lives in this repository: API routes and schemas in `worker/app`,\nBFF in `worker/bff`, runtime and wallet code in `containers`, and SDKs in `sdk`.\nVoice, compute, Builder, BFF, account and billing behavior share the existing API\nWorker and persisted resource identities. There are no npm workspaces.\n\n```sh\nnvm use\nnpm ci\nnpm run dev\nnpm test\nnpm run type-check\n```\n\nThe website and admin frontend are separate sibling repos, `../web` and\n`../raindrop`, with their own installs and builds. BFF setup, user login,\nbrowser keys, row permissions, and the public example are documented in\n[docs/bff.md](docs/bff.md). The example's React frontend lives in\n`../web/examples/message-board`; the CLI remains in `../canopy/cli`.\n\nOrdered D1 migrations remain in `migrations`, including BFF migrations 050–056.\nKeep their order and apply pending migrations before releasing the API. The\naccount Durable Object bindings, database IDs, storage namespaces, and routes\nretain their existing identities. See [billing](docs/billing.md) for the shared\nusage charging interface and [STAGING.md](STAGING.md) for staging operations.\n\n## Deploy the API from your Mac\n\nFrom this directory, run:\n\n```sh\nnpm run deploy\n```\n\nThis uses your Wrangler login or `CLOUDFLARE_API_TOKEN`, applies pending remote\naccount D1 migrations, runs API preflight, and publishes `mainbrella-api` directly\nfrom the current checkout. GitHub Actions and a push are not required. For the\nfirst login, run `npx wrangler login`. BFF also requires its two deployed\n[configuration secrets](docs/bff.md#setup).\n\n`npm run deploy:api` also applies pending remote account D1 migrations before\npublishing and stops if migration fails. Wrangler options such as\n`npm run deploy:api -- --outdir=dist` are passed through to deployment.\n\nUse `npm run deploy -- --check` to run API checks and bundle without provider\naccess or publishing. The previous complete backend rollout remains available as\n`npm run deploy:all`; it includes container images, the preview gateway, and\nprivate services. Deploy the web UI separately afterward.\n\nThe session-authenticated `/containers` API resolves paid access from Stripe and\nreserves starts in an account-owned `ContainerAccount` Durable Object. Each\ncontainer runs in its own private `UserContainer` in `mainbrella-containers`.\nThe original `user:<userId>` machine remains the `small` slot; additional slots\nuse `user:<userId>:slot:<number>`. Container creation and quota reservation remain\nexclusive to `POST /containers`; client ownership, plan, resource and lease\nheaders are never forwarded.\n\nThe [preview ingress design and private runtime contract](docs/preview-ingress.md)\ncover the runtime, gateway, account endpoints and SDK/dashboard integration.\nPreviews are enabled on `mainbrella.dev`; live transport, Next.js/browser,\ndevelopment WebSocket/hot reload and account/generation isolation checks passed.\n\n## Browser terminal\n\n`GET /containers/terminal?id=<container id>&createdAt=<ISO generation>&cols=80&rows=24` requires\n`Upgrade: websocket`, an explicit allowlisted browser Origin, and the existing\n`mainbrella_session` cookie. `createdAt` is required and must exactly match the\ncurrent running container. Dimensions default to 80×24, must be integer strings,\nand clamp to 1–500 columns / 1–200 rows. Unknown or duplicate parameters are\nrejected. Client headers cannot override the account, image, resources, session\nname, generation, or deadline.\n\nThe API checks paid access, reads the authenticated account's container list,\nthen forwards only Upgrade and trusted generation/expiration/dimension headers\nto `GET https://internal/terminal`. Cookie and Authorization are not forwarded.\nThe DO rechecks the generation and persisted lease before `exec()` to reject\nstop/recreate races. A stopped, stale, or expired container returns 409; terminal\ncapacity returns 429; service failures are sanitized as 503. No terminal request\ncalls `start()`, writes quota, or resets the hard expiration.\n\nThe private DO runs a PTY attached to the fixed tmux session `main`. The browser\nuses binary UTF-8 stdin, raw binary stdout, JSON `{cols, rows}` resizing and\n`{type:\"ack\"}` output acknowledgements. The DO sends `ready`, `exit` and sanitized\n`error` control messages. Non-hibernating sockets retain the live process handle.\nDisconnect terminates only the attached tmux client. The existing idle/hard\nexpiration alarm still destroys the container and closes terminal sockets.\n\nApply the database migrations, then run `npm run deploy:all` here to check compatibility\nand deploy the container Worker before the API. Deploy `../web` afterward. The image is built in GitHub\nActions; local deployments require GitHub CLI authentication, not Docker.\nExisting old-image containers need to be stopped and recreated. This path does\nnot need SSH tokens. The image deploy script reads `IMAGE_BUILD_SECRET`\nfrom the shell or ignored backend `.env` for the live image manifest.\n`/containers/ssh` accepts an explicit container ID; `007_ssh_container_id.sql`\nbinds tokens to that slot as well as its generation. The SSH gateway protocol is unchanged.\n\n## Local development\n\nStart a Docker-compatible engine (OrbStack, Docker Desktop, or Colima), then run:\n\n```sh\nnpm ci\nnpm run dev\n```\n\n`npm run dev` starts the API and private container Worker together, with local\nD1 and Durable Object storage in `.wrangler/state`. It applies pending local\nmigrations for both `delta` and `mainbrella-preview-routes` before startup. The\ntemporary local configs disable R2 storage billing so source uploads do not\nrequire the production inventory and billing bootstrap. The\nAPI is available at\n`http://localhost:8787`; API docs are at `/docs`. Wrangler builds the Node image\nfrom `./containers/Dockerfile` and starts containers on demand. This requires\nWrangler 4.136.0 or newer. Press `r` in the dev terminal to rebuild the image.\n`npm run dev:lan` exposes the same setup on your local network.\n\nBFF development needs no Cloudflare D1 credentials. The launcher starts a private\nloopback service using Wrangler's local D1 bindings, creating an isolated database\nfor each BFF project under `.wrangler/state/bff/<database-id>/`. Project data\npersists across restarts; `--persist-to <directory>` moves this storage along with\nthe other local state. Provisioning, SQL, migrations, functions, and account-deletion\ncleanup all use these local databases, even if remote BFF credentials are present.\nDeployed Workers continue to use Cloudflare D1 and require the BFF secrets.\n\nEach project directory contains its Wrangler config. To inspect a project database\ndirectly, use its database ID from the local `bff_projects` record:\n\n```sh\nnpx wrangler d1 execute DB --local \\\n  --config .wrangler/state/bff/<database-id>/wrangler.jsonc \\\n  --persist-to .wrangler/state/bff/<database-id> \\\n  --command 'SELECT name FROM sqlite_schema'\n```\n\nLocal projects previously provisioned against remote D1 are not copied into local\nstorage. Create a new local BFF project to use the local database path.\n\nTo use your signed-in Codex CLI for Build inference, run:\n\n```sh\ncodex login\nnpm run dev -- --ai=codex\n```\n\nThe launcher starts a private loopback Node bridge to `codex app-server` over\nstdio, using the model in your Codex configuration. Set `CODEX_PATH` if the CLI\nis outside your PATH. Build turns use low reasoning effort, independently of\nyour CLI preference, to fit the four-minute inference deadline. Codex inference\nruns remotely and uses your Codex account; this mode does not exercise the\nproduction Workers AI model. The app-server\n[dynamic-tool protocol](https://learn.chatgpt.com/docs/app-server) is experimental\nand the bridge enables `experimentalApi`.\n\nMainbrella's existing build agent still validates and executes all six file and\ncommand tools, persists source in local R2 with references in D1, repairs compiler errors, and creates\nDocker-backed previews. Assistant updates use the existing activity stream.\nCodex runs from an empty temporary directory with local execution integrations\ndisabled. The bridge uses an ephemeral bearer token, never exposes a browser\nendpoint, and stops with Wrangler. Errors, timeouts and inference disconnects\ncancel pending Codex work; closing the browser activity stream keeps the durable\nbuild running as usual. Abandoned sessions expire after 12 idle minutes, with a\n30-minute absolute limit. Restarting dev loses active Codex sessions; those\nbuilds fail explicitly and can be retried as new turns.\n\nThis mode changes only the launcher's temporary API configuration and retains\nthe AI binding for image generation. Deployment configurations stay\nunchanged. App-server does not expose a per-response token cap; the bridge checks\nreported usage at each tool handoff/completion and the existing build agent\nenforces its cumulative budgets. Plain `npm run dev` keeps the default Workers\nAI path.\n\nLocal preview links use `http://<token>.localhost:8787/` and route through the\nlocal API Worker to the container, including WebSockets. Open them on the Mac\nrunning Wrangler. Reissue previews created before this setup to get local links.\nIf overriding the API port, pass `--port <port>`; the dev launcher carries that\nport into local preview and project alias routing. The dev launcher uses temporary copies of the Worker configurations with\nproduction routes removed and `LOCAL_DEV=true` on both Workers, preserving local\npreview subdomains. Restart dev after changing a Worker configuration. Restarting\nWrangler stops local workloads; start a new container afterward and republish\nany endpoint that was bound to the previous generation.\n\nThe launcher checks for leftover project proxies at startup, every 15 seconds,\nand after Wrangler exits. During dev, a workload must have been stopped for at\nleast 30 seconds and have no matching Docker app container before its proxy is\nremoved. Cleanup preserves active workloads and unrelated Docker containers.\nShutdown also removes orphan proxies created during that dev session. If the\nlauncher is forcibly killed, the next dev startup cleans recorded stopped work.\n\nOrbStack works through its `orbstack` Docker context. If Wrangler cannot find its\nengine, select the context and set the socket explicitly before starting dev:\n\n```sh\ndocker context use orbstack\nexport DOCKER_HOST=\"$(docker context inspect orbstack --format '{{.Endpoints.docker.Host}}')\"\n```\n\nLocal authentication and trial/subscription records are separate from production.\n`npm run dev` includes only the Node image. Use `npm run dev:all` to enable\nNode, Python, Rust, Go, and DevOps locally from their existing Dockerfiles.\nStop the current dev server before switching; Wrangler builds the images and\nstarts machines on demand. Additional Wrangler options can be passed through,\nfor example `npm run dev:all -- --ip 0.0.0.0`.\n\n## Welcome emails\n\nNew email, Google, and Apple accounts receive a plain-text welcome email from\nAndrew Arrow <andrew@mainbrella.com>. Existing sign-ins, provider linking,\nanonymous accounts, and the app-review account do not send a welcome email.\nThe `WELCOME_EMAIL` Workers binding in `wrangler.jsonc` handles sending without\nan API token and restricts the sender to `andrew@mainbrella.com`. Before deploying,\nonboard `mainbrella.com` under Cloudflare Email Service → Email Sending.\nProduction sends run through `ctx.waitUntil`; failures are logged as\n`welcome_email_failed` and do not prevent signup. Delivery is best effort with\nno automatic retries. Local Wrangler development simulates sending.\n\n## Marketing emails\n\n`POST /api/send-marketing-email` requires the Raindrop admin's current browser\nsession (`oneone@gmail.com`) and a trusted `Origin` header. For example, call\nfrom Raindrop with `credentials: 'include'`:\n\n```ts\nawait fetch(`${API_ORIGIN}/api/send-marketing-email`, {\n  method: 'POST',\n  credentials: 'include',\n  headers: { 'content-type': 'application/json' },\n  body: JSON.stringify({\n    to: 'foo@bar.com',\n    subj: 'New idea',\n    from: 'Bob Smith <bob.smith@mainbrella.com>',\n    message: 'hey everyone just want to say hi',\n    fancy: true,\n  }),\n});\n```\n\nThe `MARKETING_EMAIL` Workers binding sends directly through Cloudflare Email\nSending, without an account-ID environment variable or API token. The sender\ndomain must be onboarded to Email Sending. Bare sender addresses and display\nnames are supported. Edit the HTML header and footer in\n`worker/templates/marketing-email.ts`; the plain-text message is escaped and\ninserted between them, preserving line breaks. A plain-text email part is also\nsent. The optional `fancy` boolean defaults to `true`, preserving the branded\ntemplate. Set `fancy: false` to send the supplied message on a white background\nwith a normal font and a simple unsubscribe link below the message, without a\nvisible subject heading, branded header or footer, buttons, or images.\nSubjects are limited to 998 characters, messages to 100,000 characters,\nand JSON request bodies to 1 MiB.\n\nThe endpoint awaits acceptance and returns HTTP 202 with\n`{ \"ok\": true, \"messageId\": \"...\" }`; acceptance does not confirm inbox delivery.\nInvalid requests return 400, oversized bodies 413, provider rate/quota limits\n429, send failures 502, and an unavailable binding 503. No automatic retries\nare performed. Local Wrangler development simulates sending.\n\nIn both modes, both email parts include `https://mainbrella.com/unsubscribe?email=<encoded address>`.\nThe public page automatically calls `POST /api/unsubscribe` with `{ \"email\": \"...\" }`,\nwithout signing in. It sets matching users' `marketing_email_unsubscribed` flag\nand permanently saves the address opt-out, including recipients without accounts.\nEvery marketing send checks these preferences first. Unsubscribed recipients\nreturn 409 `recipient_unsubscribed`; preference lookup failures return 503\n`email_preferences_unavailable` and do not send. Apply migration\n`033_marketing_email_unsubscribe.sql` before deploying this change.\n\n## AI receptionist\n\nThe receptionist API is enabled by `VOICE_ENABLED=true` and the SQLite-backed\n`RECEPTIONIST_CALL` Durable Object binding (`ReceptionistCall`) in\n`wrangler.jsonc`. Set `VOICE_PUBLIC_ORIGIN` to the exact public HTTPS API origin.\nApply migrations through `045_voice_sip.sql` before using the API:\n\n```sh\nnpm run db:migrate:local\n# Apply to the production D1 database only as an explicit release step:\nnpm run db:migrate:remote\n```\n\nCustomer routes use the `mainbrella_session` cookie. Browser requests include a\ntrusted `Origin`; mutations and WebSocket upgrades require it. Save or read the\naccount's agent with `PUT` or `GET /voice/agent`, inspect availability with\n`GET /voice/config`, and start a browser or text session with\n`POST /voice/calls` and `{ \"channel\": \"browser\" }` or `{ \"channel\": \"text\" }`.\nThe response includes a call snapshot and its exact `websocketUrl`. Connect an\n`@cloudflare/voice/client` `VoiceClient` for agent `ReceptionistCall` using a\ncustom `VoiceTransport` pointed at that URL. Do not append query parameters;\nthe API authenticates the WebSocket with the session cookie and trusted Origin.\nA text connection receives the greeting immediately and uses `sendText()`; it\nmust not send `startCall()`. A browser connection calls `startCall()` after\nconnecting and streams 16 kHz PCM audio. End sessions with\n`POST /voice/calls/{callId}/end`; account call history is available at\n`GET /voice/calls` and is limited to the latest 50 calls.\n\nThe lockfile currently resolves `@cloudflare/voice` 0.5.0 and `agents` 0.28.1.\nThe former is a compatibility package that re-exports the Agents Voice API. It\ndoes not provide a `TwilioAdapter`, so phone media uses this Worker’s explicit\nbridge: Aura `linear16` mono PCM at 16 kHz is converted to Twilio's 8 kHz μ-law,\nand inbound μ-law is expanded to the 16 kHz PCM expected by Flux. The configured\nmodels are `@cf/deepgram/flux` STT, `@cf/meta/llama-3.3-70b-instruct-fp8-fast`\nwith a 256-token response limit, and `@cf/deepgram/aura-1` TTS. Available Aura\nvoices are `asteria`, `luna`, and `athena`.\n\nInbound telephony also supports Telnyx phone numbers and TeXML application SIP\nsubdomains. Follow the [Telnyx setup guide](docs/telnyx-voice.md) to configure a\nnew number, credentials, independent PSTN/SIP rates, deployment and a test call.\nTelnyx availability is returned in `telephony.telnyx`; call history includes\n`phoneProvider` (`twilio` or `telnyx`, null for browser/text). Historical phone\ncalls remain Twilio. Both providers share the existing account limits and wallet.\n\nSigned-in users can browse available US and Canadian voice numbers at\n`GET /voice/numbers/search`. The Voice workspace exposes this under **Search\nnumbers** at `/voice/app/#numbers`. It uses the existing server-side\n`TELNYX_API_KEY` and the `VOICE_NUMBER_SEARCH_LIMIT` binding in `wrangler.jsonc`\n(20 searches per minute per account). Browsing does not require a saved\nreceptionist, calling credits, or `VOICE_ENABLED`.\n\nFor example, `GET /voice/numbers/search?query=FLOWERS&areaCode=800&type=toll-free`\nsearches for numbers ending in `3569377`. Letters map to the phone keypad;\npatterns exclude the country code and, with an area code, search only the seven\nlocal digits. Supported filters are `country` (`US` or `CA`), `query`, `areaCode`,\n`match` (`ends_with`, `contains`, or `starts_with`), `type` (`local` or\n`toll-free`), `locality` (city), `region` (two-letter state/province),\n`includeSimilar` (`true` or `false`), and `limit` (1–50). Defaults are US, local,\nends-with, exact matches only, and 50 results. Queries allow at most ten keypad\ndigits, or seven with an area code; unknown and duplicate parameters are rejected.\n\nResults include the number, location, verified exact-match status, and available\nprovider rental/setup estimates; usage charges are separate. Similar results\nare opt-in and labeled, and missing prices are returned as null. Searches never\nbuy, hold, reserve, or configure numbers. Inventory can change after a search.\nInvalid filters return 400, expired/missing sessions 401, rate limits 429, and\nmissing configuration or provider outages 503. The full schema is in\n`openapi.json`; the upstream filters follow the\n[Telnyx number search API](https://developers.telnyx.com/api-reference/phone-number-search/list-available-phone-numbers).\n\nInbound Twilio telephony supports phone numbers (PSTN) and\n[Twilio Programmable Voice SIP Domains](https://www.twilio.com/docs/voice/api/sending-sip).\nThe PBX or SIP endpoint connects to Twilio, which sends a signed HTTP webhook\nand bridges bidirectional audio over WebSockets to this Worker. Mainbrella does\nnot terminate SIP/RTP directly or provision Twilio numbers or SIP Domains.\n\nFor either transport, set `TWILIO_AUTH_TOKEN` as a Wrangler secret and configure\n`TWILIO_ACCOUNT_SID`, `TWILIO_AGENT_ID` (the saved agent's ID), and\n`VOICE_PUBLIC_ORIGIN`. Then configure one or both destinations:\n\n| Transport | Destination variable | Provider rate variable |\n| --- | --- | --- |\n| PSTN | `TWILIO_PHONE_NUMBER`, an E.164 number | `VOICE_PHONE_MICRO_USD_PER_MINUTE` |\n| SIP | `TWILIO_SIP_URI`, e.g. `sip:receptionist@your-domain.sip.twilio.com` | `VOICE_SIP_MICRO_USD_PER_MINUTE` |\n\nEach rate is the provider's total per-minute cost in micro-USD for that transport,\nincluding Media Streams, before Mainbrella's markup. For example, a combined\n$0.0125/minute provider tariff is `12500`; verify your actual tariff. No default\nor cross-transport rate is assumed. A destination remains unavailable until its\nown rate is configured. SIP-only installations do not need a phone number or\nPSTN rate.\n\n`GET /voice/config` reports `telephony.phoneNumber`, `telephony.sipUri` and a\nper-agent `telephony.webhookUrl`. `configured` is true when at least one priced\ndestination is available; unavailable destinations are null. Configure the\nwebhook URL using HTTP POST on the Twilio number and/or the SIP Domain's\n\"A call comes in\" handler. For SIP, attach a Twilio Credential List or IP Access\nControl List to authenticate your PBX or SIP endpoint, then dial the configured\nSIP URI. Use a Programmable Voice SIP Domain, rather than an Elastic SIP Trunk.\n\nThe called SIP user and host must match `TWILIO_SIP_URI`. Regional hosts such as\n`your-domain.sip.us1.twilio.com` are accepted when configured with that exact host.\nHost matching ignores case; user matching preserves case. The signaling ports\n5060/5061 and an optional `;transport=udp`, `;transport=tcp`, or `;transport=tls`\nsuffix do not affect destination matching. Other URI parameters and headers are\nrejected. Calls to other users or domains are rejected even with a valid Twilio\nsignature. Both destinations map to the single `TWILIO_AGENT_ID`.\n\nThe incoming form webhook and media WebSocket handshake are checked with\nTwilio's SDK signature validation. TwiML passes a media token that expires after\n60 seconds and can be claimed once. PSTN and SIP calls both use `channel: \"phone\"`;\ncall history distinguishes them with `phoneTransport: \"pstn\"` or `\"sip\"` (null\nfor browser/text calls). Historical phone calls are reported as PSTN.\n`transferNumber` must be `null`; transfers, outbound calling, CRM and calendar\nintegrations are not supported.\n\nEach account can have three active calls and 100 calls started in a rolling day.\nCalls end after 15 minutes or 60 caller turns. Each call retains a snapshot of\nthe agent settings used at start. Transcripts are stored in D1 for the account's\nlifetime; raw audio recordings are not stored. Temporary Agent SQLite history and\nbusiness configuration are purged 60 seconds after a call ends, after in-flight\ninference and transcript writes settle; D1 call history remains available. There is no transcript deletion\ncontrol yet, so production operators must account for transcript retention when\nsetting their data-retention policy.\n\nVoice uses the owner's prepaid wallet for all three channels, with the existing\n50% AI markup. Each Llama request reserves an input-byte upper bound plus the\n256-token output limit, then settles reported input/cached/output tokens. Aura\ncharges input characters, including the greeting; Flux reserves five seconds of\n16 kHz PCM at a time and settles the audio bytes forwarded. Rates are taken from\n[Cloudflare Workers AI pricing](https://developers.cloudflare.com/workers-ai/platform/pricing/).\nPhone transport reserves the 15-minute maximum at its PSTN or SIP rate, then\nsettles rounded-up minutes from Twilio’s reported duration and releases the unused\nhold. SIP transport receipts use `twilio-sip-inbound`; PSTN receipts retain\n`twilio-inbound`. Missing provider duration retains the hold until a retry obtains\nit. Number rental is outside this per-call tariff.\n\nCredits and spending limits are checked before allocation, connection and paid\ninference, and every five seconds while active. A failed reservation or balance\ncheck ends all active account calls. Account-wide stops retry durably; phone\nshutdown also explicitly completes the Twilio call through its API, with retries.\nThe media stream's following `<Hangup />` remains a second shutdown path.\n\n`voice_usage` retains per-operation financial receipts without cascading call\nor user foreign keys. Each settled receipt writes an `inference` event to the\nexisting append-only `accounting_ledger`, including the user, call, agent,\nchannel, model, provider cost, markup and charge. Wallet settlement and ledger\noutbox writes use the same receipt ID, so retries cannot double-charge. Scheduled\nwork reconciles reported receipts. Unknown provider outcomes keep their hold for\nreconciliation and are never replayed. Transcript and call-history cleanup does\nnot delete financial receipts.\n\nThe automated Voice tests use deterministic AI fixtures and Miniflare to exercise\nAPI behavior, the Agent protocol, and transcript persistence. They do not qualify\nlive Workers AI inference or a deployed Twilio number and webhook. Run the\nrelevant checks with:\n\n```sh\nnpm run type-check\nnpm run test:openapi\nnpm run docs:generate\nnpm run test:api\n```\n\n## Verification\n\n```sh\nnpm ci\nnpm run type-check\nnpm run check:containers\nnpm run test:plans  # Paid billing, account quotas, lifecycle, SSH and terminals\nnpm test           # Full suite, including legacy fixtures noted below\n```\n\nRetained admin/Apple-auth tests use an explicit test-only compatibility schema\nin `worker/app/fixtures/legacy-compatibility.sql`. It is not a production migration\nand does not enable native/community features on a fresh Mainbrella database.\nCI runs the full runtime, API and SDK suites, OpenAPI checks, type checking,\nJavaScript syntax checks and schema generation. Linux CI exercises file writes\nwith GNU coreutils; those write tests skip explicitly when it is unavailable locally.\n\n## Production deployment\n\nAll production backend code lives here. `containers/` contains the private\ncontainer Worker, lifecycle controller, terminal bridge, Dockerfile, and tests.\n`wrangler.containers.jsonc` deploys `mainbrella-containers`; `wrangler.jsonc`\ndeploys `mainbrella-api`. The API retains the original cross-Worker `USER_CONTAINER` binding and adds a\nSQLite `CONTAINER_ACCOUNT` binding/export for atomic per-account reservations. `../reference` is\nbenchmark and historical material only; production builds do not read it.\n\n```sh\nnpm run deploy:preflight   # Read deployment compatibility metadata; no starts\nnpm run deploy:all         # Preflight, containers, then API; stops on failure\nnpm run deploy:containers  # Container Worker with the CI-published image\nnpm run deploy:api         # Account D1 migrations, then API only\nnpm run deploy:bootstrap-activity # One-time recovery for missing AccountActivity export (10061)\n```\n\nApply migrations before deploying the API, including 006 (billing webhook\nreceipts and billing operation leases) and 007 (SSH container IDs). Configure the existing\n`STRIPE_SECRET_KEY` and `STRIPE_PUBLISHABLE_KEY`, and add `STRIPE_WEBHOOK_SECRET`.\nRegister `https://api.mainbrella.com/subscription/webhook` in Stripe for checkout,\nsubscription, schedule, invoice and payment/refund changes as described below.\nKeep webhook signing secrets in Wrangler secrets; never commit them.\n\nFor the current release, deploy containers before API, then web. The preflight\nrequires an entitlement-aware, generation-specific predecessor API, the complete\nmigration ledger (including `011_operational_status.sql`), and configured\n`IMAGE_BUILD_SECRET` and separate `MONITORING_SECRET`. It checks the authenticated\nimage lease endpoint without acquiring a lock. The deployment script continues\nto validate the shared image map and acquire its lease before publication.\n\nThe original paid-entitlement rollout required API first because its predecessor\ndid not send entitlement headers. That historical sequence is not safe for the\ncurrent release. If preflight rejects an older API, stop and use the staged\nbootstrap guidance in [deployment.md](docs/deployment.md). That runbook also\ncovers release evidence, partial failures and rollback.\n\nThe container image includes Node 24, bash, and tmux, with outbound internet enabled.\nDeploying an image does not replace running containers; stop and recreate old\ncontainers to use the updated image.\n\nStartup sets the Linux hostname to `mainbrella`, including after workspace\nrestore, so Python's `http.server` can bind to `0.0.0.0` without failing on\nCloudflare's 64-character default hostname. The rename is best effort for custom\nimages and local Docker environments that do not permit hostname changes.\n\nDeveloper SSH keys are configured in `wrangler.containers.jsonc`. Connect with\n`npx wrangler containers ssh <INSTANCE_ID> --config wrangler.containers.jsonc`.\nPrivate keys stay local. Status polling and SSH attachment do not renew the idle\nlease; terminal input/output does.\n\n## Terminal image publishing\n\nThe **Build terminal image** GitHub Actions workflow builds a Linux amd64 image,\nchecks Node/bash/tmux, and pushes it to the Cloudflare managed registry. It runs\non main when the Dockerfile, workflow, or dependency lockfile changes, and can\nalso be run manually. CI publishes `terminal-image.json` on the `terminal-image`\nGitHub release, containing the immutable registry digest and Dockerfile hash.\nIt does not deploy production Workers.\n\nOne-time setup:\n\n1. Create a Cloudflare custom API token with **Account → Containers → Edit**,\n   limited to account `2b7a9be82bb64187230703b024e25157`. No zone permissions are\n   required for this image-only workflow. Leave IP filtering unset for GitHub\n   runners. Add it as the repository Actions secret `CLOUDFLARE_API_TOKEN`.\n2. Push the workflow and Dockerfile to main, then run **Build terminal image**\n   under the repository Actions tab if it has not already run.\n3. Install GitHub CLI and authenticate with `gh auth login` for this private\n   repository. Keep your existing Wrangler login for production deployment.\n4. Run `npm run deploy:all` locally after the image workflow succeeds.\n\nThe deploy script downloads the terminal release manifest and live custom-image\nmanifest, verifies the registry account,\nrepository, digest, and Dockerfile hash, and supplies Wrangler a temporary config\nwith digest-pinned entries for `terminal` and all ready/publishing custom images.\nSet `IMAGE_BUILD_SECRET` in the shell or ignored backend `.env` (the same value\nas the API Worker and GitHub Actions); a missing secret or\nunavailable custom manifest stops deployment rather than removing user images. The tracked config remains\nthe build blueprint. Use the npm deploy commands rather than invoking\n`wrangler deploy --config wrangler.containers.jsonc` directly, which would still\nattempt a local Docker build. `npm run deploy:containers -- --dry-run` exercises\nthe same image selection without publishing the Worker.\n\nIf the local Dockerfile differs from the published one, deployment stops before\nuploading. Push that Dockerfile to main and wait for CI to publish its image.\nRegistry references and hashes are public metadata; API tokens stay in Actions\nsecrets and are never put in the deployment manifest. The GitHub release and\nrepository must remain accessible to the authenticated deployment user.\n\n## Paid plans and enforcement\n\n`containers/plan-policy.js` is the authoritative policy imported by both workers.\n`GET /subscription/config` publishes these same definitions to the web UI.\n\n| Plan | Monthly USD fee | Concurrent containers | Starts per UTC month | Hard session limit | Idle timeout |\n| --- | ---: | ---: | ---: | --- | --- |\n| No paid plan | $0 | 0 | 0 | No access | No access |\n| Usage | $5 minimum | 100 | 10,000 | 24 hours | 30 minutes |\n| Builder (legacy) | $5 | 5 | 1,000 | 1 hour | 10 minutes |\n| Pro (legacy) | $180 | 100 | 10,000 | 24 hours | 30 minutes |\n| Scale (legacy) | $999 | 500 | 100,000 | 72 hours | 60 minutes |\n\nEvery plan supports Lite, Small, Medium, Large, and XL machines, up to 4 vCPU / 12 GiB RAM / 20 GB disk. Legacy Builder includes 250 compute-unit hours/month and 28 concurrent units; Pro 9,000 and 128; Scale 50,000 and 640. Units/hour are 1, 6, 10, 16, and 28. Both the unit ceiling and container ceiling apply. Browser terminal, SSH and outbound internet are included. Each container permits\nup to four attached terminals (browser and SSH combined); each account permits\nten live SSH access tokens, each expiring within 15 minutes or the machine deadline. Snapshots,\nfilesystem persistence after stop, custom sizes, team seats, SDKs, enhanced logs,\naudit exports and priority capacity are unavailable on all plans. Legacy plans have no compute overages. New subscriptions use the $5 minimum and durable usage ledger described in [docs/usage-billing.md](docs/usage-billing.md).\n\nA logged-in account has no container allowance until its recognized subscription\n(a quantity-one base fee plus explicitly allowed metered items) is active and its current plan period has a successful\nStripe payment. Trials, incomplete, past-due, unpaid, canceled and paused\nsubscriptions do not grant access. Local `pro_billing.plan` is a synchronized\nrecord, not authorization. Stripe lookup failures fail closed with 503; the API\nnever substitutes Builder. Unpaid starts return 402 `subscription_required`\nbefore reserving usage or provisioning a machine. An unpaid attempt from an\nexisting billing account also schedules revocation of its existing machines,\nwithout delaying the 402 response; failed cleanup retains slots for alarm retries.\n\nReservations are shared across all slots, browser sessions and Bearer sessions.\nThe account saves a reservation before boot; readiness runs outside the reservation\nlock so multiple machines can start together. Failed or ambiguous starts remain\ncharged. Stopping, upgrading, downgrading, canceling and resubscribing do not reset\nusage. Legacy compute allowances reset at UTC month rollover; usage billing and\nspending caps follow the subscription billing period. Start limits reset on UTC\nmonths for all plans. Legacy usage migrates from the original slot without\ngranting a fresh quota.\n\nEvery reservation has a persistent sequence number. Slot cleanup fences canceled\nreservations, so delayed boots and responses cannot resurrect a stopped machine\nor alter its replacement. Reconciliation persists its entitlement decision before\nfanout and continues stopping reachable machines if another slot is unavailable;\nfailed cleanup retains the slot and retries after 30 seconds.\n\nEach machine has a fixed hard deadline capped by its paid period. An upgrade\nchanges limits for new sessions and never extends existing hard deadlines.\nRenewal also leaves the original hard deadline in place; start a new container\nafter it expires.\nDowngrades and cancellation take effect at the end of the current paid period.\nWhen a lower tier becomes effective, the oldest unexpired containers within its cap remain; excess\ncontainers stop and remaining session/idle deadlines are clamped. Loss of paid\naccess stops all containers. Polling and token issuance do not renew idle time;\nterminal input/output can renew idle time within the fixed hard deadline. Machine\nand account alarms enforce deadlines without an open dashboard.\n\n## Subscription changes\n\nPurchases require login and belong to the account's Stripe customer. Checkout\nsession ownership is verified on completion. A second purchase is rejected while\nany live subscription exists; existing subscribers use plan-change endpoints.\n\n- Upgrades use a Stripe-hosted confirmation showing the immediate prorated charge.\n  The higher tier requires a successfully paid invoice for that tier.\n- Downgrades use an explicitly confirmed Stripe subscription schedule at renewal,\n  preserving the current paid tier until then. Selecting the current plan removes\n  a pending downgrade. Custom schedules work across the separate plan products.\n- Cancellation is explicitly confirmed, removes a pending downgrade, and sets\n  `cancel_at_period_end`. Resume before expiration clears that cancellation;\n  after expiration, a new checkout is required.\n- The billing portal provides payment-method updates and invoice history. Plan\n  changes and cancellation use the dedicated app flows to preserve their rules.\n\nSee [Stripe's update confirmation flow](https://docs.stripe.com/customer-management/portal-deep-links)\nand [subscription schedules](https://docs.stripe.com/billing/subscriptions/subscription-schedules)\nfor the underlying billing mechanisms.\n\nThe signed webhook verifies the raw body, timestamp and HMAC before reading an\naccount, resolves live Stripe state rather than trusting event order, and saves\nan event receipt only after entitlement reconciliation succeeds. Failed revocation\nremains retryable. Configure subscription, schedule, invoice, checkout and payment\nrefund/dispute events: `customer.subscription.*`, `subscription_schedule.*`,\n`invoice.*`, `checkout.session.*`, `charge.refunded`, and `charge.dispute.created`.\nStatus, startup and terminal/SSH connections also resolve\nlive billing state, so a saved plan name cannot bypass payment checks.\n\n## API automation\n\nSee [API.md](API.md) for authentication, endpoints, curl examples, limits and retry\nbehavior, and [SKILL.md](SKILL.md) for the reusable automation skill. Both files\nare also published by `../web`. Backend changes update the copies here; coordinate\npublication through [the backend handoff](docs/backend-handoff.md) when the web\nrepository is being edited independently.\n\nLifecycle, execution, images, and SSH issuance accept `Authorization: Bearer mb_<key-value>`\nwithout Origin. Create named keys at `https://mainbrella.com/api-keys/`.\nLogin-session Bearer credentials remain supported for compatibility.\nCookie mutations still require a trusted Origin. Browser terminals remain\ncookie-only. Paid entitlements and quota are identical for UI and automation.\nCleanup remains available during a Stripe outage and cannot start a machine.\n\nTo install the skill in Codex, copy `SKILL.md` and `API.md` into\n`~/.codex/skills/mainbrella-containers/` (or the equivalent skills directory for\nyour agent), then invoke `$mainbrella-containers`. Provision `MAINBRELLA_API_KEY`\nseparately using the instructions in `API.md`.\n\n## Repository lookup authentication\n\nRepository launches and `/repo-launches/resolve` use the optional API Worker\nsecret `REPO_RUN_GITHUB_TOKEN` to authenticate GitHub metadata, commit, and tree\nlookups. Without it, requests share GitHub's unauthenticated limit of 60 per hour\nper IP. A personal access token generally allows 5,000 requests per hour, shared\nwith other requests by that GitHub user; secondary limits still apply.\n\nCreate a dedicated personal access token for public repository reads (a classic\nPAT with no scopes is sufficient). Set it from the backend directory:\n\n```sh\nnpx wrangler secret put REPO_RUN_GITHUB_TOKEN --config wrangler.jsonc\nnpm run deploy:api\n```\n\nFor local development, set the same binding in `.dev.vars`. Keep the token in\nsecrets, separate from `IMAGE_BUILD_GITHUB_TOKEN`. It is sent only to GitHub's API,\nredacted from launch error logs, and never passed into containers, launch records,\nor browser responses. Private repositories remain rejected even if the token can\naccess them. GitHub redirects remain rejected. Invalid or expired tokens return\n`github_unavailable`; rotate the secret rather than retrying unauthenticated.\nSee [GitHub's rate limits](https://docs.github.com/en/rest/using-the-rest-api/rate-limits-for-the-rest-api).\n\n## Custom image build and deployment\n\nThe **Build custom image** workflow and reviewed backend scripts must be on\n`main` in an operator-controlled private build repository before enabling builds.\nSet `IMAGE_BUILD_GITHUB_REPOSITORY=owner/repo` in the API configuration. Public\nrepositories are refused because Actions artifacts contain source and image layers.\nSet `IMAGE_BUILD_SECRET` (at least 32 characters) in both the API Worker and\nGitHub Actions, and set `IMAGE_BUILD_GITHUB_TOKEN` in the API Worker to a token\nwith metadata read and Actions write access to that private repository. `CLOUDFLARE_API_TOKEN` in Actions\nmust permit both registry access and deployment of the container Worker.\nSee [custom image rollout](docs/custom-images.md) for cache/private-base setup,\nrequired migrations, one-use registry credentials and bounded live qualification.\n\nThe original live-manifest rollout required an API-first bootstrap. For the\ncurrent release, apply all remote D1 migrations, configure the custom-image\nworkflow and its secrets, then use `npm run deploy:all` with `IMAGE_BUILD_SECRET`\nin the shell or backend `.env`. If the deployed API predates the manifest/lease\ncontract, follow [the staged bootstrap runbook](docs/deployment.md) before\naccepting builds; do not deploy the current API ahead of its runtime.\nNode loads this secret from `.env` automatically for container deployments; an\nexplicit shell/Actions value takes precedence.\n\nPreparation downloads the recipe and pinned base image on a trusted runner.\nThe user Dockerfile builds and runs its compatibility check on a separate,\ndisposable runner with no registry, deployment, or API callback secrets.\nPublication loads the resulting archive without executing it, pushes it, reports\n`publishing` with its immutable digest, deploys the assembled image map, then\nreports `ready`. The terminal release and live API manifest feed the same deploy\nscript; the old `custom-images.json` release snapshot is no longer consumed.\n\nAll production image-map deployments acquire an API lease. The lease lasts 12\nminutes; the Wrangler subprocess is bounded to 10 minutes, and deployments wait\nup to 8 minutes for a competing lease. This serializes local deployments, builds,\nand deletion reconciliation. Deleted images disappear from the live manifest\nand trigger the workflow's `reconcile` operation. An hourly reconciliation retries\nfailed removals; deleting an image does not stop an already-running container.\nIf Actions is unavailable, attached image slots can remain until reconciliation\nsucceeds. Dry runs validate the live manifest without acquiring a deployment lease.\n\nExplicit GitHub dispatch rejections (400/401/403/404/422) refund the monthly build\nreservation atomically. Ambiguous network/server failures keep the reservation.\n`npm run test:images` and CI exercise the SQL state machine through generated image selection and the\ncontainer controller, deployment failure handling, quota refunds, and workflow\nsyntax. It uses simulated GitHub/Cloudflare boundaries; a production build and\nlaunch still need a smoke test after rollout. The broader `npm test` currently\nalso includes retained legacy admin/Apple tests with isolated compatibility fixtures.\n\n## OpenAPI documentation\n\nThe API uses the same Chanfana + Hono + Zod setup as Cubacadabra. Run `npm run dev`\nand open `http://localhost:8787/docs` for Swagger UI or `/redocs` for ReDoc.\nThe OpenAPI 3.1 document is served at `/openapi.json` and generated directly from\n`worker/app/openapi-*.ts`. Existing handlers retain runtime validation, CORS,\nauthentication, background work, and WebSocket responses.\n\nExport a standalone document without starting Wrangler or configuring secrets:\n\n```sh\nnpm run docs:generate\n# Optional output path (parent directory must exist):\nnpm run docs:generate -- /tmp/mainbrella-openapi.json\n```\n\nThe default generated `openapi.json` is ignored by Git. To document a new endpoint,\nadd its schema alongside related routes in `worker/app/openapi-*.ts`, using\n`register(api, method, path, schema, handler)`. Use Hono path parameters such as\n`/images/:id` and describe them with `request.params`; the document uses `{id}`.\nInclude a unique `operationId`, summary, tag, request schema, response schemas,\nand the correct security scheme. The shared helpers in `openapi-shared.ts` cover\nJSON bodies/responses and common errors. For a new feature module, register it in\n`openapi.ts`. Add the method/path to `openapi.test.ts` and run:\n\n```sh\nnpm run test:openapi\nnpm run type-check\nnpm run docs:generate\n```\n\nBilling mutations use browser cookies and a trusted Origin; container/image\nautomation uses the session Bearer credential. Native app and internal service\ntokens have separate security schemes. WebSocket routes describe the HTTP upgrade;\nSwagger UI does not open terminal WebSocket sessions.\n\n### API keys\n\nSigned-in web users can create and revoke automation credentials at `/api-keys/`.\nBefore deploying API key support, apply D1 migration `010_api_keys.sql` using\n`npm run db:migrate:remote`, then deploy the API and web builds. Only token hashes\nare stored; the secret is returned once. Keys authorize the existing container,\nimage, and SSH issuance routes, with the same account entitlements and quotas.\n\n## HTTP execution\n\n`POST /containers/exec?id=<id>&createdAt=<ISO generation>` runs a foreground shell\ncommand using API keys or browser authentication. It returns separate stdout,\nstderr, exitCode, timedOut and outputTruncated. See API.md for limits and retry\nsemantics. Generation and paid lease checks run again inside the private DO;\nexecution never starts a machine or reserves quota. The runtime caps commands at\nfour concurrent requests, 60 seconds and 1 MiB combined output. Stop and deadline\nrevocation cancel active work. No image rebuild or database migration is needed.\n\nDeploy the container Worker before the API for this additive endpoint, then deploy\nweb so the published docs and verification tool match. An API deployed before its\nprivate runtime will return execution_unavailable. Run the web `verify:agent`\ncommand with the provisioned API key after rollout; it consumes one start and\ncleans up only its own generation. Until rollout, local process tests and API\nfixtures verify the contract but do not satisfy the production release gate.\n\n## HTTP files\n\n`GET` and `PUT /containers/files?id=<id>&createdAt=<ISO generation>&path=<absolute-path>`\ntransfer raw binary files up to 1 MiB. Writes use atomic replacement and an existing\nparent directory. Reads return `application/octet-stream`; see API.md for path,\npermission and retry semantics. File operations recheck the generation and paid\nlease before process launch, renew idle activity, and share the four-command pool.\nThe runtime bounds each operation by 30 seconds and the hard deadline.\n\nNo database migration or image rebuild is needed for catalog/base images, which\nalready include `/bin/sh` and GNU coreutils. Deploy the container Worker, then API,\nthen web. The doctor checks both file routes, and verification checks binary transfer, command execution and generation cleanup.\n\n`node --test containers/files.test.mjs` runs real local filesystem/process checks.\nWrite tests require GNU coreutils on PATH, as provided by the Linux catalog images;\non macOS, prepend an available coreutils `libexec/gnubin` directory to PATH. Without\nit those tests explicitly skip. `npx tsx --test worker/app/files.test.ts` verifies\npublic authentication, account ownership, error handling and binary forwarding.\n\nDirectory metadata/listing and mkdir/remove/move/chmod use the separate `/containers/files/*` routes. They share authorization, the generation-bound operation pool and bounded execution. Images must include GNU stat/find/sed/coreutils. Filesystem watchers remain unsupported. See API.md for symlink, pagination and partial-mutation semantics.\n\n## Local project hosting\n\nStart the backend with `npm run dev` to enable project endpoints in the Projects UI.\nThe default project URL uses `p-<project UUID without hyphens>.localhost`; a custom\nalias such as `app.localhost` reaches the same published application. In the UI,\nadd the alias, click **Verify DNS**, then click **Activate locally** after it reaches\nthe pending TLS state. Local mode simulates DNS ownership, routing, and TLS; it\ndoes not require public DNS records or issue certificates. The application still\nuses the selected port inside its container and must listen on `0.0.0.0`.\n\nIf the backend API uses a different port, pass it to Wrangler and point the web\napp at that API, for example:\n\n```sh\nnpm run dev -- --port 8899\nVITE_API_URL=http://localhost:8899 npm run dev\n```\n\nLocal projects use the existing account and paid/trial access checks. Docker must\nbe running for container-backed applications. See\n[docs/project-domains.md](docs/project-domains.md) for the local and deployed\ndomain flows.\n\n## Agent API and local SDKs\n\n`GET /capabilities` publishes deployment features and shared runtime limits without\nauthentication or provisioning. Account allowances and deployed catalog IDs remain\nin authenticated `GET /containers`.\n\nManaged execution uses `POST /containers/executions` with a generation and required\n`Idempotency-Key`, followed by GET for retained results, DELETE for cancellation,\nand GET `/containers/executions/:executionId/events` for SSE output and cursor replay.\nIt permits up to 15 minutes within the hard lease, shares the four-operation pool,\nand retains up to 32 execution records per container for one hour. Disconnect only\ndetaches. A runtime restart interrupts unfinished jobs and stops their matching\ngeneration; it never replays a command. See [API.md](API.md) for the full contract.\n\nThe dependency-free packages in [sdk/javascript](sdk/javascript/README.md) and\n[sdk/python](sdk/python/README.md) install locally. They are not published to npm\nor PyPI. Both support creation, generation-specific cleanup, binary files,\nforeground commands, filesystem metadata/mutations, managed argv/cwd/env, stdin, signals, PTY resize, retained-job listing and streamed reconnect. Commands and environment values are not retained. Guest-wide OS process listing is not implemented.\n\n`npm run sdk:qualify` creates versioned archives, clean-installs them and writes hashes and qualification evidence. `npm run sdk:qualify:deployed` installs those exact npm/wheel candidates and verifies their deployed workflows with an explicit two-start budget and recovery checkpoints; follow [the release runbook](docs/sdk-release.md) for its required arguments and account scope. Registry publication and paid deployed qualification remain separate gates. `npm run docs:package` creates the installable skill/reference bundle. The backend API.md/SKILL.md and SDK READMEs are authoritative; run web `npm run docs:sync` after changing them. Both repositories check public contract agreement in CI.\n\nProtected application previews now have a private runtime, isolated gateway and\nauthenticated `/containers/previews` API, with hash-only routing in a separate\ndatabase. They are enabled on the qualified isolated `mainbrella.dev` gateway.\n`npm run check:previews` bundles the gateway without\ndeployment. The default deploy command publishes the runtime, gateway, and API\nafter checking both database migration sets. Apply routing migrations with\n`npm run db:migrate:previews:remote`. See\n[the ingress handoff](docs/preview-ingress.md) for configuration, sharing semantics,\nfailure reconciliation and recorded live evidence. SDK helpers and capability-gated dashboard controls exist locally; verify the deployed dashboard controls separately.\n`npm run verify:previews` supplies a one-start transport probe with recovery\ncheckpoints; follow the handoff's required arguments and explicit budget. Its\nsuccess does not satisfy the remaining framework, isolation and deployment gates.\n`npm run previews:preflight` checks the staged isolation/routing configuration and\nremote routing schema with no writes or starts; `-- --local` checks configuration\nonly. Normal deployment runs it alongside the account deployment preflight;\nenabled project hosting also requires the project routing schema.\n\n`npm run verify:agent` exercises create, execution, files, streaming and cleanup,\nconsuming one start. `npm run benchmark:api -- --samples=5 --concurrency=1` runs\nbounded foreground samples and saves raw results with their methodology. Both\nrequire a provisioned `MAINBRELLA_API_KEY` and pass account/capability checks before\nlaunching. They preserve all pre-existing containers.\n\n## Workload observability\n\nGeneration-bound `/containers/events` reads a bounded seven-day lifecycle journal. Runtime monitor/status observations record natural stops without guest exec or idle renewal. `/containers/metrics` uses opaque provider labels; `/containers/webhook` configures signed callback delivery with encrypted signing keys, durable retries, deduplication identities and bounded retention. Metrics and webhooks are disabled until explicitly configured and qualified. Arbitrary customer webhook destinations remain unsupported; only trusted operator-controlled HTTPS relays are allowed. SDKs expose these operations and signature verification. See [the observability runbook](docs/workload-observability.md).\n\n## Operational status\n\nApply `011_operational_status.sql` before deploying the updated API. The five-minute\nscheduled handler records website/API reachability and authentication-database\navailability. These scopes do not establish successful login, provisioning, SSH,\nimage-build or billing workflows. `/status` reports unobserved or stale components\nas unknown; `/status/history` exposes 31 days of observations with cursor pagination.\n\nConfigure a separate `MONITORING_SECRET` to authorize internal observation and\nincident submissions. API keys cannot report public status. Operator-authored\nincident text is public. `npm run status:canary` uses a dedicated provisioned\naccount and this secret to report a one-start create/execute/files/stream/cleanup\nprobe. It has no recurring schedule, preserving control over its start budget.\n\nSee [the backend handoff](docs/backend-handoff.md), [security evidence](docs/security-evidence.md)\nand [runtime feasibility](docs/runtime-feasibility.md) for web integration and release gates.\n\n\n### Size pricing and cost assumptions\n\nPolicy lives in `containers/plan-policy.js`. All plans offer the same five sizes; omitted size defaults to Lite. Runtime is reserved account-wide before boot, with unused runtime released on confirmed stop. Delayed or unreadable machines retain reservations. Budget deadlines survive restarts and terminal activity. Legacy sessions are capped at the UTC month boundary; monthly allowance does not roll over. Starts limits are now 1,000 / 10,000 / 100,000 per UTC month, as lifecycle safeguards. Legacy plans have no automatic overages. Usage accounts explicitly authorize a higher spending cap before overages; billing follows their subscription period.\n\nAt full allowance utilization, Builder and Pro cost $0.02 per compute-unit hour; Scale costs $0.01998. Small / Medium / Large / XL effective hourly prices on Pro are $0.12 / $0.20 / $0.32 / $0.56. These are about 33–42% above equivalent CPU/RAM at E2B/Daytona's published $0.0504/vCPU-hour and $0.0162/GiB-hour rates, before their storage charges and E2B plan fees. Partially used subscriptions have higher effective hourly prices. Resources, CPU scheduling and platform features differ; this is a resource-rate comparison, not a workload-performance benchmark.\n\nCloudflare's published marginal rates (checked 2026-10-05) are $0.072 per active vCPU-hour, $0.009 per provisioned GiB-hour, and $0.000252 per provisioned disk GB-hour. Full-CPU compute costs/hour for our sizes are $0.007254 / $0.074016 / $0.129024 / $0.220032 / $0.401040. XL has the greatest cost per unit ($0.0143229). Worst-case container compute at full allowance is therefore $3.58 / $128.91 / $716.14, leaving roughly 28% before other costs. Cloudflare's shared included usage is excluded rather than assigned to each customer. This is not a full gross-margin guarantee: egress, Workers/DO requests and duration, logging, image builds/storage, payment fees, taxes, support and operational capacity must be monitored separately. CPU-light workloads cost less. The automated pricing check verifies the compute envelope, not total profitability.\n\nSources: [Cloudflare pricing](https://developers.cloudflare.com/containers/platform/pricing/), [E2B pricing](https://e2b.dev/pricing), [Daytona pricing](https://www.daytona.io/pricing).\n\nDeploy the private containers Worker before the API Worker, then publish the web build. This ensures the runtime enforces compute deadlines before the API advertises larger sizes and allowances. The `deploy` script uses that order.\n\n## Private Services between machines\n\nThe October 7 commits add an account-owned HTTP service registry, native\n`*.internal` routing, exact-generation membership, and dashboard management.\nCreate a network, register a backend port as `api`, and attach a frontend as a\ncaller; `http://api.internal/users` connects them without exposing the backend\nthrough a public preview or putting a Mainbrella API key in the guest. Membership\nand machine lifecycles are independent. This is a local prototype; require\n`networking.privateServices: true` in `/capabilities` before using it.\n\nSee [the API contract](API.md#private-services-between-machines) for requests,\ncleanup and limits. It supports bounded plain HTTP, not arbitrary TCP, HTTPS,\nWebSockets, private IPs or direct database connections.\n\nThe saved October 7 local reports confirm private HTTP between two machines and\na backend with no preview. Both full runs failed later at frontend preview\nchecks, with cleanup recorded; they do not establish complete qualification.\n`scripts/verify-private-services.mjs` is the bounded local verifier.\n\nThe separate [local users demo](examples/private-services/README.md) subsequently\npassed private Go/SQLite access, frontend preview HTML/API, and browser rendering\nof three rows at 390×844, 768×1024, 1280×800, and 1440×900 without horizontal\noverflow. Its launcher leaves the two Lite app generations running and provides\nexplicit cleanup. This successful deployment does not replace the broader\nisolation verifier or establish production qualification.\n\nFor competitor copy, compare against Daytona's [linked sandboxes](https://www.daytona.io/docs/en/sandboxes/#linked-sandboxes),\nreviewed October 7, 2026: same-runner parent/child networking, DNS aliases,\nephemeral children and cascading parent deletion. Mainbrella's implemented\nadvantage for HTTP services is explicit membership among independently managed\nmachines, including attachment after creation. Daytona also documents direct\nport connections; our HTTP prototype does not establish transport parity,\nperformance superiority, or production availability.\n"}