Developer docs  /  Rules & restrictions
The framework

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 into lib/; 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 with browser.
  • Static imports only. import x from './x.js' at the top of the file works; dynamic import('./x.js') throws. Hoist every import.
  • No build step. No TypeScript compile, no JSX transform, no bundler. Ship .js with JSDoc types. For React, use the vendored UMD + htm pattern; 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.verifyHmac against req.rawBody.

Hard caps

These are per-invocation ceilings. They are not plan-dependent and cannot be raised:

LimitValueNotes
CPU time30 sPer invocation; the run is killed at the cap.
Wall clock300 sAbsolute ceiling for any single run.
Memory128 MBPer invocation.
Public HTTP request~60 sThe edge caps a synchronous request; respond or defer before this.
Cron-invoked handler~65 sThe budget for a [[cron]] fire. Chunk long work across fires.
Deferred work~310 sscheduler.now and one-shot scheduler.at get the long budget.
Outbound fetch55 s max30 s default; pass a shorter timeout on user-facing paths.
DB statement5 sPer statement. Paginate instead of scanning.
The pattern behind the numbers: answer HTTP requests fast, and push anything slow to 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 as import { f } from 'lib/util.js' from any handler or tool.
  • Handlers are not importable. Never import one route file from another; move the shared part into lib/.
  • 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.

Deploy validators (what gets rejected)

The deploy pipeline rejects code that fights the platform's defaults. Each rejection points at the canonical alternative.

Missing export const access (most common rejection) — every routed file (api/*.js, pages/*.js, mcp/*.js; not lib/) must declare export const access = 'public' | 'member' | 'admin' | 'scheduler' (tools also allow 'user' but never 'scheduler'). Add the line next to the default export as you write each handler. See skill api/access.

AI keys declared as a [[secret]] — there is no kind field on [[secret]]; declaring one fails the deploy. Declare an [ai] table with required = true instead. [[secret]] is only for app-internal raw values and each entry needs a key. See skill ai/add-an-llm-endpoint.

Build tools (package.json dependency) — rejected: react, react-dom, next, vue, svelte, @sveltejs/kit, nuxt, @remix-run/dev, vite, webpack, rollup, esbuild. Load deps from CDNs at runtime instead — see skill pages/add-react-and-htm-spa.

ORMs (package.json dependency or import anywhere in api//lib/) — rejected: drizzle-orm, prisma, @prisma/client, knex, sequelize, typeorm, mongoose. Use db.query with raw SQL.

Build scripts (package.json scripts.build) — rejected. There is no build step.

TypeScript files in api/ or public/ (.ts, .tsx) — rejected. Use plain .js with JSDoc for types.

npm SDKs that have a Hatchable equivalent — rejected with redirect:

If you reached for… Use this instead
puppeteer, puppeteer-core, playwright browser (browser.html / pdf / screenshot / session)
@anthropic-ai/sdk, openai, ai (Vercel SDK) ai (ai.generateText, ai.streamText, ai.embed) — same shape, gateway routes BYOK
pg, pg-promise, postgres, mysql, mysql2 db (db.query)
sharp, jimp, @squoosh/lib browser.screenshot for HTML→image; for other image work, render server-side via your provider's API
bullmq, bull, bee-queue, agenda scheduler.at(when, route) for one-shot deferred fires; [[cron]] for recurring
ioredis, redis, @upstash/redis db (use a Postgres table — Redis clients aren't available)
nodemailer, @sendgrid/mail, resend, @aws-sdk/client-ses email.send
@aws-sdk/client-s3, aws-sdk, @google-cloud/storage storage.put
fs.writeFile / appendFile / createWriteStream storage.put (no persistent FS)
child_process not available; use browser for headless Chromium

Reserved namespaces:

  • /api/auth/* is reserved unconditionally. Files under api/auth/ are rejected at deploy whether or not [auth] is enabled. Use api/account/, api/identity/, etc. for project-owned auth code.
  • Reserved tables when [auth] is enabled: users, sessions, verifications, passkeys, accounts, invited_users. CREATE TABLE and DROP TABLE on these are rejected; ALTER TABLE to add columns is fine.

For debugging a failing deploy, see skill ops/debug-a-failing-deploy.

Try it without consequences: the 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.