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:
- No outbound
fetch— the capsule server cannot reach the network at all. - No server env —
.env.cloudbed.serveris ignored (with a warning) until the deploy is claimed. - They expire 7 days after the last update. Re-deploying resets the clock; claiming removes it.
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:
POST /__cloudbed/unlockwith JSON{"password": "..."}— returns{"ok": true}plus theSet-Cookieheader to replay.- Send the plaintext password in an
x-cloudbed-passwordheader on any request.
Details worth knowing:
- The password is sent over TLS at deploy time and stored only as a salted PBKDF2 hash;
passwordProtected(a boolean, never the hash) appears in deploy responses and public metadata. - Setting a new password invalidates every outstanding unlock cookie. Open WebSocket connections stay up until they reconnect.
- Owner tooling is unaffected: the inspect routes keep their own bearer auth, so
cloudbed inspect,logs, anddbwork without the visitor password. - Wrong attempts are rate-limited (HTTP 429) per capsule.
- Anonymous and claimed deploys can both use it. It's a visitor gate, not end-user auth — everyone shares one password; use sign-in for per-user identity.
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.