Deploys & limits

Every deploy runs as its own isolated app on *.cloudbed.app, with its own SQLite database and live-query hub. There are two modes.

Anonymous deploys

cloudbed deploy with no account gives you a live URL immediately. Anonymous deploys are sandboxed:

The CLI stores the deploy's one-time claim token in .cloudbed/deploy.json (gitignored — treat that file as a secret; it's also shown once as a claim: link at deploy time).

Claiming

cloudbed login          # device-flow auth in your browser
cloudbed claim          # in the app directory

Claiming attaches the deploy to your account: it becomes permanent, outbound fetch is enabled, and .env.cloudbed.server syncs on each deploy. The claim token is invalidated, and a cloudbed.json binding is written that you can commit — anyone with owner access who deploys from that directory updates the same app.

If you log in before the first deploy, you skip the anonymous stage entirely and get an owned deploy from the start.

Your claimed apps are listed at dashboard.cloudbed.dev.

Server env

Claimed deploys read .env.cloudbed.server (KEY=VALUE lines) and expose it as ctx.env — server-side only, never shipped to the client. Up to 64 keys, 16 KB per value, 64 KB total.

Password protection

Gate a capsule behind a shared password at deploy time:

cloudbed deploy --password "hunter2"    # set or replace the password
cloudbed deploy                         # redeploys keep the current setting
cloudbed deploy --no-password           # remove the gate

While a password is set, every public route — the app shell, /client.js, styles, stored files, HTTP endpoints, and WebSocket connections — answers 401 until the visitor unlocks. Browsers get a small unlock page; entering the password sets a signed, HttpOnly, week-long cookie and redirects back. Everything else gets a machine-readable body with code password_required.

Agents have two non-browser paths in:

Details worth knowing:

Server-side rendering

Capsules can opt into SSR with capsule({ ssr: true }). Deploys then include an extra SSR bundle built from the app's client entry. Shell GETs (/ and other HTML routes) return pre-rendered HTML plus an embedded query snapshot, so public pages are readable with curl and hydrate without a blank first paint.

SSR renders as a guest because shell GETs do not carry browser credentials. In local dev, ?guest=<name> selects the render guest, using the same sanitization as WebSocket guest auth. Query pre-runs and render code must be read-only; any database or file write fails SSR for that request. Failures, missing SSR bundles, timeouts, and quota blocks fall back to the normal CSR shell with HTTP 200 and a log entry you can inspect.

Releases and rollback

Every deploy of a capsule is recorded as a numbered release (bundles are content-addressed and deduplicated, so history is cheap). cloudbed releases lists them; cloudbed rollback [--to <seq>] republishes an earlier release as a new one — append-only, so a rollback can itself be rolled back. Server env is not part of a release. Ship broken code with confidence: the way back is one command.

The API surface is GET /v1/deploys/:id/releases and POST /v1/deploys/:id/rollback (body {"to": <seq>} optional), authenticated like redeploys.

Scheduled jobs

Capsules define scheduled jobs with cron({ schedule }, fn) in the server SDK. Deploy responses include the static cron list, and cloudbed cron ls adds live status when the CLI has owner auth:

cloudbed cron ls
cloudbed cron run digest

Scheduling is backed by Durable Object alarms inside the capsule. Cloudbed only arms alarms for claimed, unsuspended deploys; anonymous deploys can define crons, but scheduled firing starts after cloudbed claim. Local development is different on purpose: cloudbed dev runs cron timers locally while the dev server is active.

If a capsule wakes up after missing one or more scheduled times, Cloudbed fires one catch-up run for the due job and then schedules the next future occurrence. There are no automatic retries. Successes and failures are recorded in the capsule run history; the most recent 50 runs are retained and surfaced through the status payload and CLI.

Suspension pauses scheduled firing and blocks manual triggers. Unsuspending the deploy re-arms the capsule alarm.

The manual trigger API is authenticated like other deploy control-plane calls:

POST /v1/deploys/:id/crons/:name/run
Authorization: Bearer <token>

It runs the named job immediately and returns {"ok": true, "result": ...} on success. Unknown cron names return not_found, suspended deploys return suspended, and handler failures return handler_error while still being recorded in run history.

Web Push

Capsules send notifications from server code with ctx.push.send(payload, opts?) and subscribe browsers with the client SDK's usePush() / enablePush() helpers.

Hosted delivery is gated the same way as other privileged outbound work: claimed, unsuspended deploys can send; anonymous unclaimed deploys can collect subscriptions but delivery stays disabled until claim. Local cloudbed dev enables sends so you can test against real browser subscriptions.

Each deploy stores up to 1,000 Web Push subscriptions. Delivery to a push service that returns HTTP 404 or 410 prunes that expired subscription automatically; other delivery failures are counted but retained.

cloudbed push ls
cloudbed push send --title "Hello" --body "From Cloudbed" --url "/"

cloudbed push ls reports subscription count, whether VAPID keys are configured, and whether sends are enabled. cloudbed push send broadcasts a test notification by default, or targets subscriptions registered by one auth subject with --user <sub>.

Deleting a deploy

