Opinionated, on purpose.
The framework says no to a lot so that every app stays portable, reviewable by an agent, and safe to run next to thousands of others. Everything rejected here has a supported equivalent, and the deploy error tells you which one.
The runtime is a sandbox, not Node
Your handlers run in a locked-down JavaScript sandbox. It executes plain modern JS and gives you the platform's capabilities through one SDK namespace. It deliberately does not give you:
- No
npm install. Dependencies are never installed. Pure-JS libraries can be vendored intolib/; everything infrastructural (database, email, storage, AI, browser) is already in the SDK. - No filesystem writes, no child processes. Persist bytes with
storage.put; run headless Chromium withbrowser. - Static imports only.
import x from './x.js'at the top of the file works; dynamicimport('./x.js')throws. Hoist every import. - No build step. No TypeScript compile, no JSX transform, no bundler. Ship
.jswith JSDoc types. For React, use the vendored UMD +htmpattern; the SDK docs link the recipe. - Crypto is a safe subset. Hashing, HMAC sign/verify, PBKDF2 key derivation (password hashing, up to 1M iterations), and public-key verify (JWTs, webhook signatures) all work. Private-key signing does not. Inbound webhooks should use
webhooks.verifyHmacagainstreq.rawBody.
Hard caps
These are per-invocation ceilings. They are not plan-dependent and cannot be raised:
| Limit | Value | Notes |
|---|---|---|
| CPU time | 30 s | Per invocation; the run is killed at the cap. |
| Wall clock | 300 s | Absolute ceiling for any single run. |
| Memory | 128 MB | Per invocation. |
| Public HTTP request | ~60 s | The edge caps a synchronous request; respond or defer before this. |
| Cron-invoked handler | ~65 s | The budget for a [[cron]] fire. Chunk long work across fires. |
| Deferred work | ~310 s | scheduler.now and one-shot scheduler.at get the long budget. |
| Outbound fetch | 55 s max | 30 s default; pass a shorter timeout on user-facing paths. |
| DB statement | 5 s | Per statement. Paginate instead of scanning. |
scheduler.now(route), which gets five minutes. Deferring to a cron tick does not buy time; cron fires get ~65 s.Sharing code: the lib/ rule
- Shared helpers live once, in the project-root
lib/, imported asimport { f } from 'lib/util.js'from any handler or tool. - Handlers are not importable. Never
importone route file from another; move the shared part intolib/. - Vendoring is for pure-JS only. If a library needs native code, a socket, or the filesystem, it will not run; use the SDK equivalent.
What deploy rejects, and what to use instead
The section below is the same text every connected agent receives in the project manifest, rendered from one source so the two can never disagree. References like see skill point at the recipe library agents read over MCP; the linked docs pages cover the same ground for humans.
dry_run_deploy MCP tool runs every validator and returns the full error list without deploying. Importing a repo runs the same gate and routes any blockers into a guided fixing flow. See import & deploy.