cloudbed delete (or DELETE /v1/deploys/:id, or the ✕ on the dashboard) permanently removes a deploy: capsule database and stored files are wiped, the slug and extra subdomains are freed, release history is dropped, and bundles nothing else references are garbage-collected. There is no undo — release history dies with the deploy.

API tokens

cloudbed token create mints a bearer token for scripting the platform API — personal (acts as you) or, with --deploy, scoped to a single deploy. Tokens are shown once at creation; list and revoke them with cloudbed token list / cloudbed token revoke <id>.

Extra subdomains

cloudbed domains add <name>.cloudbed.app reserves an additional cloudbed.app subdomain for a deploy (owner or claim-token holder only). Labels are [a-z0-9-], 3–63 characters, and a few are reserved. Auth user ids are pairwise per capsule across all its hostnames, while tokens stay audience-pinned to whichever origin the app is served from.

Limits

Storage and handler caps apply to every deploy; the daily counters apply to anonymous deploys only and disappear on claim.

Limit Value
Deploy payload (bundled server + client + optional SSR) 4 MB
State size 1 MB
State rows 16,384
Serialized row size 64 KB
Rows returned per query 1,000
HTTP endpoint request body 2 MB
Stored file size 1 MB
Stored files per deploy 64 files / 16 MB total
File writes per handler run 8
Handler wall-clock time 10 s
Scheduled jobs per deploy 5
Scheduled job wall-clock time 30 s
Scheduled job run history 50 runs
Web Push subscriptions per deploy 1,000
Web Push sends per handler run 10
Web Push payload JSON 3,500 bytes
Log retention 1,000 entries / 256 KB
Endpoint requests / day (anonymous only) 10,000
Mutations / day (anonymous only) 1,000
Anonymous deploys per IP / day 50

Hitting a quota returns a machine-readable error code, so agents can react programmatically.

Suspension and rate limits

Apps suspended for abuse return HTTP 410 before any capsule code runs. Normal browser requests get a small HTML page saying the app has been suspended; WebSocket upgrades get a plain 410.

The platform also applies short burst limits to anonymous deploy creation, claim attempts, and capsule traffic. Burst rejections return HTTP 429 with Retry-After: 60. These burst limits are separate from the daily anonymous quotas above; the per-day counters remain authoritative for accepted anonymous deploys and capsule usage.

Inspecting a deploy

cloudbed inspect (from the app directory, or with an explicit deploy id) reports the deploy's URL, name, mode, expiry, and scheduled jobs. When you hold the deploy's inspect token, it also includes the list of deployed queries and mutations. Logs written via ctx.log are retrievable the same way. All of it supports --json.

Usage metering

Cloudbed records additive usage metrics for app endpoints, shell and asset serves, stored files, inspect routes, live-query subscriptions, mutations, and publish-triggered query reruns.

Use cloudbed usage [--days N] from a directory with a cloudbed.json binding to see totals and per-day rows. It requires owner auth and supports --json. The HTTP APIs accept an owner token; the dashboard calls the same read-only APIs with its signed-in browser identity.

The per-deploy owner API is:

GET /v1/deploys/:id/usage?days=N
Authorization: Bearer <owner-token>

days defaults to 7 and is clamped to 1–90. asOf is the timestamp when the cached usage body was computed. The JSON response is:

{
  "deployId": "abc123def456",
  "days": 7,
  "asOf": "2026-07-03T12:00:00.000Z",
  "totals": {
    "requests": 0,
    "mutations": 0,
    "queryRuns": 0,
    "publishes": 0,
    "bytes": 0
  },
  "byDay": [
    { "day": "2026-07-03", "requests": 0, "mutations": 0, "queryRuns": 0, "bytes": 0 }
  ]
}

The owner-wide dashboard API uses the same clamp and returns one compact row per deploy that has usage data:

GET /v1/me/usage?days=N
Authorization: Bearer <owner-token>
{
  "days": 7,
  "asOf": "2026-07-03T12:00:00.000Z",
  "deploys": [
    { "deployId": "abc123def456", "requests": 0, "mutations": 0, "queryRuns": 0, "bytes": 0 }
  ]
}

The owner-wide limits API reads each active deploy's current daily counters and storage totals:

GET /v1/me/limits
Authorization: Bearer <owner-token>
{
  "asOf": "2026-07-03T12:00:00.000Z",
  "deploys": [
    {
      "deployId": "abc123def456",
      "mode": "anonymous",
      "today": { "date": "2026-07-03", "requests": 12, "mutations": 1 },
      "quotas": { "requestsPerDay": 10000, "mutationsPerDay": 1000 },
      "state": { "bytes": 4096, "rows": 10, "maxBytes": 1048576, "maxRows": 16384 },
      "files": { "count": 1, "bytes": 2048, "maxCount": 64, "maxBytes": 16777216 }
    }
  ]
}

quotas is null for claimed deploys because daily request and mutation caps apply to anonymous deploys only. State and stored-file caps still apply. This endpoint is cached for about 60 seconds.

Both usage endpoints are served from a server-side cache with a roughly 10-minute TTL, so responses can lag by up to about 10 minutes. The dashboard shows a 7-day usage summary plus daily-quota and storage gauges per app, and displays dashes while usage metering or limits status is unavailable.

Local platform dev returns usage_unavailable until Analytics Engine SQL credentials are configured.